Tuesday, October 21, 2008

Configuration Section Handlers via IConfigurationSectionHandler redux

So, it occurs to me that I was pretty lax with my post last month on Configuration Section Handlers and IConfigurationSectionHandler. Sorry for those of you who came to find information and instead got "wow how easy!" - something of a disservice...

Anyway - IConfigurationSectionHandler is an interface in the System.Configuration namespace that, when implemented, allows your software to define and read custom configuration sections. The idea is that you'll put these configuration sections in your app.config or web.config file and permit configuration that doesn't require recompiling your codebase after making changes.

There is another way to handle custom configuration sections - you could inherit from System.Configuration.ConfigurationSection and provide attributes on your properties that you want to be read from configuration. This approach certainly works - there's a writeup on this approach on MSDN here - but I prefer IConfigurationSectionHandler because it affords me more control over the process, even though apparently its use has been deprecated. =(

The first step before you start touching code is to determine the shape of your XML configuration section. I'll use planets as an (admittedly contrived) example:

    1 <planets>

    2     <planet name="Mercury" distanceFromSun=".38"

    3         diameter="4880" mass="3.30e23" />

    4     <planet name="Venus" distanceFromSun=".72"

    5         diameter="12103" mass="4.869e24" />

    6     <planet name="Earth" distanceFromSun="1"

    7         diameter="12756" mass="5.972e24" />

    8     <planet name="Mars" distanceFromSun="1.52"

    9         diameter="6794" mass="6.4219e23" />

   10     <planet name="Jupiter" distanceFromSun="5.2"

   11         diameter="142984" mass="1.900e27" />

   12     <planet name="Saturn" distanceFromSun="9.54"

   13         diameter="120536" mass="5.68e26" />

   14     <planet name="Uranis" distanceFromSun="19.218"

   15         diameter="51118" mass="8.683e25" />

   16     <planet name="Neptune" distanceFromSun="30.06"

   17         diameter="49532" mass="1.0247e26" />

   18     <planet name="Pluto(?)" distanceFromSun="39.5"

   19         diameter="2274" mass="1.27e22" />

   20 </planets>


This is a pretty straightforward bit of XML. A single root 'planets' element that surrounds nine 'planet' elements, each with some attributes - name, distance from the sun (in astronomical units), diameter in kilometers, and mass in kilograms. The next step is deciding how to represent this in your program. Certainly, you could leave it as XML - there's nothing wrong with that - but in practice I find it more likely you'll want to use an object and its properties to hold and manipulate this data. Here's an example:

    1 public class Planet {

    2     public string Name { get; set; }

    3     public float DistanceFromSun { get; set; }

    4     public int Diameter { get; set; }

    5     public float Mass { get; set; }

    6 

    7     public Planet(string name, float distanceFromSun,

    8         int diameter, float mass) {

    9         Name = name;

   10         DistanceFromSun = distanceFromSun;

   11         Diameter = diameter;

   12         Mass = mass;

   13     }

   14 }


This is pretty straightforward so far. All we need now is a way to go from the XML representation to a collection of Planet instances. That's what IConfigurationSectionHandler is for.

In order to do this, your project will need a reference to System.Configuration, if it doesn't have one already. For our Planets configuration, we'll create a new handler called PlanetsConfigurationHandler and with it implement IConfigurationSectionHandler.

IConfigurationSectionHandler is a very small interface. It contains only one method to implement:
object Create(object parent, object configContext, XmlNode section);
As you can see this method is passed a parent object, a context object, and an XmlNode which represents the section itself. Now, I'm going to be completely honest and say that I have no idea what parent and configContext are for. The section parameter is the only one I'm interested in, and it's the only one I've ever used, so I'm going to say it's pretty safe to ignore them for now.

A word of caution. The Configuration system that is responsible for calling your handler will assume that you're not storing any state in your handler, and that it is thread-safe. This means that you really shouldn't use any external or internal state in the body of the Create method. You should assume that your handler will be called multiple times per instance, in random order.

So by this point, all you need to do is write your XML parsing code. For the sake of sanity and simplicity, I'm going to convert this old 1.0-style XmlNode object into a 3.5-style XDocument object, and work with it from there. I prefer to keep up with the current developments in programming languages. If you can't use 3.5 for whatever reason... well, there are plenty of System.Xml references available out there. You're resourceful, you'll find something. ;)

Here's my handler:

    1 public class PlanetsConfigurationHandler

    2     : IConfigurationSectionHandler {

    3 

    4     public object Create(

    5         object parent, object configContext, XmlNode section) {

    6 

    7         XDocument doc = XDocument.Parse(section.OuterXml);

    8         XElement root = (XElement)doc.FirstNode;

    9 

   10         IList<Planet> rList = new List<Planet>();

   11 

   12         foreach (var element in root.Elements() ) {

   13             if (element.Name != "planet")

   14                 throw new ConfigurationErrorsException(

   15                     "planets section only accepts" +

   16                     " 'planet' elements.");

   17 

   18             try {

   19                 string name = element.Attribute("name").Value;

   20                 float distanceFromSun =

   21                     float.Parse(

   22                         element.Attribute("distanceFromSun").Value);

   23                 int diameter =

   24                     int.Parse(

   25                         element.Attribute("diameter").Value);

   26                 float mass =

   27                     float.Parse(

   28                         element.Attribute("mass").Value);

   29 

   30                 Planet newPlanet =

   31                     new Planet(name, distanceFromSun, diameter, mass);

   32 

   33                 rList.Add(newPlanet);

   34             } catch (Exception ex) {

   35                 throw new ConfigurationErrorsException(

   36                     "Error reading planet element."

   37                     , ex);

   38             }

   39         }

   40 

   41         return rList;

   42     }

   43 }


