Welcome to blogs.conchango.com Sign in | Join | Help

Welcome to blogs.conchango.com

Paulo Reichert's Blog

Configuration Management in .NET 2.0

Among the myriad of very cool features that .NET 1.0 introduced when it was first launched was the support for XML configuration files. It was really nice to be able to configure the application “on-the-fly” by using these config files and having the application catching up the new settings as soon as the XML configuration was saved to the hard drive. Other than the Framework’s settings, you could add your own settings, easily extending the config file to support the needs of your applications.

Sometimes this power is a bit overlooked, but if we consider the options we had before .NET, which were the Windows Registry, with all its messy API, INI files with all its dumbness and custom made config files with all its complexity, the .NET solution was remarkably clean and elegant. However, there was one thing still missing in the configuration stuff: It wasn’t easy to change these config files at runtime.

Now Microsoft has evolved the configuration model to a state that not only allows you to better define your configuration settings in the config file with much more complex constructs, but also allows you to read and write these settings using a very powerful object model.

Today what I’m going to show you is how to write an application that generates its own configuration settings, save it to the config file and subsequently loads these settings from the config file for application consumption.

The code I will show you below is actually production code on my newest pet project, named NGroups, which is an open source networking toolkit. I will talk more about that in the near future.

The configuration I need for such a project is something like the piece of XML below:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <
ngroups enabled="true">
        <
channel defaultProvider="NChannelProvider">
           
<providers>
                <
provider name="NChannelProvider" type="NGroups.NChannelProvider, NGroups.Core" />
           
</providers>
            <
protocols>
                <
protocol ref="UDP" properties="(mcast_addr=228.1.2.3;mcast_port=45566;ip_ttl=32)" />
               
<protocol ref="PING" properties="(timeout=3000;num_initial_members=6)" />
               
<protocol ref="FD" properties="(timeout=3000)" />
                <
protocol ref="VERIFY_SUSPECT" properties="(timeout=1500)" />
            </
protocols>
        </
channel>
        <
protocols>
            <
protocol name="UDP" type="NGroups.Protocols.UdpProtocol, NGroups.Core" />
            <
protocol name="PING" type="NGroups.Protocols.PingProtocol, NGroups.Core" />
           
<protocol name="FD" type="NGroups.Protocols.FdProtocol, NGroups.Core" />
           
<protocol name="VERIFY_SUSPECT" type="NGroups.Protocols.VerifySuspectProtocol, NGroups.Core" />
           
<protocol name="UNICAST" type="NGroups.Protocols.UnicastProtocol, NGroups.Core" />
       
</protocols>
    </
ngroups>
</
configuration>

My steps to create the configuration code were the following: In the first place, I had to know which settings I needed for my application. Once defined the code I needed, I devised an XML format that better represented such settings. I have then written my sample XML file containing the sample configuration I wanted to achieve from the application and then started designing a configuration model that resembled that XML document in code.

The .NET Framework 2.0 implements three classes and a number of attributes that are relevant to the configuration work. The first class is the System.Configuration.ConfigurationSection. This class represents a major section within the configuration file and is most of all a container for all the configuration settings. The next class and probably the most important is the System.Configuration.ConfigurationElement, which represents a configuration element or in other words, the stuff contained within a configuration section. The third class here is the System.Configuration.ConfigurationElementCollection, which represents a collection of similar configuration elements.

.NET Framework configuration classes

On the picture above you can see the inheritance chain for the classes defined in the framework.

On the picture below, you can see how these classes map to the configuration I want to achieve on my application, which makes very easy to understand the code as we write it:

Configuration Mapping

I have then derived the following object model for the configuration settings:

NGroups Configuration Object Model

The model may look complex at first glance, but actually it is very simple. Below is an explanation of the actual code.

All the classes are in the namespace NGroups.Configuration. The first one I want to take a look at is the NGroupsSection class, as it is the top level on our config code. The class is implemented as follows:

