ONDotNet.com    
 Published on ONDotNet.com (http://www.ondotnet.com/)
 See this if you're having trouble printing code examples


Configuration Handlers in .NET

by Ben Lowery
03/17/2003

Extending the CLR Configuration System

The registry is dead! Long live the registry! In the shiny new world of .NET-based applications with xcopy deployment and smart clients, we can no longer use the registry to hold application configuration information. Instead, we're supposed to use XML-based configuration files accessed via the CLR's pluggable configuration framework, System.Configuration, which lives in the System assembly. This article will introduce you to the configuration system and show you how to extend it using custom configuration section handlers.

Overview of the Configuration System

To start, let's take a look at how the configuration system works and for what it was intended. The configuration system works by reading in settings from specially-named XML files. For console and WinForms applications, the file has the same name as the executable, plus a .config extension. For example, if you had an application named MyApp.exe, the config file would be named MyApp.exe.config. For ASP.NET applications, the config file is always named web.config. Either way, configuration files are meant to be read-only, and you'll find nothing in the configuration framework that helps you write to configuration files.

Configuration files are meant for application-level settings that rarely change after installation, not user-level settings like window placement or favorite color. Per-user settings should be stored elsewhere, preferably in an application-specific folder under the user's Application Data folder, inside of Isolated Storage, or even inside of the dreaded registry (under HKEY_CURRENT_USER/Software/YourCompany/). That said, if we did need to modify one, configuration files are just XML files. We can use the standard facilities present in the System.Xml assembly to do whatever it is we need to do. Be warned, this may not always work out. For applications deployed using HTTP (like Chris Sells' Wahoo app), we would not have any way to write out the configuration file. Also, if we do write settings back to our config file from inside of an ASP.NET application, the ASP.NET application will be restarted.

Configuring Configuration

Moving right along, let's take a look at how a configuration file is structured. Configuration files are broken up into two main parts. The first part, contained within a <configSections> element, is really metadata that the framework uses to determine how to parse the remainder of the file. Let's take a look at a sample layout:

<configuration>

  <configSections>

    <sectionGroup name="blowery.org">
    
      <section name="basics"
               type="BasicConfigSample.SectionHandler, BasicConfigSample"/>

    </sectionGroup>

  </configSections>

  <blowery.org>
  
    <basics>
    
      <firstName>Jack</firstName>
    
      <lastName>Hoya</lastName>
    
    </basics> 
  
  </blowery.org>

</configuration>

Here we have one <sectionGroup> which contains one <section>. The <section> has a name and a type. The name specifies the name of the XML element containing the configuration section; the type specifies the class that will be used to parse the configuration section. The <sectionGroup> can group different sections together under a common parent element. In the sample above, we're stating that we have a base element, <blowery.org>, and within that base we have a section, <basics>. When the framework parses the <basics> section, it should use the BasicConfigSample.SectionHandler class from the BasicConfigSample assembly.

To kick off the parser and access the settings for a configuration section, we call ConfigurationSettings.GetConfig("sectionName"). For the example above, the call would be ConfigurationSettings.GetConfig("blowery.org/basics"). Notice that we have to specify the path down to the section when we ask for the settings.

Given this little snippet of configuration file, let's go over what's happening when we call ConfigurationSettings.GetConfig("blowery.org/basics"). First, the configuration system parses the <configSections> looking for <section> tags. For each <section> found, the configuration system creates an instance of the type specified in the type attribute and associates the configuration path with the instance. The type must implement the IConfigurationSectionHandler interface. In this case, the framework instantiates a BasicConfigSample.SectionHandler, casts it to a IConfigurationSectionHandler, and associates the instance with the path blowery.org/basics. When we call GetConfig(), Create() is called and the framework passes on the return value to our calling program.

If any nodes are found that don't match an IConfigurationSectionHandler instance, a ConfigurationException is thrown, and processing halts. We could catch this exception, but we generally wouldn't want to. The error may or may not be due to a misconfiguration of your portion of the configuration file, so catching it and eating it wouldn't really be safe. If we do catch it, we should at least rethrow it.

Implementing the Interface

Now we need to create a simple object that implements IConfigurationSectionHandler and use it from our code. First, here's the definition of the interface:

public interface IConfigurationSectionHandler {
  object Create(object parent, 
                object configContext, 
                XmlNode section);  
}

To implement the interface, all we need is a Create() method that takes three parameters. That seems pretty easy. Before we implement it, let's go over the parameters and the return value.

