Cfg-NET is a configuration library for .NET with built-in validation and error reporting.
Support for XML and JSON configurations are built-in. An example of fruits with their colors is provided below.
<cfg id="1">
<fruit>
<add name="apple">
<colors>
<add name="red" />
<add name="yellow" />
<add name="green" />
</colors>
</add>
<add name="banana">
<colors>
<add name="yellow" />
</colors>
</add>
</fruit>
</cfg>
In XML, collections are named elements containing nested <add/>
elements. The collection elements may not contain attributes. Only the root (e.g. <cfg/>
) and the <add/>
elements may have attributes.
Here's the same in JSON:
{
"id" : 1,
"fruit": [
{
"name":"apple",
"colors": [
{"name":"red"},
{"name":"yellow"},
{"name":"green"}
]
},
{
"name":"banana",
"colors": [
{"name":"yellow"}
]
}
]
}
In JSON, collections are named arrays of objects. The collections may not have properties. Only the root object or objects within a collection array may have properties.
In code, a corresponding C# model for fruits and their colors looks like this:
using System.Collections.Generic;
class Cfg {
public int Id {get; set;}
public List<Fruit> Fruit { get; set; }
}
class Fruit {
public string Name { get; set; }
public List<Color> Colors {get; set;}
}
class Color {
public string Name {get; set;}
}
To enable configurations, have each class
inherit CfgNode
and decorate the properties
with the Cfg
custom attribute:
using System.Collections.Generic;
using Cfg.Net;
class Cfg : CfgNode {
[Cfg]
public int Id { get; set;}
[Cfg]
public List<Fruit> Fruit { get; set; }
}
class Fruit : CfgNode {
[Cfg]
public string Name { get; set; }
[Cfg]
public List<Color> Colors {get; set;}
}
class Color : CfgNode {
[Cfg]
public string Name {get; set;}
}
Inheriting from CfgNode
provides base methods:
Load()
for loading or checking a configurationErrors()
andWarnings()
to check afterLoad
is calledSerialize()
to get a text representation
The Cfg
attribute adds validation and modification
instructions. It has these options:
- validation
required
unique
domain
withdelimiter
andignoreCase
optionsminLength
and/ormaxLength
minValue
and/ormaxValue
regex
withignoreCase
option
- transformation
value
, as in default valuetoLower
ortoUpper
trim
,trimStart
, ortrimEnd
To make sure some fruit is defined in our configuration, we
would add required=true
to the fruit list like this:
class Cfg : CfgNode {
[Cfg(required=true)] // THERE MUST BE SOME FRUIT!
public List<Fruit> Fruit { get; set; }
}
If we want to make sure the fruit names are unique, we could
add unique=true
to the fruit name attribute like this:
class Fruit : CfgNode {
[Cfg(unique=true)] // THE FRUIT MUST BE UNIQUE!
public string Name { get; set; }
[Cfg]
public List<Color> Colors {get; set;}
}
If we want to control what colors are used, we could
add domain="red,green,etc"
to the color name attribute like this:
class Color : CfgNode {
[Cfg(domain="red,yellow,green,blue,purple,orange")]
public string Name {get; set;}
}
Load the configuration into the model like this:
// let xml be your configuration
var cfg = new Cfg();
cfg.Load(xml);
After loading, always examine your model for any
issues using the Errors()
and Warnings()
methods:
//LOAD CONFIGURATION
var cfg = new Cfg();
cfg.Load(xml);
/* CHECK FOR WARNINGS */
Assert.AreEqual(0, cfg.Warnings().Length);
/* CHECK FOR ERRORS */
Assert.AreEqual(0, cfg.Errors().Length);
/* EVERYTHING IS AWESOME!!! */
By convention, an error means the configuration is invalid. A warning is something you ought to address, but the program should still work.
Errors and warnings should be reported to the end-user so they can fix them. Here are some example errors:
Remove the required fruit and...
fruit must be populated in cfg.
Add another apple and...
Duplicate name value apple in fruit.
Add the color pink...
An invalid value of pink is in name. The valid domain is: red, yellow, green, purple, blue, orange.
If Cfg-NET doesn't report issues, your configuration is valid. You can loop through your fruits and their colors without a care in the world:
var cfg = new Cfg();
cfg.Load(xml);
foreach (var fruit in cfg.Fruit) {
foreach (var color in fruit.Colors) {
/* use fruit.Name and color.Name... */
}
}
You never have to worry about a Cfg
decorated list being null
because it is initialized as the configuration loads. Moreover,
if you set default values (e.g. [Cfg(value="default")]
), a
property is never null
.
Play with the apples and bananas on .NET Fiddle.
The Cfg
attribute's optional properties
offer simple validation and transformation.
If it's not enough, you have options:
- Overriding
PreValidate()
- Overriding
Validate()
- Overriding
PostValidate()
If you want to modify a configuration before
validation, override PreValidate()
like this:
protected override void PreValidate() {
if (Provider == "Bad Words") {
Provider = "Good Words";
Warn("Watch your language!");
}
}
To perform validation involving more than
one property, override Validate()
like this:
public class Connection : CfgNode {
[Cfg(required = true, domain = "file,folder,other")]
public string Provider { get; set; }
[Cfg()]
public string File { get; set; }
[Cfg()]
public string Folder { get; set; }
/* CUSTOM VALIDATION */
protected override void Validate() {
if (Provider == "file" && string.IsNullOrEmpty(File)) {
Error("file provider needs file attribute.");
} else if (Provider == "folder" && string.IsNullOrEmpty(Folder)) {
Error("folder provider needs folder attribute.");
}
}
}
When you override Validate
, add issues using
the Error()
and Warn()
methods.
Overriding PostValidate
gives you an opportunity
to run code after validation. You may check Errors()
and/or Warnings()
and make further preparations.
protected override void PostValidate() {
if (Errors().Length == 0) {
/* make further preparations... */
}
}
If the attributes and methods aren't enough,
you may inject customizers (e.g. things
implementing ICustomizer
) into
your model's contructor.
After your configuration is loaded into code, you
can serialize it back to a string with Serialize()
.
// load
var cfg = new Cfg();
cfg.Load(xml);
// modify
cfg.Fruit.RemoveAll(f => f.Name == "apple");
cfg.Fruit.Add(new Fruit {
Name = "plum",
Colors = new List<Color> {
new Color { Name = "purple" }
}
});
// serialize
var result = cfg.Serialize();
This produces a result of:
<cfg>
<fruit>
<add name="banana">
<colors>
<add name="yellow" />
</colors>
</add>
<add name="plum">
<colors>
<add name="purple" />
</colors>
</add>
</fruit>
</cfg>
Note: If you loaded XML, it serializes to XML. If you loaded JSON, it serializes to JSON.
Sometimes you need to write a configuration in
code. If you do this, be sure to call Load()
without parameters.
var cfg = new Cfg {
Fruit = new List<Fruit> {
new Fruit {
Name = "Apple",
Colors = new List<Color> {
new Color {Name = "red"},
new Color {Name = "aqua"}
}
}
}
};
// Call Load() to check for errors and warnings
cfg.Load();
// I put an error in there on purpose (hint: aqua is invalid)
Assert.AreEqual(1, cfg.Errors().Length);
So, if you need configurations for your programs, give Cfg-NET a try. I use it in all the programs I write, and I am very happy with it. Thank you for taking the time to read this. I appreciate the stars and feedback.