public sealed class NGroupsSection : ConfigurationSection
{
    #region Private Static Variables
    private static readonly ConfigurationProperty _propEnabled;
    private static readonly ConfigurationProperty _propChannel;
    private static readonly ConfigurationProperty _propProtocols;
    private static ConfigurationPropertyCollection _properties;

    #endregion

    #region Constructors
    static NGroupsSection()
    {
        // Define the properties
        NGroupsSection._propEnabled = new ConfigurationProperty("enabled", typeof(bool), true, ConfigurationPropertyOptions.None);
        NGroupsSection._propChannel = new ConfigurationProperty("channel", typeof(ChannelSettings), null, ConfigurationPropertyOptions.Required);
        NGroupsSection._propProtocols = new ConfigurationProperty("protocols", typeof(ProtocolSettingsCollection), null, ConfigurationPropertyOptions.Required); 

        // Add the properties to the properties collection
        NGroupsSection._properties = new ConfigurationPropertyCollection();
        NGroupsSection._properties.Add(NGroupsSection._propEnabled);
        NGroupsSection._properties.Add(NGroupsSection._propChannel);
        NGroupsSection._properties.Add(NGroupsSection._propProtocols);
    }

    #endregion 

    #region Public Properties
    /// <summary>
    /// This property represents the <code>enabled</code> attribute
    /// in the configuration file.
    /// </summary>
    [ConfigurationProperty("enabled", DefaultValue = true)]
    public bool Enabled
    {
        get { return (bool)base[NGroupsSection._propEnabled]; }
        set { base[NGroupsSection._propEnabled] = value; }
    }

    /// <summary>
    /// This property represents the <code>application</code> subsection
    /// in the configuration file.
    /// </summary>
    [ConfigurationProperty("channel")]
    public ChannelSettings Channel
    {
        get { return (ChannelSettings)base[NGroupsSection._propChannel]; }
        set { base[NGroupsSection._propChannel] = value; }
    } 

    /// <summary>
    /// This property represents the <code>protocols</code>
subsection
    ///
in the configuration file.
    ///
</summary>
    [ConfigurationProperty("protocols")]
    public ProtocolSettingsCollection Protocols
    {
        get { return (ProtocolSettingsCollection)base[NGroupsSection._propProtocols]; }
        set { base[NGroupsSection._propProtocols] = value; }
    }
   
#endregion
}

The most important pieces of code in this method are first the constructor, where I create instances of System.Configuration.ConfigurationProperty classes. Instances of that class represents configuration properties in the configuration file. These properties are added to a System.Configuration.ConfigurationPropertyCollection object. These properties are also added to the collection on the base class, which stores a key/value pair with key being the instance of ConfigurationProperty and value being the value of that attribute.

I then add three properties to map to the properties I want in the configuration property. They look like ordinary properties, except that they return values from the collection in the base class and they are decorated with the ConfigurationProperty attribute, which is the piece that does the trick. This attribute is used by the System.Configuration.ConfigurationManager to serialize the class into the XML configuration property that we want. The cool thing about that attribute is that if the property it is decorating contains a single value, then it will map such a property as an attribute in the XML element. If the value of the property is of type System.Configuration.ConfigurationElementCollection, then it will map the collection as a sub element in the config file. If you go back to the XML we want to achieve, you will see that Protocols is a sub element with a list of configured protocols.

Then we move on to implement the configuration settings. As they are very similar in their code (actually I have written the first one and then Cut & Paste to the second one), I will show only the more "complex" one, or in other words, the one with more properties. That is as follows:

public sealed class ProtocolSettings : ConfigurationElement
{

    #region
Private Member Variables
    private readonly ConfigurationProperty _propName;
    private readonly ConfigurationProperty _propType;
    private readonly ConfigurationProperty _propRef;
    private readonly ConfigurationProperty _propSettings;
    private readonly ConfigurationPropertyCollection _properties;
    private NameValueCollection _propertyNames;

    #endregion 