The first parameter, parent, is typed as System.Object. The parent is used when we need to support chains of configuration files, which we're not going to do until later. For now, we'll ignore this one. The second parameter, configContext, is currently only used when the IConfigurationSectionHandler is being used by an ASP.NET application. When the handler is called by ASP.NET, this instance will be an HttpConfigurationContext. Again, we're going to ignore this for the time being. The last parameter, section, is an XmlNode that represents the configuration section. In our case, the XmlNode will point to the <basics> element. Finally, when we're done, we need to return an object that represents our configuration settings. We should define a type for our settings (we'll use BasicSettings) and document that our implementation returns that type. Here's our implementation:

using System;
using System.Configuration;
using System.Xml;

namespace BasicConfigSample
{
  public class SectionHandler : IConfigurationSectionHandler
  {
    /// <summary>Returns a BasicSettings instance</summary>
    public object Create(object parent, 
                         object context, 
                         XmlNode section) {
      string f = section["firstName"].InnerText;
      string l = section["lastName"].InnerText;
      return new BasicSettings(f,l);
    }
  }
  
  public class BasicSettings
  {
    internal BasicSettings(string first, string last) {
      FirstName = first;
      LastName = last;
    }
  
    public readonly string FirstName;
    public readonly string LastName;

    public override string ToString() {
      return FirstName + " " + LastName;
    }
  }
}

As stated before, to use the section handler, we need to ask the ConfigurationSettings object for the proper config section:

using System;
using System.Configuration;

namespace BasicConfigSample 
{
  class EntryPoint 
  {
    const string mySection = "blowery.org/basics";

    [STAThread]
    static void Main(string[] args) {
      BasicSettings settings;
      settings = (BasicSettings)ConfigurationSettings.GetConfig(mySection);
      Console.WriteLine("The configured name is {0}", settings);
    }
  }
}

And there we have it, our very own custom section handler, an equal citizen with all of the other section handlers supplied by the framework.

Missing Settings and Defaults

So far, we've only talked about the case where everything exists and is configured properly. What happens if the configuration settings are not present in the config file? To illustrate, what happens if we have a config file that looks like this?

<configuration>

  <configSections>

    <sectionGroup name="blowery.org">
    
      <section name="basics"
               type="BasicConfigSample.SectionHandler, BasicConfigSample"/>

    </sectionGroup>

  </configSections>
  
  <!-- missing section!! -->

</configuration>

If the configuration system cannot find a node that matches the path we asked for, it does not call Create() on the section handler and ConfigurationSettings.GetConfig() simply returns null. Returning null is a bit of a pain. Any place we call GetConfig(), we'll have to check the return value for null and do the right thing, loading defaults if necessary. That's rather error-prone, but we can wrap this up to make it easier to use. A factory method on the BasicSettings class that checked for null and loaded a default, if necessary, would do the trick. We'll move the code that grabs the settings object from the EntryPoint to our new factory method and rewrite the EntryPoint to use the new factory method:

using System;
using System.Configuration;
using System.Xml.Serialization;
using cs = System.Configuration.ConfigurationSettings;

namespace BasicConfigSample
{

  public class BasicSettings
  {
    /* same as before */

    private BasicSettings() {
      FirstName = "<<not";
      LastName = "set>>";
    }
    
    const string section = "blowery.org/basics";   

    public static BasicSettings GetSettings() {
      BasicSettings b = (BasicSettings)cs.GetConfig(section);
      
      if(b == null)
        return new BasicSettings();
      else
        return b;
    }
  }
	
  class EntryPoint 
  {
    [STAThread]
    static void Main(string[] args) {
      BasicSettings settings = BasicSettings.GetSettings();
      Console.WriteLine("The configured name is {0}", settings);
    }
  }
	
  /* SectionHandler stays the same */
}

The astute reader might notice that the default instance looks like a prime candidate for becoming a Singleton. Luckily, the configuration framework already caches the result of the call to IConfigurationSectionHandler.Create(), so it's one less piece we have to implement.

Configuration Parenting

So far, we've covered how to implement a very simple section handler and how to wrap up the calls to GetConfig() to get around the null return problem. Next, we're going to dive into configuration parenting and discuss how it affects our custom section handler.

Remember how an element in the configuration file that has no matching <section> tag causes the configuration system to thrown a ConfigurationException? If you've played around with web.config files, you may be wondering how the <system.web> section works; no <configSections> or <section> elements are present, so how does the configuration system know which IConfigurationSectionHandler to use? The answer lies in configuration file parenting.

When the configuration system parses our configuration file, it also parses a master configuration file, stored in a file called machine.config, which lives in the Config folder of your framework install directory. Open the file up; contained within is a long list of <sectionGroup> and <section> tags at the top of the file. When the configuration system can't find a section handler in your configuration file, it walks up to machine.config and checks there. If you decide to register your section handler in machine.config, you should seriously consider strongly naming the assembly and registering it with the GAC. That way, anyone who looks in machine.config can use your configuration handler. Strictly speaking, you don't have to register your assembly in the GAC, but it's a good idea.

The machine.config file can also hold machine-wide default settings. If you search machine.config for <system.web>, you'll find all of the defaults used by ASP.NET. Changes to this file would affect all of the ASP.NET applications running on that machine.

So what does all this mean to the lowly developer implementing IConfigurationSectionHandler? Simply, it means that we may have to parse and merge settings from different config files. In fact, for ASP.NET applications, Create() can be called many times, once for each directory above the ASP.NET page in question that defines a web.config file, plus possibly once more for machine.config. For example, if we defined our configuration section handler in machine.config and had the IIS layout shown below, our configuration handler would be called four times.

Multiple web.config file diagram
Multiple web.config file diagram

A couple interesting things about the ASP.NET implementation: First, if a web.config in the hierarchy doesn't contain settings, it will be skipped, and the next config file in the hierarchy will be checked. Second, there's a discrepancy between the ASP.NET configuration system and the DefaultConfigurationSystem used by console and WinForms applications. If a section is redefined in a child configuration file, ASP.NET deals with it and doesn't throw an error. However, a console or WinForms app will throw a ConfigurationException, stating that the section in question has already been defined. I rather like the ASP.NET approach; it supports xcopy deployment (I don't have to know if the section handler is already registered) and just does what I would expect. At the very least, it would be nice if the framework teams resolved this difference before v1.1 gets released. Anyway, back to how parenting affects implementing the interface.

