Monday, April 17, 2006
« Utah County .NET User Group - Threading ... | Main | XPath Negative Assertion...Sorta »

I've been doing some experimentation lately regarding application configuration files.  I know that there is a horse out there that is really dead due to repetitive beatings, but I'd like to add my two kicks, er, cents.

In all of your .NET application development, if you've ever done configuration files and deserialized their XML contents to objects you may have encountered the following links (originally created, as I understand it, by Craig Andera):

They're all variations on a theme.  The predominant overriding theme describes how to read in a custom XML chunk from a configuration file and deserialize it into the appropriate object type at runtime.  This effectively renders the XML element and attribute values as properties and collections and makes the configuration file settings readable at runtime.  Rather than having to constantly re-code and re-engineer the details of reading XML elements manually and repetitively, these posts demonstrate a technique that relies on objects found the System.Xml.Serialization namspace to automatically do the work for you.  Very handy indeed.  I've used these exact strategies for a few years in my own work to great success.  All in all, I really feel that the original solution has lots of merit and I really like it.

Essentially, the gist is that you create a ConfigSectionHandler class that implements the IConfigurationSectionHandler interface.  This interface is required by the .NET runtime as the means by which it calls into your library so that you can perform custom work on the XmlNode that it provides in the .Create() method.  The strategy here is to create a generic handler that can resolve the appropriate Type to create for deserialization so that all of the work is done for you by a single handler.  This is accomplished by including a 'type' attribute on the XML element to be deserialized.

A simple example might resemble the following:

app.config

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
   <configSections>
      <sectionGroup name="devstone">
         <section name="settings" type="TestApplication.Configuration.ConfigHandler, TestApplication" />
      </sectionGroup>
   </configSections>
   <devstone>
      <settings type="TestApplication.Configuration.Settings, Testapplication">
         <setting1>Test Value 1</setting1>
         <setting2>Test Value 2</setting2>
      </settings>
   </devstone>
</configuration>

ConfigSectionHandler.cs

namespace TestApplication.Configuration {
   public sealed class ConfigSectionHandler : IConfigurationSectionHandler {
      object IConfigurationSectionHandler.Create(object parent, object configContext, XmlNode section) {
         XPathNavigator nav = section.CreateNavigator();
         string typeName = (string)nav.Evaluate("string(@type)");
         Type type = Type.GetType(typeName);
         if ( null == type )
            throw new ArgumentException(string.Format("Invalid type specification in configuration file: '{0}'", typeName));
         XmlSerializer ser = new XmlSerializer(type);
         return ser.Deserialize(new XmlNodeReader(section));
      }
   }
}

Settings.cs

namespace TestApplication.Configuration {
   [XmlRoot("settings")]
   public sealed class Settings {
     private string _setting1, _setting02;
    
     [XmlElement("setting1")]
     public string Setting1 {
        get { return _setting1; }
        set { _setting1 = value; }
     }
    
     [XmlElement("setting2")]
     public string Setting2 {
        get { return _setting2; }
        set { _setting2 = value; }
     }
   }
}

I got to thinking recently as a simple, side, pet project to see if I might improve upon these methodologies.  A few avenues led to dead-ends while others were more fruitful and enjoyable.  I'd like to present these here, leading up to what I think might be my favorite solution, though the jury's still out on it.  I've got to give it a few more spins around the parking lot first.

First of all, I like to treat each 'configuration section object' as a singleton.  That is, that only one instance of the object should ever exist that represents the settings.  There is absolutely no need to be writing code like this everywhere:

Settings settings = (Settings)ConfigurationSettings.GetConfig("devstone/settings");

To accomplish this I convert my settings class into a singleton following the prescribed pattern for lazy initialization:

Settings.cs

namespace TestApplication.Configuration {
   [XmlRoot("settings")]
   public sealed class Settings {
      public static Settings Instance = SettingsProvider.Instance;
      private sealed class SettingsProvider {
         private SettingsProvider() { }
         internal static Settings Instance { get { return ConfigurationSettings.GetConfig("devstone/settings") as Settings; } }
     }
   
     
// same field and property declarations as before...
   }
}