    #region Constructors
    public ProtocolSettings()
    {
        this._propName = new ConfigurationProperty("name", typeof(string), null, ConfigurationPropertyOptions.IsKey);
        this._propType = new ConfigurationProperty("type", typeof(string), null, ConfigurationPropertyOptions.None);
        this._propRef = new ConfigurationProperty("ref", typeof(string), null, ConfigurationPropertyOptions.None);
        this._propSettings = new ConfigurationProperty("settings", typeof(string), null, ConfigurationPropertyOptions.None);
 

        this._properties = new ConfigurationPropertyCollection();
        this._properties.Add(this._propName);
        this._properties.Add(this._propType);
        this._properties.Add(this._propRef);
        this._properties.Add(this._propSettings);
    }

    public ProtocolSettings(string name, string type) : this()
    {
        this.Name = name;
        this.Type = type;
    }

    #endregion 

    #region Properties
    [ConfigurationProperty("name")]
    public string Name
    {
        get { return (string)base[this._propName]; }
        set { base[this._propName] = value; }
    } 

    [ConfigurationProperty("type")]
    public string Type
    {
        get { return (string)base[this._propType]; }
        set { base[this._propType] = value; }
    } 

    [ConfigurationProperty("ref")]
    public string Ref
    {
        get { return (string)base[this._propRef]; }
        set { base[this._propRef] = value; }
    } 

    [ConfigurationProperty("settings")]
    public string Settings
    {
        get { return (string)base[this._propSettings]; }
        set { base[this._propSettings] = value; }
    } 

    public NameValueCollection Parameters
    {
       
get
        {
            if (this._propertyNames == null)
            {
                lock (this)
                {
                    if (this._propertyNames == null)
                    {
                        this._propertyNames = new NameValueCollection(StringComparer.InvariantCulture);
                        foreach (ConfigurationProperty property in this._properties)
                        {
                            if ((property.Name != "name") && (property.Name != "type")
                                && (property.Name != "ref") && (property.Name != "settings"))
                            {
                                this._propertyNames.Add(property.Name, (string)base[property]);
                            }
                        }
                    }
                }
            }

            return this._propertyNames;
        }
    }

    protected override ConfigurationPropertyCollection Properties
    {
       
get
        {
            this.UpdatePropertyCollection();
            return this._properties;
        }
    }

    #endregion

    #region Private Methods
    private string GetProperty(string name)
    {
        if (this._properties.Contains(name))
        {
            ConfigurationProperty property = this._properties[name];
            if (property != null)
            {
                return (string)base[property];
            }
        }
        return null;
    } 

    private bool SetProperty(string name, string value)
    {
        ConfigurationProperty property = null;

        if (this._properties.Contains(name))
        {
            property = this._properties[name];
        }
       
else
        {
            property = new ConfigurationProperty(name, typeof(string), null);
            this._properties.Add(property);
        }       

        if (property != null)
        {
            base[property] = value;
            return true;
        }       

        return false;
    } 

    protected override bool IsModified()
    {
        if (!this.UpdatePropertyCollection())
        {
            return base.IsModified();
        }
        return true;
    } 

    protected override bool OnDeserializeUnrecognizedAttribute(string name, string value)
    {
        ConfigurationProperty property = new ConfigurationProperty(name, typeof(string), value);
        this._properties.Add(property);

        base[property] = value;
        this.Parameters[name] = value;
        return true;
    }

    internal bool UpdatePropertyCollection()
    {
        bool isModified = false;   
        ArrayList newPropertyNames = null;
       
        if (this._propertyNames != null)
        {
            foreach (ConfigurationProperty property in this._properties)
            {
                if (((property.Name == "name") || (property.Name == "type")
                    || (property.Name == "ref") || (property.Name == "settings"))
                    || (this._propertyNames.Get(property.Name) != null))
                {
                    continue;
                } 

                if (newPropertyNames == null)
                {
                    newPropertyNames = new ArrayList();
                } 

                newPropertyNames.Add(property.Name);
                isModified = true;
            }

            if (newPropertyNames != null)
            {
                foreach (string name in newPropertyNames)
                {
                    this._properties.Remove(name);
                }
            } 

            foreach (string name in this._propertyNames)
            {
                string oldVal = this._propertyNames[name];
                string newVal = this.GetProperty(name);
                if ((newVal == null) || (oldVal != newVal))
                {
                    this.SetProperty(name, oldVal);
                    isModified = true;
                }
            }
        }
        return isModified;
    }

