The .NET framework makes application configuration really simple by supplying an out-of-the-box XML configuration file structure and associated classes to access the information contained within it.
By default, it allows configuration key-value pairs to be set and retrieved easily at run time. For example, the following configuration settings:
<appSettings>
<add key="ServerName" value="WinDB1"/>
<add key="DatabaseName" value="MyData"/>
<add key="EnableFeature" value="true"/>
</appSettings>
…can be retrieved using:
var appSettingsReader = new AppSettingsReader();
var serverName = (string)appSettingsReader.GetValue("ServerName", typeof(string));
var databaseName = (string)appSettingsReader.GetValue("DatabaseName", typeof(string));
var enableFeature = (bool)appSettingsReader.GetValue("EnableFeature", typeof(bool));
…or (if you add a reference to System.Configuration):
var serverName = ConfigurationManager.AppSettings["ServerName"];
var databaseName = ConfigurationManager.AppSettings["DatabaseName"];
var enableFeature = bool.Parse(ConfigurationManager.AppSettings["EnableFeature"]);
…although as per a past post of mine, Encapsulated and strongly-typed access to .NET configuration files I don’t recommend that you litter your code with references to AppSettingsReader (or ConfigurationManager), but instead that you wrap all configuration up in a class so that settings are encapsulated away.
It is often necessary to hold more complex configurations though. Many developers I’ve worked with have tried to use the key-value model to store such settings, using a single string with various delimiters to hold separate different values. For instance, given a person construct with an ID, title, first name, last name and birth date attributes, some developers would be tempted to do something like this:
<appSettings>
<add key="Person1" value="I:1|T:Mr|F:Joe|L:Bloggs|D:1980-01-01"/>
</appSettings>
…and then use string split functions to populate a Person class when reading information from the configuration file. While this approach works, it results in brittle, hard to read configuration files and complex string logic to take strings apart.
Instead, it is better to use the exotic configuration capabilities included in the framework. Here is an example demonstrating how you might store configuration information to populate the following Person class:
using System;
namespace ConfigurationExample
{
public class Person
{
public int Id { get; set; }
public string Title { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public DateTime BirthDate { get; set; }
}
}
The configuration file to store such settings might look like this:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<configSections>
<section name="people" type="ConfigurationExample.PeopleConfigurationSectionHandler, ConfigurationExample" />
</configSections>
<people>
<person id="1">
<title>Mr</title>
<firstName>Joe</firstName>
<lastName>Bloggs</lastName>
<birthDate>
<year>1980</year>
<month>1</month>
<day>1</day>
</birthDate>
</person>
<person id="2">
<title>Miss</title>
<firstName>Jane</firstName>
<lastName>Black</lastName>
<birthDate>
<year>1984</year>
<month>2</month>
<day>3</day>
</birthDate>
</person>
</people>
</configuration>
A class implementing IConfigurationSectionHandler must be written to read this exotic configuration file structure; you will need a reference to System.Configuration in order to use this interface. The location of your class is given in the ‘configSections’ declaration at the top of the file. In this case the framework is being instructed to look for a class with name and namespace ‘ConfigurationExample.PeopleConfigurationSectionHandler’ in the ‘ConfigurationExample’ DLL/project in order to read the configuration section named ‘people’.
That class could look something like this:
using System;
using System.Collections.Generic;
using System.Configuration;
using System.Xml;
namespace ConfigurationExample
{
public class PeopleConfigurationSectionHandler : IConfigurationSectionHandler
{
public object Create(object parent, object configContext, XmlNode section)
{
var people = new List<Person>();
foreach (XmlNode node in section.ChildNodes)
{
if (node.NodeType == XmlNodeType.Comment)
{
continue;
}
var id = GetAttributeInt(node, "id");
var title = GetChildNodeString(node, "title");
var firstName = GetChildNodeString(node, "firstName");
var lastName = GetChildNodeString(node, "lastName");
var birthDateNode = GetChildNode(node, "birthDate");
var birthDateYear = GetChildNodeInt(birthDateNode, "year");
var birthDateMonth = GetChildNodeInt(birthDateNode, "month");
var birthDateDay = GetChildNodeInt(birthDateNode, "day");
people.Add(new Person
{
Id = id,
Title = title,
FirstName = firstName,
LastName = lastName,
BirthDate = new DateTime(birthDateYear, birthDateMonth, birthDateDay),
});
}
return people;
}
private static string GetAttributeString(XmlNode node, string attributeName)
{
try
{
return node.Attributes[attributeName].Value;
}
catch
{
var message = string.Format("Could not read attribute named '{0}' in people section of configuration file", attributeName);
throw new ConfigurationErrorsException(message);
}
}
private static int GetAttributeInt(XmlNode node, string attributeName)
{
try
{
var value = GetAttributeString(node, attributeName);
return int.Parse(value);
}
catch
{
var message = string.Format("Could not convert value stored in attribute named '{0}' to an integer", attributeName);
throw new ConfigurationErrorsException(message);
}
}
private static XmlNode GetChildNode(XmlNode parentNode, string nodeName)
{
try
{
return parentNode[nodeName];
}
catch
{
var message = string.Format("Could not find node named '{0}' in people section of configuration file", nodeName);
throw new ConfigurationErrorsException(message);
}
}
private static string GetChildNodeString(XmlNode parentNode, string nodeName)
{
try
{
return parentNode[nodeName].InnerText;
}
catch
{
var message = string.Format("Could not read node named '{0}' in people section of configuration file", nodeName);
throw new ConfigurationErrorsException(message);
}
}
private static int GetChildNodeInt(XmlNode parentNode, string nodeName)
{
try
{
var value = GetChildNodeString(parentNode, nodeName);
return int.Parse(value);
}
catch
{
var message = string.Format("Could not convert value stored in node named '{0}' to an integer", nodeName);
throw new ConfigurationErrorsException(message);
}
}
}
}
Although there is quite a lot of code there, it’s all pretty simple. More importantly, it’s not too brittle and very simple to extend should you wish to add more attributes to the Person class.
The method required by the IConfigurationSectionHandler interface is Create:
public object Create(object parent, object configContext, XmlNode section)
This is what gets called when the framework asks for the contents of the ‘people’ section. The ‘section’ parameter contains the people node from the XML. In essence, the code creates a list in which Person objects are stored, and loops over the child nodes in order to extract the correct information.
There are two important points to note about the code:
1) The code that detects for the presence of XML comments:
if (node.NodeType == XmlNodeType.Comment)
{
continue;
}
…ensures that developers don’t break the code by adding comments into the XML.
2) All the helper methods in the class (e.g. GetAttributeString, GetChildNodeString, etc…) must expect there to be issues with the XML and throw meaningful, self-explanatory exceptions when things do go wrong. When there are problems with these types of sections it is not always obvious what the problem is from the default exceptions .NET throws (particularly if you’re using this technique in the start up code of a Windows Service) so you will save yourself a lot of head scratching further down the line by writing good error messages. The chances are that you will have more than one section handler in your project so it’s a good idea to encapsulate such helper methods away in an abstract ConfigurationSectionHandler base class to cut down on duplication and to promote reuse.
Assuming you have registered your section handlers correctly in the ‘configSections’ declaration, you can retrieve the Person objects as follows:
var people = (IList<Person>)ConfigurationManager.GetSection("people");
Since the IConfigurationSectionHandler interface uses the ‘object’ type as its return value you need to cast the result to the correct type. It would have been nice if the interface was made generic, but I guess you can’t have everything!
If you’re following the advice in my previous post on configuration encapsulation, you’ll probably want to wrap this call up in your ConfigurationFile class, as follows:
using System.Collections.Generic;
using System.Configuration;
namespace ConfigurationExample
{
public class ConfigurationFile
{
private readonly IList<Person> people;
public ConfigurationFile()
{
people = (IList<Person>)ConfigurationManager.GetSection("people");
}
public IList<Person> People
{
get
{
return people;
}
}
}
}
Note that a reference to the Person list is stored when the class constructs to save the running the cast each time you want to retrieve the contents of the configuration file section.
In summary, it’s a little more work to write exotic configuration file sections, but the advantages over brittle, difficult to extend key-value implementations is certainly worth the extra effort.