Note, it's not a true singleton...more on that in a bit.  Now, to reference the settings object I can simply use the simpler, more intuitive code and access the single instance globally across my application:

Settings settings = Settings.Instance;

In fact, this is more akin to the methodology I've used now for some time, but still some things grate on me.

  1. I have to build my configuration XML files explicitly knowing the types.  One type for the <configSections /> section.  Another type for the attribute on the XML element to be deserialized.  I'd rather only need to remember one type.  Also, the 'type' of the object to be deserialized isn't the business of the configuration file nor the eyes of the person viewing it.  How many times do you want to actually be able 'change' the type via the configuration file?  Nonetheless, I enjoy the clean elegance of the aforementioned solution.
  2. While not explicitly heinous, I don't really like having seemingly unnecessary accessors on my deserialized type except for the sole purpose of supporting deserialization (e.g. public 'set' accessors on properties, having to make the 'Settings' type public with a public parameterless constructor (though implicit it may be in this case)).
  3. If there are other members (e.g. properties that aggregate values) I have to explicitly identify them as [XmlIgnore()] so as to not permit them to (de)serialize.  This is trivial in reality, but thought I'd mention it.

I'd like to address these bit by bit.

First of all, let's address the notion of 'Type'.  As I mentioned, I don't really care to have to remember two types (though Scott Weinstein's blog post does address this issue some by generating the XML necessary for you.  I'd rather not have my XML polluted with unnecessary type information.  Therefore, let's move the generic handler into a base class from which the Settings class can derive, thereby providing it's own type information.  This will also necessitate a change to the configuration file to reflect the new type information and to the handler to be an abstract base class:

app.config

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
   <configSections>
      <sectionGroup name="devstone">
         <section name="settings" type="TestApplication.Configuration.Settings, TestApplication" />
      </sectionGroup>
   </configSections>
   <devstone>
      <settings>
      ...
      </settings>
   </devstone>
</configuration>

ConfigSectionHandlerBase.cs

namespace TestApplication.Configuration {
   public abstract class ConfigSectionHandlerBase : IConfigurationSectionHandler {
      object IConfigurationSectionHandler.Create(object parent, object configContext, XmlNode section) {
         try {
            XmlSerializer ser = new XmlSerializer(this.GetType());
            using ( StringReader reader = new StringReader(section.OuterXml) )
               return ser.Deserialize(reader);
         }
         catch ( Exception ) {
            // here, just for example return a clean instance if the object failed to deserialize
            return Activator.CreateInstance(this.GetType());
         }
      }
   }
}

Settings.cs

namespace TestApplication.Configuration {
   [XmlRoot("settings")]
   public class Settings : ConfigurationSectionHandlerBase {
      // same singleton, field, and property declarations as before
   }
}

Now the XML is cleaned up a little bit as no further type information needs to be specified.  However, there is an issue with this code.  Granted it's not very serious, but I view it as a shortcoming and not as clean as it might otherwise be.  The error isn't very obvious.

If you add the following code to the Settings class you'll see what I mean:

public Settings() { Console.WriteLine("Instance created"); }

The C# compiler will automatically provide a default, public constructor for a class that does not have an explicit constructor defined.  By adding this it simply makes it explicit and obvious.  If you were to run an application that consumed this Settings class, you'd see the text "Instance created" appear twice on the console window.  This illustrates two points:

  1. Even though we wrote code to make the class a 'singleton' it's not truly a singleton because it has a default public constructor.  This public constructor is required by the serialization engine so we're kinda stuck with it.  (We'll address this shortly).
  2. The .NET runtime is creating an instance of our class to be able to call the Create() method so that we can in turn create another instance (our 'singleton' instance) and return it.  This is pretty ugly.  Besides, the settings object shouldn't really have to know how to deserialize itself - that responsibility rightfully belongs to someone else.

Therefore, let's get to the final proposed solution.  Please bear with me on this as it's all kinda conceptual here - I've got to experiment a bit more to see if I like it, but I think I do.

SOLUTION:

First of all, let's move the implementation of the settings object (the one that gets deserialized) into an inner class.  Note, it still has to be public, with public get/set accessors on serialized methods, with a public constructor, but we can live with that.  I'll name this class Impl for 'Implementation'.  Ultimately, this type is not generally accessed except by the Settings class; unfortunately, there's nothing really stopping anyone from getting to it.  Peeve: I wish deserialized types could be internal or private.

Second, let's keep our base handler type, but delegate control to another inner class called Handler.  By doing this we can extend the base class to provide further metadata about the type to be deserialized along with some other information.  Additionally, it can be a private class - the runtime won't care in this case.

Third, we redo the Settings class such that it's properties are static readonly properties that redirect to the inner Impl 'singleton' class.

Fourth, we update the configuration file to reference the private, inner Handler class.

app.config

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
   <configSections>
      <sectionGroup name="devstone">
         <section name="settings" type="TestApplication.Configuration.Settings+Handler, TestApplication" />
      </sectionGroup>
   </configSections>
   <devstone>
      <settings>
         <setting1>Test Value 1</setting1>
         <setting2>Test Value 2</setting2>
      </settings>
</configuration>

ConfigSectionHandlerBase.cs

namespace TestApplication.Configuration {
   public abstract class ConfigSectionHandlerBase : IConfigurationSectionHandler {
      protected virtual bool CreateOnError { get { return true; } }
      protected abstract Type DeserializedType { get; }
     
      object IConfigurationSectionHandler.Create(object parent, object configContext, XmlNode section) {
         try {
            XmlSerializer ser = new XmlSerializer(this.DeserializedType);
            using ( StringReader reader = new StringReader(section.OuterXml) )
               return ser.Deserialize(reader);
         }
         catch ( Exception ) {
            // perform logging if you'd like
            if ( this.CreateOnError )
               return Activator.CreateInstance(this.DeserializedType);
            else
               throw;
         }
      }
   }
}

Settings.cs

namespace TestApplication.Configuration {
   public sealed class Settings {
      private Settings() { }
     
      public static string Setting1 { get { return Impl.Instance.Setting1; } }
      public static string Setting2 { get { return Impl.Instance.Setting2; } }
     
      private class Handler : ConfigSectionHandlerBase {
         protected override Type DeserializedType { get { return typeof(Impl) } }
      }
     
      [XmlRoot("settings")]
      public sealed class Impl : ConfigurationSectionHandlerBase {
         public static Impl Instance = ImplProvider.Instance;
         private sealed class ImplProvider {
            private ImplProvider() { }
            internal static Impl Instance { get { return ConfigurationSettings.GetConfig("devstone/settings") as Impl; } }
         }
        
         private string _setting1, _setting02;
        
         [XmlElement("setting1")]
         public string Setting1 {
            get { return _setting1; }
            set { _setting1 = value; }
         }
        
         [XmlElement("setting2")]
         public string Setting2 {
            get { return _setting2; }
            set { _setting2 = value; }
         }
      }
   }
}

Ultimately, I think I like this solution a lot.  It does require a bit more code in the outset, but pays dividends in being much more readable.  Now, all XML serialization responsibilities are delegated directly to the Impl class and any other custom properties and methods can be tied directly to the Settings class as necessary.  Additionally, the implementor doesn't even need to know they're dealing with a single instance, but reference the properties directly:

string s1 = Settings.Setting1;

vs

string s1 = Settings.Instance.Setting1;

I'll have to experiment more with this, but it seems like a pretty cool solution.  What do you think?  I'll be posting a .NET 2.0 version soon, this is all 1.1 - I just wanted to get my thoughts out there.

Monday, April 17, 2006 9:29:00 AM (Mountain Standard Time, UTC-07:00)  #    Disclaimer  |  Comments [1]  |  Trackback