    #endregion

}

The class above has some important pieces of code to note. The first one is the constructor: It is not actually different from the one written to the NGroupsSection class, except that it sets the ConfigurationPropertyOptions.IsKey when instantiating ConfigurationProperty for the "name" property. It sets that property as the key in a collection of similar configuration elements. That enumeration has other values such as IsRequired, for example.

We have the same set of properties mapping to the settings we want on the config file, but here we have a bigger set of properties.

The Parameters property is used to return a NameValueCollection containing all the properties specified in the configuration file that don't map to properties in the class. In that way, other implementations of the class using this configuration element can expect additional settings without you having to specify them on the configuration class. They are not strongly typed into properties, but can be accessed from the Parameters collection.

There is then the Properties property. This property is intended to support the configuration infrastructure in the framework and is called to get the list of properties available on that implementation of a configuration element.

I have also a set of methods there. These methods are detailed below:

  • GetProperty and SetProperty: These methods are used to get and set respectively values for all the properties in the configuration section. They prove handy when accessing the additional settings not specified in the strongly typed properties.
  • IsModified: This method is used by the Framework to determine whether something has changed in this configuration element before saving them on the config file.
  • OnDeserializeUnrecognizedAttribute: This method gets called when an attribute in the config file doesn't match a property in the class. Then you have the opportunity to add it to the collection of additional properties.
  • UpdatePropertyCollection: This method, when called, loads all the configuration settings in the base class's collection of properties and match then to the collection of names already stored in the instance. If there are additional properties then it will add these properties to the collection of names and return true.

That's pretty much all that is needed to create a configuration element. The last class I'm showing the implementation here is the ProtocolSettingsCollection. That is the collection of ProtocolSettings used in the configuration file. The class is implemented as follows:

[ConfigurationCollection(typeof(ProviderSettings))]
public
sealed class ProtocolSettingsCollection : ConfigurationElementCollection
{

    #region
Private Static Variables
    private static readonly ConfigurationProperty _propProtocols;
    private static ConfigurationPropertyCollection _properties;

    #endregion

    #region
Constructors
    static ProtocolSettingsCollection()
    {
        ProtocolSettingsCollection._propProtocols = new ConfigurationProperty(null, typeof(ProtocolSettingsCollection), null, ConfigurationPropertyOptions.DefaultCollection);
        ProtocolSettingsCollection._properties = new ConfigurationPropertyCollection();
        ProtocolSettingsCollection._properties.Add(ProtocolSettingsCollection._propProtocols);
    }

    #endregion 

    #region Public Properties
    public ProtocolSettings this[int index]
    {
       
get
        {
            return (ProtocolSettings)base.BaseGet(index);
        }
       
set
        {
            if (base.BaseGet(index) != null)
            {
                base.BaseRemoveAt(index);
            }
            this.BaseAdd(index, value);
        }
    }

    #endregion 

    #region Methdods
    public void Add(ProtocolSettings protocol)
    {
        if (protocol != null)
        {
            protocol.UpdatePropertyCollection();
            this.BaseAdd(protocol);
        }
    } 

    public void Clear()
    {
        base.BaseClear();
    } 

    public void Remove(string name)
    {
        base.BaseRemove(name);
    }

    protected override ConfigurationElement CreateNewElement()
    {
        return new ProtocolSettings();
    }
 
    protected override object GetElementKey(ConfigurationElement element)
    {
        string key = ((ProtocolSettings)element).Name;    

        if (string.IsNullOrEmpty(key))
        {
            key = ((ProtocolSettings)element).Ref;
        }
 
        return key;
    }

