WindowsDevCenter.com
oreilly.comSafari Books Online.Conferences.

advertisement


AddThis Social Bookmark Button

Configuration Handlers in .NET
Pages: 1, 2

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