When the configuration system finds a configuration element in machine.config and in your local config file, it first calls Create() using the XmlNode in machine.config, then calls Create() using the XmlNode in our config file. When it calls Create() for our local file, it passes in the object returned from the call to Create() on machine.config's XmlNode. We are expected to do the right thing when it comes to merging the current node with the parent settings. The chaining always starts with machine.config and walks down the directory tree.

Our little section handler from before isn't well-suited for interesting override behavior, so let's write a new one. This one will sum the value attribute of a <sum> element. Also, instead of looking for blowery.org/code, we'll look for blowery.org/sum.

using System;
using System.Configuration;
using System.Xml.Serialization;
using cs = System.Configuration.ConfigurationSettings;

namespace ParentingSample
{
  public class Settings 
  {
    const string section = "blowery.org/sum";

    private int sum;

    internal Settings(int start) {
      sum = start;
    }

    private Settings() {
      sum = 0;
    }

    public int Total { 
      get { return sum; } 
    }

    internal int Add(int a) { 
      return sum += a; 
    } 
        
    public override string ToString() {
      return Total.ToString();
    }

    public static Settings GetSettings() {
      Settings b = (Settings)cs.GetConfig(section);
      
      if(b == null)
        return new Settings();
      else
        return b;
    }  
  }
	
  class SectionHandler : IConfigurationSectionHandler
  {
    public object Create(object parent, 
                         object context, 
                         XmlNode section) 
    {
      int num = int.Parse(section.Attributes["value"].Value);
      
      if(parent == null)
        return new Settings(num);

      Settings b = (Settings)parent;
      b.Add(num);

      return b;
    }
  }
}

Notice the new code in the SectionHandler. If parent is not null, we cast it to a BasicSettings and call Add() with the parsed value. Here, we handle merging the current node with the parent settings. Otherwise, we start the chain by creating a new BasicSettings initialized with the first number.

To test this code, we'll need the setting in two config files. In machine.config, we'll register the section handler and base setting like this:

<configuration>
  <configSections>
    <sectionGroup name="blowery.org>
      <section 
        name="sum" 
        type=""ParentingSample.SectionHandler, ParentingSample"/>
    </sectionGoup>
    
    <!-- other sections -->
  
  </configSections>
  
  <blowery.org>
    <sum value="10"/>
  </blowery.org>
  
  <!-- other settings -->
  
</configuration>

In our local application config file, we'll set up another value like this:

<configuration>
  <!-- section already registered in machine.config -->
  <blowery.org>
    <sum value="5"/>
  </blowery.org>
</configuration>

Now, if we ran the program, we should see a result of 15. Pretty neat. This example is pretty simple, but it does show you the basics of how to grab the parent settings and merge them with the local settings. The most difficult thing here was deciding how our settings should merge with their parents.

Where Are We?

We've covered quite a bit about implementing a custom configuration section handler. There are some other techniques that come in handy when working with configuration files that we have not outlined here. For example, the System.Xml.Serialization namespace in the System.Xml assembly can radically simplify the parsing code for a configuration section. Also, take a good look at machine.config for examples of how to structure your configuration to support parenting and overrides in a flexible, robust manner. ASP.NET does a wonderful job of this and a lot can be learned by studying how it handles parenting and overrides between machine.config and a web.config file. Thanks for reading, and I hope you learned a lot from the article. If you'd like to download the accompanying sample code for this article, you can grab the .zip file from www.blowery.org/code/ConfigurationSamples.zip. If you have questions, feel free to contact me via email.

Related Links

Ben Lowery is a developer at FactSet Research Systems, where he works on all things great and small.


Return to ONDotnet.com

Copyright © 2009 O'Reilly Media, Inc.