    #endregion

The class above is not really different than the other ones on its constructor. I have then added a this[] property to the class so that we can access configuration settings by index and Add, Clear and Remove methods that guess what? They add an item to the collection, clear all the items from the collection and remove a specified item from the collection. You wouldn't guess, would you? :-P

The important methods in this class are CreateNewElement and GetElementKey, which are used by the runtime to correctly instantiate the configuration element needed for this collection.

After having written all this code, the next step is easy: We just create a new Windows or Console application and stuff in some code like this:

NGC::NGroupsSection ngroupsSection = new NGC::NGroupsSection();
ngroupsSection.Enabled = true

NGC::ChannelSettings channelSection = new NGC::ChannelSettings();
ngroupsSection.Channel = channelSection; 

NGC::ProviderSettingsCollection providers = new NGC::ProviderSettingsCollection();
NGC::ProviderSettings provider = new NGC::ProviderSettings();
provider.Name = "NChannel";
provider.Type = "NGroups.NChannel, NGroups.Core";
providers.Add(provider);
channelSection.Providers = providers;

NGC::ProtocolSettingsCollection protocolDefinitions = new NGC::ProtocolSettingsCollection();

for
(int i = 0; i < 10; i++)
{
    NGC::ProtocolSettings protocol = new NGC::ProtocolSettings();
    protocol.Name = "PROT" + i.ToString();
    protocol.Type = "NGroups.Protocols.Protocol, NGroups.Core";
    protocolDefinitions.Add(protocol);
}
ngroupsSection.Protocols = protocolDefinitions; 

NGC::ProtocolSettingsCollection protocolReferences = new NGC::ProtocolSettingsCollection();
for
(int i = 0; i < 10; i++)
{
    NGC::ProtocolSettings protocol = new NGC::ProtocolSettings();
    protocol.Ref = "PROT" + i.ToString();
    protocol.Settings = "(setting=1,setting=2,setting=3)";
   
protocolReferences.Add(protocol);
}
channelSection.Protocols = protocolReferences; 

Configuration config = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None);
config.Sections.Add("ngroups", ngroupsSection); 

config.Save();

Well, the code above just create a number of instances of our classes, set some sample values and use the ConfigurationManager to open the application configuration file, getting a reference to the System.Configuration.Configuration object pointing to the app's config file.

To read from the config file, you can use a code like the one bellow:

Configuration config = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None);
NGC::NGroupsSection  section = (NGC::NGroupsSection)config.GetSection("ngroups");

Console
.WriteLine("NGroups is {0}.", (section.Enabled) ? "enabled" : "not enabled");
Console
.WriteLine("\nProviders in the channel:\n");

foreach (NGC::ProviderSettings provider in section.Channel.Providers)
{
    Console.WriteLine(provider.Name);

Console.WriteLine("\nProtocols in the channel\n");

foreach
(NGC::ProtocolSettings protocol in section.Channel.Protocols)
{
    Console.WriteLine(protocol.Ref);
}

After running the above code, you should get an output like:

NGroups is enabled.

Providers in the channel:

NChannel

Protocols in the channel

PROT0
PROT1
PROT2
PROT3
PROT4
PROT5
PROT6
PROT7
PROT8
PROT9

Well, that's all folks. I hope you have enjoyed it, if you have any questions just drop a comment here and I will get back to you. I have stripped out the source code of NGroups and posted a zip file that you can get here. It contains only the configuration stuff and the test console application, so you can run it to see the results.

Published 31 May 2005 23:10 by paulo.reichert
Filed under: ,

Comments

 

paulo.reichert said:

Yet more useful code, that'll save me the time of figuring out how to do this.

One, question about the collections. In my implementation I have a HandlerSection and a HandlerSectionCollection. But when I generate the XML I end up with:

<handlers>
<add name="handler1" />
<add name="handler2">
<handlers>

Any ideas how I can get it to generate it the way I actually want:

<handlers>
<handler name="handler1" />
<handler name="handler2">
<handlers>

It's not really a big deal, I guess, but I spent hours yesterday trying to figure it out ... with no luck.

Keep up the good work :)

Simon
June 6, 2005 12:15
 

paulo.reichert said:

Hello there, mate!

Good point there. It looks like you can;t assign the names to the elements on the collection.