Please excuse the weird formatting - I don't want the code lines to wrap.

So - pretty straightforward. I take the XML section data and using the XDocument elements I parse it for its content. A couple things to notice here: I'm assuming that the root element is correctly named 'planets' - in fact, it's possible it'll be named something else - you'll see how later - so I'm trusting here that the configuration system has passed me the correct section. We'll go with that for now. Second, I am doing some validation on the inner xml - none will be done for me - and I'm making sure that the only thing in my configuration section are 'planet' elements. Third, I've wrapped the rest of the body of the iterator in a try/catch block. I'm using .Parse methods which will throw an exception if the string they're trying to parse is null or malformed - I catch that exception, then throw a ConfigurationErrorsException, passing in the original exception in its constructor to preserve context. This will allow callers of your library to understand what went wrong, when something does.

We've got one last thing to do in order to actually *use* this handler. You've got to register it with the configuration system by adding a bit of XML in the 'Configuration' section:

    1 <configSections>

    2     <section

    3       name="planets"

    4       type="ConfigDemo.PlanetsConfigurationHandler, ConfigDemo"/>

    5 </configSections>


This should be pretty much self-explanatory. This bit of XML tells the configuration system that you've got a custom handler that will handle any sections named 'planets'. Earlier I mentioned that it's possible to use an arbitrary name when dealing with a configuration section. Well, this is where that name is selected. If you change the 'name' attribute's value here, you'll have to use the same value for the name of the section itself - that is, the root element of the section's XML. You'll also need to use that name when you retrieve the section itself - which brings me to my final bit of code:

    1 class Program {

    2     static void Main(string[] args) {

    3         var planets =

    4             (IList<Planet>)

    5             ConfigurationManager.GetSection("asdasd");

    6 

    7         string planetTemplate = "{0}: {1}au, {2}km, {3}kg";

    8 

    9         foreach (var planet in planets) {

   10             Console.WriteLine(

   11                 string.Format(planetTemplate,

   12                 planet.Name,

   13                 planet.DistanceFromSun,

   14                 planet.Diameter,

   15                 planet.Mass)

   16             );

   17         }

   18 

   19         Console.ReadLine();

   20     }

   21 }


This is a console application that retrieves the configuration section 'planets' from App.config, then displays the planets on the console. Very simple. Notice that we need to cast the return from ConfigurationManager.GetSection to the type we expect to receive - that method returns 'object', which is to be expected since it couldn't possibly know what type to expect your handler to return. Once you've got your data out of configuration, it's up to you to decide what to do with it.

So, that's that. I do however have one last thing to say. I've been getting into functional programming quite a bit, and as such I like to exercise those newly-forming muscles any chance I can get. As I mentioned above, the Create method needs to avoid internal or external state. Any time it is given an input (the xml configuration section) it should provide the same output. This sounds an awful lot like it needs to be a pure function, to me - so let's change the method to make it a little more obvious that that's what we're going for:

    1 public class PlanetsConfigurationHandler

    2     : IConfigurationSectionHandler {

    3 

    4     public object Create(

    5         object parent, object configContext, XmlNode section) {

    6         XDocument doc = XDocument.Parse(section.OuterXml);

    7         XElement root = (XElement)doc.FirstNode;

    8 

    9         if (root.Descendants().Any(e => e.Name != "planet"))

   10             throw new ConfigurationErrorsException(

   11                 "planets section only accepts" +

   12                 " 'planet' elements.");

   13 

   14         try {

   15             return

   16                 (from p in root.Descendants()

   17                 let newPlanet = new Planet(

   18                     p.Attribute("name").Value,

   19                     float.Parse(p.Attribute("distanceFromSun").Value),

   20                     int.Parse(p.Attribute("diameter").Value),

   21                     float.Parse(p.Attribute("mass").Value)

   22                 )

   23                 select newPlanet).ToList();

   24         } catch (Exception ex) {

   25             throw new ConfigurationErrorsException(

   26                 "Error reading planet element."

   27                 , ex);

   28         }

   29     }

   30 }


Whoa. Way different - and yet the functionality is the same. Much fewer lines of code here, as well. I'll leave it as an exercise to the reader to work this out, if it's not already apparent - it's not a very tough query, really. I have faith. :)

Good luck with your own IConfigurationSectionHandler implementations - feel free to shoot me any questions you may have about the process, and I'll answer as best I can.


kick it on DotNetKicks.com

2 comments:

Unknown said...

great post! the interface route is deprecated though. there's a new way of doing this in .net 2.0. i have an example at http://devpinoy.org/blogs/jakelite/archive/2009/01/10/iconfigurationsectionhandler-is-dead-long-live-iconfigurationsectionhandler.aspx

cheers!

Ronnie Overby said...

Extremely helpful. Thanks for a great post.