How to Create a Custom Configuration Section for .NET Configuration File
.NET configuration files are a versatile way for .NET applications to store configuration information. They are hierarchical and use XML syntax. More importantly, they are also extensible fairly easily. You can read more about them on MSDN.
Now, by default, there is an appSettings
section provided by default and this is quite suitable for storing key/value pairs to provide a flat set of data to your application, but what if your configuration is not flat? What if it's fairly complicated with a hierarchy?
Well, in that scenario, the good folks at Microsoft have provided the means to extend the configuration file. Here's how.
The configuration file has the following structure:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<configSections>
<!--
here we have told the .NET Frameworkwhat kind of custom section we are declaring
name: the element name under which it will appear further down the config file
type: the .NET type to which our custom section will be deserialized,
in this particular case it is the
class WorkSection in
namespace BackgroundWorker.Configuration in
assembly BackgroundWorker
-->
<section name="work" type="BackgroundWorker.Configuration.WorkSection, BackgroundWorker" />
</configSections>
<appSettings>
<add key="Foo" value="Bar" />
</appSettings>
<connectionStrings>
</connectionStrings>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
</startup>
<!-- this is our custom section containing our custom configuration settings -->
<work>
<!-- the "tasks" element is a collection of "task" elements in a BasicMap -->
<tasks>
<task name="StartupReactor">
<!-- the "details" element is a collection of "detail" elements in an AddRemoveClearMap -->
<details>
<add name="time" value="2014-01-01T00:00:00" />
<add name="powerOutput" value="50%" />
</details>
</task>
<task name="ProduceElectricity" />
<task name="ShutdownReactor">
<details>
<add name="time" value="2014-12-31T23:59:59" />
</details>
</task>
</tasks>
</work>
</configuration>
All it takes is adding is a few classes to enable this configuration section above.
- WorkSection
- TaskElementCollection
- TaskElement
- DetailElementCollection
- DetailElement
Here's how you would access the above configuration in code.
// work is of type WorkSection which exposes a property
// called Tasks of type TaskElementCollection
var work = (WorkSection)ConfigurationManager.GetSection("work");
// task is of type TaskElement which exposes a property
// called Name of type string and another property
// called Details of type DetailElementCollection
foreach (var task in work.Tasks.Cast<TaskElement>())
{
Console.Out.WriteLine(task.Name);
// detail is of type DetailElement which exposes a property
// called Name of type string and another property
// called Value of type string as well
foreach (var detail in task.Details.Cast<DetailElement>())
{
Console.Out.WriteLine(string.Format("{0}={1}", detail.Name, detail.Value));
}
}
It's all fairly straightforward to use, really.
Here's how the classes mentioned above look in code. Comments are inline.
Configuration\WorkSection.cs
// extend ConfigurationSection to create the top level element of
// our section
public class WorkSection : ConfigurationSection
{
// declare a static field of type ConfigurationProperty for each
// attribute or element you want the section to have
private static ConfigurationProperty tasks;
// declare a ConfigurationPropertyCollection to hold all
// ConfigurationProperty fields we defined above so we can
// return them (as you will see further on)
private static ConfigurationPropertyCollection properties;
// initialize each of the ConfigurationProperty and
// ConfigurationPropertyCollection fields in the static constructor
static WorkSection()
{
// construct each ConfigurationProperty as follows
// the first parameter controls the name of the attribute or element
// the second parameter defines the type of it;
// an intrinsic type can be used for an attribute
// a type derived from ConfigurationElement can be used for sub-elements
// other parameters can be used to specify a converter,
// a validator, and other options
tasks = new ConfigurationProperty("tasks", typeof(TaskElementCollection), null, ConfigurationPropertyOptions.IsRequired);
// stick all of the ConfigurationProperty instances defined above in a collection
properties = new ConfigurationPropertyCollection
{
tasks
};
}
// leverage the collection provided by the base class to
// load and save the actual value of the configuration option
public TaskElementCollection Tasks
{
// use the configuration property defined above as the key
// for the key/value pair, like so
get
{
return (TaskElementCollection)this[tasks];
}
set
{
this[tasks] = value;
}
}
// return the set of configuration properties we have defined
// for this section by overriding the Properties property
protected override ConfigurationPropertyCollection Properties
{
get
{
return properties;
}
}
}
Creating ConfigurationElement
is essentially the same as above, except for the fact that you have to extend ConfigurationElement
rather than ConfigurationSection
, which extends ConfigurationElement
itself!
Here's what TaskElement
looks like:
Configuration\TaskElement.cs
using System;
using System.Collections.Generic;
using System.Configuration;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BackgroundWorker.Configuration
{
public class TaskElement : ConfigurationElement
{
private static ConfigurationProperty name;
private static ConfigurationProperty details;
private static ConfigurationPropertyCollection properties;
static TaskElement()
{
name = new ConfigurationProperty("name", typeof(string), string.Empty, ConfigurationPropertyOptions.IsRequired);
details = new ConfigurationProperty("details", typeof(DetailElementCollection));
properties = new ConfigurationPropertyCollection
{
name,
details
};
}
public TaskElement()
: base()
{
}
protected override ConfigurationPropertyCollection Properties
{
get
{
return properties;
}
}
public string Name
{
get
{
return (string)this[name];
}
set
{
this[name] = value;
}
}
public DetailElementCollection Details
{
get
{
return (DetailElementCollection)this[details];
}
set
{
this[details] = value;
}
}
}
}
And DetailElement
looks like:
Configuration\DetailElement.cs
using System;
using System.Collections.Generic;
using System.Configuration;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BackgroundWorker.Configuration
{
public class DetailElement : ConfigurationElement
{
private static ConfigurationProperty name;
private static ConfigurationProperty value;
private static ConfigurationPropertyCollection properties;
static DetailElement()
{
name = new ConfigurationProperty("name", typeof(string), string.Empty, ConfigurationPropertyOptions.IsRequired);
value = new ConfigurationProperty("value", typeof(string), string.Empty, ConfigurationPropertyOptions.IsRequired);
properties = new ConfigurationPropertyCollection
{
name,
value
};
}
public DetailElement()
: base()
{
}
protected override ConfigurationPropertyCollection Properties
{
get
{
return properties;
}
}
public string Name
{
get
{
return (string)this[name];
}
set
{
this[name] = value;
}
}
public string Value
{
get
{
return (string)this[value];
}
set
{
this[value] = value;
}
}
}
}
Now let's look at the final piece of this puzzle, i.e., a collection of ConfigurationElement
items.
They come in two flavors; well, four actually. There are two aspects, though as follows:
- how a sub element appears in a collection of elements, syntactically speaking
- how a collection is sorted across a hierarchy of config files (remember that .NET config files are hierarchical?)
Syntactically, you could have a basic map or an add/remove/clear map.
A basic map looks like:
<details>
<detail ... />
<detail ... />
<detail ... />
<detail ... />
</details>
An add/remove/clear map looks like:
<details>
<clear />
<add ... />
<add ... />
</details>
Or
<details>
<remove ... />
<add ... />
<add ... />
</details>
And here's how we define an add/remove/clear map:
Configuration\DetailElementCollection.cs
using System;
using System.Collections.Generic;
using System.Configuration;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BackgroundWorker.Configuration
{
public class DetailElementCollection : ConfigurationElementCollection
{
public DetailElementCollection()
: base()
{
}
public override ConfigurationElementCollectionType CollectionType
{
get
{
// set the style of our collection;
// "AddRemoveClearMap" in this case
return ConfigurationElementCollectionType.AddRemoveClearMap;
}
}
protected override ConfigurationElement CreateNewElement()
{
return new DetailElement();
}
protected override object GetElementKey(ConfigurationElement element)
{
var detailElement = (DetailElement)element;
return detailElement.Name;
}
}
}
The basic map is very similar:
Configuration\TaskElementCollection.cs
using System;
using System.Collections.Generic;
using System.Configuration;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BackgroundWorker.Configuration
{
public class TaskElementCollection : ConfigurationElementCollection
{
public TaskElementCollection()
: base()
{
}
public override ConfigurationElementCollectionType CollectionType
{
get
{
// set the style of our collection;
// "BasicMap" in this case
return ConfigurationElementCollectionType.BasicMap;
}
}
// set the name of the sub element;
// "task" in this case
protected override string ElementName
{
get
{
return "task";
}
}
protected override ConfigurationElement CreateNewElement()
{
return new TaskElement();
}
protected override object GetElementKey(ConfigurationElement element)
{
var taskElement = (TaskElement)element;
return taskElement.Name;
}
}
}
I should point out that you could follow the pattern described above for ConfigurationSection
and ConfigurationElement
(defining and exposing a set of ConfgurationProperty
properties), if you wanted something like:
<details foo="bar" clue="less">
....
</details>
I'll wrap this up by saying that this is a very basic description of what you can do to create a custom configuration section. There's a lot more that you can do, but this should get you on your way.
Thanks for reading!