It is automnatically serialized and I didn't find any attribute to change the default element name. If anyone knows how to do that, please point it out and I will update the article.

Cheers!
June 7, 2005 08:47
 

Coding Sanity said:



There's a lovely article on the new Configuration Management&amp;nbsp;system in
.NET 2.0 on Paulo Rechiert's...
January 7, 2006 10:19
 

Clark Sell said:

Paulo Reichert&amp;nbsp;posted a GREAT sample using the new Configuration Management features of .Net 2.0...
January 18, 2006 12:58
 

Clark Sell said:

Paulo Reichert&amp;nbsp;posted a GREAT walkthrough / sample using the new Configuration Management features...
January 18, 2006 13:00
 

derekgreer said:

Hey Paulo.  Can you shed some light on the benefits to managing the properties yourself as you are doing here (i.e. this._properties.Add(this._propName); ) verses using the base classe's internal ConfigurationPropertyCollection (i.e. base[_propertyName])?  I've seen this both ways, but the benefits of the extra code necessary in managing this yourself isn't jumping out at me.  Thanks.

Derek Greer
April 3, 2006 06:11
 

derekgreer said:

Nevermind, I think I've got it.  Because of the introduction of the public Parameters property, there is a potential of adding properties that the base ConfigurationElement won't know about.  Because of this, you now have to manage your own collection of properties to have the ability to merge both the base classes properties and your own when accessing them, and to properly check for updates to trigger the Save().  My assumption is that if no such additional access is given to a ConfigurationElement then much of this code is unnecessary (i.e. if you don't overload the Properties, you don't need all the constructor based code, the update code, etc.)

Derek Greer
April 3, 2006 06:45
 

tony said:

Hi Paulo.

Great article. One thing I am battling with, and I think it is because I don't quite yet understand the the new Configuration model as well as I should, is how to separate parts of the configuration out into a different file. In .Net 1.1 I used the Configuration Management Application Block to do this (with a ConfigProvider element which allows me to specify a path to the new config file), but the Configuration Management Application Block is now deprecated. Any ideas on how to do this with the new ConfigurationManager in.Net 2 ?
Regards,
Tony.
May 15, 2006 08:19
 

tony said:

One other thing. I've downloaded the code and run it on my machine. I had to change the lines with "ConfigurationPropertyOptions.Required" to "ConfigurationPropertyOptions.IsRequired" and similarly the lines with "ConfigurationPropertyOptions.DefaultCollection" to "ConfigurationPropertyOptions.IsDefaultCollection" to get it to compile. Once I had done this I got the following error on the Read:
"Unable to cast object of type 'System.Configuration.DefaultSection' to type 'NGroups.Configuration.NGroupsSection'." Note that I copied the "nGroups" section from the SampleConfig.xml to the AppConfig file, since the Write doesn't seem to save it to the AppConfig file (it modifies the "TestApplication.vshost.exe.config" file correctly, but doesn't save it in the AppConfig field).
Apologies if these are inane questions, but I'm still getting my head around this.
Regards,
Tony.

May 15, 2006 10:30
 

Clark Sell said:

Paulo Reichert posted a GREAT walkthrough / sample using the new Configuration Management features of

October 26, 2006 03:14
 

Juan Carlos said:

Paulo, I read your article and compiled your code. Yet I got the same result that Tony got. In my case, I need to read the user-defined info from the app.config file. When I run your code, nothing seems to happen. When I do what Tony did and copy the info from SampleConfig.xml to the app.config file, I get the same result that Tony gets. When I implement the ideas from the article into my code, I always get null form the call to GetSection. Any ideas as to what I might be doing wrong. Thanks

February 9, 2007 16:30
 

Anthony.Steele said:

If you want the item to be introduced with a tag other than "add", look at the AddItemName property to specify another text - either on the class inherited from ConfigurationElementCollection on on the ConfigurationCollection attribute

June 8, 2007 16:26
 

Finds of the Week - February 17, 2008 » Chinh Do said:

February 18, 2008 05:23
Anonymous comments are disabled
Powered by Community Server (Personal Edition), by Telligent Systems