Click here to Skip to main content
15,860,859 members
Articles / Web Development / ASP.NET

Unraveling the Mysteries of .NET 2.0 Configuration

Rate me:
Please Sign up or sign in to vote.
4.83/5 (352 votes)
28 Jun 2007CPOL35 min read 2M   4.9K   916   457
Learn to utilize powerful new .NET 2.0 configuration features
In this article, you will learn how to utilize the powerful new .NET 2.0 configuration features to simplify and centralize your configuration code.

Introduction

One of the wonderful features of .NET has been its XML configuration features. In the days of .NET 1.x, common application settings, database connection strings, ASP.NET web server configuration and basic custom configuration data could be stored in a .config file. Custom configuration sections could use a few basic but custom structures, allowing a small variety of information to be stored in a .config file. More complex configuration, however, was most often accomplished with custom XML structures and custom parsing code. Such code could become quite complex, though, and there are a variety of ways of differing performance to accomplishing the same thing.

With .NET 2.0, the days of writing your own (probably complicated, poorly-performing and tedious) code to manage custom XML configuration structures are over. The custom configuration capabilities of the built-in XML configuration subsystem in .NET 2.0 have been greatly revamped, boasting some extremely useful and time saving features. With relative ease, just about any XML configuration structure you might need is possible with a relatively minimal amount of work. In addition, deserialization of the XML in a .config file can always be overridden. This allows any XML structure necessary to be supported without losing the other advanced features of .NET 2.0 configuration.

The Series

This is the first article in a series. All of the core concepts and important bits of knowledge required to begin using .NET 2.0 configuration sections is provided in this first article. However, the configuration framework available with .NET 2.0 is extensive. It has many capabilities and hidden features that can fill in the gaps and oversights in the framework that may seem apparent from this article. To continue learning about the .NET 2.0 configuration framework, additional articles can be found at the following links:

  1. Unraveling the Mysteries of .NET 2.0 Configuration
  2. Decoding the Mysteries of .NET 2.0 Configuration
  3. Cracking the Mysteries of .NET 2.0 Configuration

The Mystery

I have been using .NET 2.0 for a little over a year now and sadly enough, have spent many a sleepless night tinkering with custom configuration models. Custom XML configuration handling is the ever-persistent performance sink, requiring a new model for each application, depending on what kind of configuration is being stored and how it needs to be accessed (and from where). It wasn't until about eight months ago, while digging through .NET 2.0 Framework assemblies with Reflector, that I came across a curious little class: System.Configuration.ConfigurationSection. Digging a little deeper, I found a multitude of framework classes that derived from ConfigurationSection, as well as several other classes. Ever since then, I have spent more than a few sleepless nights absorbing as much knowledge about the new configuration features of .NET 2.0 as I possibly could.

Suffice it to say, there ultimately is minimal information on the internet about the true capabilities of custom configuration sections. Through internet research, hours of digging through framework code in Reflector, and continual experimentation and use of custom configuration sections, I've finally learned what I needed to know. It's been a life saver. Finally, creating and managing custom XML configuration is simple to write, simple to use, simple to manage and performs wonderfully... and I'm here to share this holy grail with all of you, my fine readers. The only thing I ask in return is to send anyone and everyone you know who uses .config files -- which should be pretty much anyone writing .NET code -- to this page and save them months of digging through code, internet forums and blog posts to learn it all. :)

Topics of Configuration

The goal of this article is to cover all the major aspects of .NET 2.0 configuration, as well as expose some of the more closely guarded (read: undocumented... for whatever reasons) secrets that could save you a lot of hassle and time. We'll start with an overview of the core namespace that exposes all of this custom configuration madness. Then we'll move on to specific implementations and uses for custom configuration. The topics of discussion are as follows:

  1. The namespace: System.Configuration
  2. The Object-Model configuration concept
  3. Writing a basic configuration section
  4. Using a custom configuration section
  5. Adding custom elements
  6. Adding element collections
  7. Advanced element collections
  8. Custom configuration section groups
  9. Saving configuration changes
  10. Configuration tips and tricks
  11. Advanced configuration topics
  12. Appendices
    1. Appendix A: The configuration collection cascade
    2. Appendix B: Including external configuration files
  13. Article history

The namespace: System.Configuration

The heart of the new .NET 2.0 configuration gem is the System.Configuration namespace. By default, this namespace is available when the System.dll assembly is referenced. It includes all of the configuration features of .NET 1.1, including the old ConfigurationSettings class (now deprecated in .NET 2.0). To gain access to all of the new .NET 2.0 configuration features, however, you must add a reference to the System.Configuration.dll assembly. It is in this assembly that you will find the core of the new configuration system, the ConfigurationManager static class.

The ConfigurationManager class is a globally accessible doorway to an application's configuration. Since the class is static, all of its members are also static. This makes reading configuration such as AppSettings, ConnectionStrings and custom configuration sections a breeze. While this is similar to the ConfigurationSettings class, it also provides several new features that allow more secure access to an application's configuration. These new features also allow configuration settings to be saved to any configuration section: custom or otherwise.

Beyond the ConfigurationManager class lies the life-blood of custom configuration sections. The following outlines the base classes available to help you write your own configuration object model. THere will also be more on this later. In addition to a set of base classes is a set of validators that can be used to ensure the accuracy of your custom configuration. Also, in the event that your needs are simple enough, a few pre-made configuration sections are available.

The Base Types

  • ConfigurationSection - The base class of all configuration sections
  • ConfigurationSectionCollection - The base class of a collection of configuration sections
  • ConfigurationSectionGroup - The base class of a configuration section group
  • ConfigurationSectionGroupCollection - The base class of a collection of configuration section groups
  • ConfigurationElement - The base class of a configuration element
  • ConfigurationElementCollection - The base class of a collection of configuration elements
  • ConfigurationConverterBase - The base class of a custom converter *1
  • ConfigurationValidatorBase - The base class of a custom validator *2

The Support Types

  • ConfigurationManager - Allows global access to all of an application's configuration
  • Configuration - A class that represents an application's configuration
  • ConfigurationProperty - A class that represents a single configuration property
  • ConfigurationPropertyAttribute - An attribute class that supports declarative definition of configuration
  • ConfigurationPropertyCollection - A collection of configuration properties
  • ConfigurationPropertyOptions - Enumeration of possible configuration property options

The Validation Types

  • CallbackValidator - Allows dynamic validation of a configuration value
  • CallbackValidatorAttribute - Attribute class for declaratively applying callback validators
  • IntegerValidator - Allows validation of an integer (Int32) configuration value
  • IntegerValidatorAttribute - Attribute class for declaratively applying integer validators
  • LongValidator - Allows validation of a long (Int64) configuration value
  • LongValidatorAttribute - Attribute class for declaratively applying long validators
  • PositiveTimeSpanValidator - Allows validation of a positive time span configuration value
  • PositiveTimeSpanValidatorAttribute - Attribute class for declaratively applying positive time span validators
  • RegexStringValidator - Allows validation of a string configuration value with a regular expression
  • RegexStringValidatorAttribute - Attribute class for declaratively applying regex validators
  • StringValidator - Allows validation of a string configuration value
  • StringValidatorAttribute - Attribute class for declaratively applying string validators
  • SubclassTypeValidator - Allows validation that a configuration value derives from a given type *3
  • SubclassTypeValidatorAttribute - Attribute class for declaratively applying subclass type validators
  • TimeSpanValidator - Allows validation of a time span configuration value
  • TimeSpanValidatorAttribute - Attribute class for declaratively applying time span validators

The Converter Types

  • CommaDelimitedStringCollectionConverter - Converts a comma-delimited value to/from CommaDelimitedStringCollection
  • GenericEnumConverter - Converts between a string and an enumerated type
  • InfiniteIntConverter - Converts between a string and the standard infinite or integer value
  • InfiniteTimeSpanConverter - Converts between a string and the standard infinite TimeSpan value
  • TimeSpanMinutesConverter - Converts to and from a time span expressed in minutes
  • TimeSpanMinutesOrInfiniteConverter - Converts to and from a time span expressed in minutes or infinite
  • TimeSpanSecondsConverter - Converts to and from a time span expressed in seconds
  • TimeSpanSecondsOrInfiniteConverter - Converts to and from a time span expressed in seconds or infinite
  • TypeNameConverter - Converts between a Type object and the string representation of that type
  • WhiteSpaceTrimStringConverter - Converts a string to its canonical format, i.e., white space trimmed from front and back

Pre-Made Configuration Sections

  • AppSettingsSection - This provides the well-known <appSettings> configuration section
  • ConnectionStringsSection - This provides the new <connectionStrings> configuration section
  • ProtectedConfigurationSection - This provides an encrypted configuration section
  • IgnoreSection - Special configuration section wrapper that is ignored by the configuration parser

Premade Configuration Collections *4

  • CommaDelimitedStringCollection - Used in conjunction with CommaDelimitedStringCollectionConverter
  • KeyValueConfigurationCollection - Used to configure key/value pairs in your configuration sections
  • NameValueConfigurationCollection - Used to configure name/value pairs in your configuration sections

Notes

  • *1 Custom converters are used to convert between string representations used in XML files to their natively typed representations in a configuration object model.
  • *2 Custom validators are used to validate the accuracy of natively typed data in a configuration object model.
  • *3 This ties into the configuration object model concept, which will be discussed in the next section.
  • *4 These common configuration collections can be used in your custom configuration sections.

In addition to all of the classes described above, you will also find other support types used by ProtectedConfigurationSection. All of the old .NET 1.x configuration classes will also still be available in the System.Configuration namespace, but for the most part, they can be ignored.

The Object-Model Configuration Concept

Before we continue on to creating some custom configuration goodness, it is important to learn about the concept of object-model configuration. The .NET 2.0 configuration system ultimately provides a set of objects that represent your configuration settings structure and provide strongly-typed access to your configuration data. This contrasts with the more common methods of storing and retrieving configuration in XML files, which usually entails reading values through the use of a DOM or streaming reader into variables, and writing changes back through the use of a DOM or streaming writer. In the more advanced configuration systems I personally have written, some caching facilities were included to speed up reading and writing configuration values. Making a configuration file highly customizable while maintaining good performance was always a point of difficulty.

Such monolithic methods of handling XML configuration data are no longer needed with .NET 2.0's configuration object model. As a simple example, take the ConnectionStrings section. By integrating the configuration management for this common configuration item into the ConfigurationManager object, it is very easy to find and access any specific database connection string by a unique name. This is because a .NET collection class exists that lists connection string configuration objects. Each connection string object is keyed on a name value. A mapping of .NET classes to XML elements looks something like this:

ConnectionStringsSection -> <connectionStrings>
ConnectionStringSettingsCollection -> [implicitly created]
ConnectionStringSettings -> <add name="MyConnection" connectionString="blahblah">

Accessing the connection string MyConnection is as simple as the following code:

C#
string myConnectionString = 
    ConfigurationManager.ConnectionStrings["MyConnection"].ConnectionString;

The ConnectionStrings section is extremely simple, so let's take a look at an often-used, but less apparent use of the .NET 2.0 configuration system: the <system.web> configuration group in an ASP.NET web.config file. You may not know it, but this complex configuration section uses the same set of classes that you can use to create your own custom configuration sections. Let's take a look at the class->element relationships for the System.Web.Configuration object model:

SystemWebSectionGroup -> <system.web>
AuthenticationSection -> <authentication>
AuthorizationSection -> <authorization>
CustomErrorsSection -> <customErrors>
CustomErrorsCollection -> [implicitly created]
CustomError -> <code lang="xml"><error statusCode="404" redirect="..."></code>
HttpModulesSection -> <httpModules>
HttpModuleActionCollection -> [implicitly created]
HttpModuleAction -> <code lang="xml"><add name="myModule" type="..."></code>
HttpHandlersSection -> <httpHandlers>
HttpHandlerActionCollection -> [implicitly created]
HttpModuleActionCollection -> <code lang="xml"><add verb="*" type="..." path="..."></code>

This is only a small subset of the full set of configuration sections available for the System.Web ASP.NET configuration section group. All of these settings are accessible through a nicely packaged object model, rooted at the System.Web.Configuration.SystemWebSectionGroup class. In the event that it's required, configuration settings can even be updated and saved back to the web.config file using this object model, assuming that the code making changes and saving has the necessary permissions.

The .NET 2.0 configuration system handles the parsing, validation, security and population of this object model for you. Aside from writing a custom configuration section, which is fairly simple, this completely removes the need to think about XML when you consume your configuration in an application. Not only that, but this nicely packaged, strongly-typed, secure object model wrapped around your configuration is directly accessible anywhere in your application without needing to worry about finding or storing file paths to custom XML configuration files in places like the registry. Gone are the days of hassling with inconsistent, inflexible or poorly performing custom configuration managers; hopefully those days are gone for good.

Writing a Basic Configuration Section

If everything before this has intimidated you, don't worry. Writing the code to provide a custom configuration section to your application is very straightforward and simple. The vast bulk of dealing with XML ugliness, security checks, type conversion, etc., is handled by existing code in the .NET 2.0 Framework. Thanks to the set of base classes available in System.Configuration, the actual volume of code required to create a simple configuration section is very, very low. Let's start by creating a simple configuration section with a string value, a boolean value and a TimeSpan value. Every configuration section must derive from the ConfigurationSection base class, so let's start with the following:

C#
#region Using Statements
using System;
using System.Configuration;
#endregion

namespace Examples.Configuration
{
    /// <summary>
    /// An example configuration section class.
    /// </summary>
    public class ExampleSection: ConfigurationSection
    {
        #region Constructors
        static ExampleSection()
        {
            // Predefine properties here
        }
        #endregion

        // Declare static property fields here

        // Declare expose properties here
    }
}

Once you have the start of a configuration section class, you will need to define the valid configuration properties. A configuration property, represented by the ConfigurationProperty class, describes a single configuration item that will be available in your configuration section. There are a couple ways of defining configuration properties, both programmatic and declarative. My personal preference is to use both methods, as the declarative method helps provide self-describing code, and the programmatic method is more strict. This ensures that only the exact object model you expect is generated and supported, but it is a little more tedious to maintain, as two things must be updated for any change to a single configuration property.
Note: In this article, I will always use both methods for the fullest examples.

Let's start filling in the code to make our little example section function. Start by defining static property fields, then create those fields in the static class constructor. Finally, expose the configuration data through C# code properties. The full source code for the custom configuration section should look something like this when it's done:

C#
#region Using Statements
using System;
using System.Configuration;
#endregion

namespace Examples.Configuration
{
    /// <summary>
    /// An example configuration section class.
    /// </summary>
    public class ExampleSection: ConfigurationSection
    {
        #region Constructors
        /// <summary>
        /// Predefines the valid properties and prepares
        /// the property collection.
        /// </summary>
        static ExampleSection()
        {
            // Predefine properties here
            s_propString = new ConfigurationProperty(
                "stringValue",
                typeof(string),
                null,
                ConfigurationPropertyOptions.IsRequired
            );

            s_propBool = new ConfigurationProperty(
                "boolValue",
                typeof(bool),
                false,
                ConfigurationPropertyOptions.None
            );

            s_propTimeSpan = new ConfigurationProperty(
                "timeSpanValue",
                typeof(TimeSpan),
                null,
                ConfigurationPropertyOptions.None
            );

            s_properties = new ConfigurationPropertyCollection();
            
            s_properties.Add(s_propString);
            s_properties.Add(s_propBool);
            s_properties.Add(s_propTimeSpan);
        }
        #endregion

        #region Static Fields
        private static ConfigurationProperty s_propString;
        private static ConfigurationProperty s_propBool;
        private static ConfigurationProperty s_propTimeSpan;

        private static ConfigurationPropertyCollection s_properties;
        #endregion
         
        #region Properties
        /// <summary>
        /// Gets the StringValue setting.
        /// </summary>
        [ConfigurationProperty("stringValue", IsRequired=true)]
        public string StringValue
        {
            get { return (string)base[s_propString]; }
        }

        /// <summary>
        /// Gets the BooleanValue setting.
        /// </summary>
        [ConfigurationProperty("boolValue")]
        public bool BooleanValue
        {
            get { return (bool)base[s_propBool]; }
        }

        /// <summary>
        /// Gets the TimeSpanValue setting.
        /// </summary>
        [ConfigurationProperty("timeSpanValue")]
        public TimeSpan TimeSpanValue
        {
            get { return (TimeSpan)base[s_propTimeSpan]; }
        }

        /// <summary>
        /// Override the Properties collection and return our custom one.
        /// </summary>
        protected override ConfigurationPropertyCollection Properties
        {
            get { return s_properties; }
        }
        #endregion
    }
}

Once you are done, that's it. This custom configuration section is ready to go. If you prefer to write less code, you could drop the static constructor and the static fields and store each value in the default properties collection by a string key. This would lead to less code required for the configuration section itself, but would be less defined in requirements overall. The properties above could be replaced with the following, and the static fields and constructor could be removed. That, by the way, is the purely declarative method:

C#
#region Properties
/// <summary>
/// Gets the StringValue setting.
/// </summary>
[ConfigurationProperty("stringValue", IsRequired=true)]
public string StringValue
{
    get { return (string)base["stringValue"]; }
}

/// <summary>
/// Gets the BooleanValue setting.
/// </summary>
[ConfigurationProperty("boolValue")]
public bool BooleanValue
{
    get { return (bool)base["boolValue"]; }
}

/// <summary>
/// Gets the TimeSpanValue setting.
/// </summary>
[ConfigurationProperty("timeSpanValue")]
public TimeSpan TimeSpanValue
{
    get { return (TimeSpan)base["timeSpanValue"]; }
}
#endregion

A quick note about what a ConfigurationProperty really is. By default, unless a custom element is explicitly written, all ConfigurationPropertys defined in a custom configuration element will be represented by XML attributes in an App.config or Web.config file. The creation and collections of custom configuration elements will be discussed later on.

Using a Custom Configuration Section

Now that you have a class written for your custom configuration section, you need to define the custom section in an App.config file. You must and add the necessary XML, so that it can be parsed by the .NET 2.0 configuration system. The App.config changes required to make this ExampleSection available would look like this, assuming that the above code is compiled into Examples.Configuration.dll:

XML
<configuration>
  <configSections>
    <section name="example" type="Examples.Configuration.ExampleSection, 
                                  Examples.Configuration" />
  </configSections>

  <example
    stringValue="A sample string value."
    boolValue="true"
    timeSpanValue="5:00:00"
  />
</configuration>

A quick explanation of the <configSections> element and its children. Unlike built-in configuration sections, which are implicitly defined, custom configuration sections must be explicitly defined. This is done through the <configSections> element and its corresponding <section> child element. It should be noted that the name of a section is generally very important and it can not be chosen arbitrarily. You will see why in a moment. The type of a configuration section maps to a fully qualified class name, followed by the assembly name. Optionally, you may specify culture, version and public key (for signed assemblies) values if you wish to ensure only a specific version of an assembly is searched for when your .config file is parsed. In the example above, the part of the string before the comma is the class name and the part after is the assembly name (excluding the .dll).

Finally, you can consume your custom configuration in code using the ConfigurationManager class. The ConfigurationManager provides a method called GetSection which allows you to access custom configuration settings if any are defined. It's generally best to use a dynamic cast and check for null when accessing custom configuration sections, like so:

C#
private string m_string;
private bool m_bool;
private TimeSpan m_timespan;

void GetExampleSettings()
{
    ExampleSection section = ConfigurationManager.GetSection("example") 
                             as ExampleSection;
    if (section != null)
    {
        m_string = section.StringValue;
        m_bool = section.BooleanValue;
        m_timespan = section.TimeSpanValue;
    }
}

A most important note here is the name chosen to represent the root element of a custom configuration section. In the example App.config file, the section was defined with the name "example." Since the code that loads the section looks for the name 'example' in the call to GetSection(), any attempt to rename the element from "example" to something else in the App.config file will cause the function GetExampleSettings() to fail. It is not possible to arbitrarily choose a name for a custom configuration section as desired unless explicit functionality is written to accommodate such a thing, possibly through the use of another configuration section.

Adding Custom Elements

By default, all configuration properties defined in a custom configuration section will be represented by attributes in a .config file. This will not always be sufficient, however, and a more complex XML structure consisting of a mix of attributes and elements may be required. Fear not: the .NET 2.0 configuration system fully supports custom configuration elements, each with their own configuration properties that can be attributes or more nested elements. To create a custom configuration element, simply write a class that derives from ConfigurationElement rather than ConfigurationSection. The implementation details for an element will be the same as for a section, with the difference between the two being that elements must be nested within a section.

Let's continue by nesting a custom element inside of our ExampleSection. Let's have this nested element store a DateTime value and an integer value. The code to create this class would be as follows:

C#
#region Using Statements
using System;
using System.Configuration;
#endregion

namespace Examples.Configuration
{
    /// <summary>
    /// An example configuration element class.
    /// </summary>
    public class NestedElement: ConfigurationElement
    {
        #region Constructors
        /// <summary>
        /// Predefines the valid properties and prepares
        /// the property collection.
        /// </summary>
        static NestedElement()
        {
            // Predefine properties here
            s_propDateTime = new ConfigurationProperty(
                "dateTimeValue",
                typeof(DateTime),
                null,
                ConfigurationPropertyOptions.IsRequired
            );

            s_propInteger = new ConfigurationProperty(
                "integerValue",
                typeof(int),
                0,
                ConfigurationPropertyOptions.IsRequired
            );

            s_properties = new ConfigurationPropertyCollection();
            
            s_properties.Add(s_propDateTime);
            s_properties.Add(s_propInteger);
        }
        #endregion

        #region Static Fields
        private static ConfigurationProperty s_propDateTime;
        private static ConfigurationProperty s_propInteger;

        private static ConfigurationPropertyCollection s_properties;
        #endregion

         
        #region Properties
        /// <summary>
        /// Gets the DateTimeValue setting.
        /// </summary>
        [ConfigurationProperty("dateTimeValue", IsRequired=true)]
        public DateTime StringValue
        {
            get { return (DateTime)base[s_propDateTime]; }
        }

        /// <summary>
        /// Gets the IntegerValue setting.
        /// </summary>
        [ConfigurationProperty("integerValue")]
        public int IntegerValue
        {
            get { return (int)base[s_propInteger]; }
        }

        /// <summary>
        /// Override the Properties collection and return our custom one.
        /// </summary>
        protected override ConfigurationPropertyCollection Properties
        {
            get { return s_properties; }
        }
        #endregion
    }
}

Adding this element to the ExampleSection we created earlier is as simple as defining a new configuration property. The following shows what's necessary to expose a nested element:

C#
public class ExampleSection: ConfigurationSection
{
    #region Constructors
    /// <summary>
    /// Predefines the valid properties and prepares
    /// the property collection.
    /// </summary>
    static ExampleSection()
    {
        // Create other properties...

        s_propElement = new ConfigurationProperty(
            "nestedElement",
            typeof(NestedElement),
            null,
            ConfigurationPropertyOptions.IsRequired
        );

        s_properties = new ConfigurationPropertyCollection();

        // Add other properties...
        s_properties.Add(s_propElement);
    }
    #endregion

    #region Static Fields
    private static ConfigurationProperty s_propElement;
    // Other static fields...
    #endregion

    #region Properties
    // ...

    /// <summary>
    /// Gets the NestedElement element.
    /// </summary>
    [ConfigurationProperty("nestedElement")]
    public NestedElement Nested
    {
        get { return (NestedElement)base[s_propElement]; }
    }

    // ...
    #endregion
}

Finally, using the element in our XML config file is as simple as adding a single <nestedElement> tag inside of the <example> tag. It is important to note that there can only be one instance of nestedElement inside the example element. Creating a nested element in this manner does not allow a collection of like-named elements. It allows a single instance of a particular element, at a specific nesting depth in your custom section. The next section will cover collections of elements in a configuration section. The complete App.config file started previously should look like this:

XML
<configuration>
  <configSections>
    <section name="example" type="Examples.Configuration.ExampleSection, 
                                  Examples.Configuration" />
  </configSections>

  <example
    stringValue="A sample string value."
    boolValue="true"
    timeSpanValue="5:00:00"
  >
    <nestedElement
      dateTimeValue="10/16/2006"
      integerValue="1"
    />
  </example>
</configuration>

Using the new nested element is again extremely simple, as a Nested property will now be exposed on our section variable from the previous usage example:

C#
private string m_string;
private bool m_bool;
private TimeSpan m_timespan;
private DateTime m_datetime;
private int m_int;

void GetExampleSettings()
{
    ExampleSection section = ConfigurationManager.GetSection("example") 
                             as ExampleSection;
    if (section != null)
    {
        m_string = section.StringValue;
        m_bool = section.BooleanValue;
        m_timespan = section.TimeSpanValue;
        m_datetime = section.Nested.DateTimeValue;
        m_int = section.Nested.IntegerValue;
    }
}

Each configuration section can be composed of any number of attributes and elements, nested as deep as required to meet the needs of your application. It's always a good idea to conform to the same XML best practices with custom configuration sections as it is for any other use of XML. As a general rule, data sets or large volumes of information should not be stored in a custom configuration section. We will discuss the reasons for this in the advanced sections. These are "configuration" sections and should be used to store structured application configuration information.

Adding Element Collections

In the previous section, we created a nested element inside of a configuration section element. The nested element is limited to only one instance and it must appear in the element that defines it in code. Creating collections or lists of elements requires a different and slightly more complex approach. To create a collection or list of configuration elements in a configuration file, you must create a class that derives from ConfigurationElementCollection. One of several varieties of collections can be created, the two primary types being Basic and Add/Remove/Clear.

Anyone who has used the <appSettings> configuration section will be familiar with an Add/Remove/Clear (or ARC map, for short) type of collection. An ARC map is a cascading collection supported in ASP.NET web.config files. A cascading collection allows entries to be added at one web site path level, and removed or cleared at a lower application path level. In addition, any new unique entries added at a lower level will be merged with all of the entries from previous levels. See Appendix A for more details on how a configuration cascade works. A basic map is more restrictive, but allows an element name other than "add" to add items to a collection. An example of a basic map with an alternate element name is System.Web's <customErrors> section, which supports a collection of <error> elements.

Since ARC map collections are the default type, let's create one and add it to our example configuration section from above. The code to create an element collection is slightly more complicated than that of a configuration section or single element, but is still very simple over all. The element collection code below follows a standard pattern used in most element collections in the .NET 2.0 Framework:

C#
[ConfigurationCollection(typeof(ThingElement),
    CollectionType=ConfigurationElementCollectionType.AddRemoveClearMap)]
public class ExampleThingElementCollection: ConfigurationElementCollection
{
    #region Constructors
    static ExampleThingElementCollection()
    {
        m_properties = new ConfigurationPropertyCollection();
    }

    public ExampleThingElementCollection()
    {
    }
    #endregion

    #region Fields
    private static ConfigurationPropertyCollection m_properties;
    #endregion

    #region Properties
    protected override ConfigurationPropertyCollection Properties
    {
        get { return m_properties; }
    }
    
    public override ConfigurationElementCollectionType CollectionType
    {
        get { return ConfigurationElementCollectionType.AddRemoveClearMap; }
    }
    #endregion

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

    public ThingElement this[string name]
    {
        get { return (ThingElement)base.BaseGet(name); }
    }
    #endregion
    
    #region Overrides
    protected override ConfigurationElement CreateNewElement()
    {
        return new ThingElement();
    }

    protected override object GetElementKey(ConfigurationElement element)
    {
        return (element as ThingElement).Name;
    }
    #endregion
}

It is generally necessary to provide an indexer accessible by numeric index. An indexer accessible by an element's key is also a useful convenience. In the case of this example, the key is a string name. The two overrides, CreateNewElement and GetElementKey, are very important in ensuring that your collection functions properly. There are two overloads of the CreateNewElement function, one that takes no parameters and another that takes an element name, in case you have overridden the default ARC Map behavior. More about this will be discussed in the advanced topics section. By default, the CreateNewElement(string elementName) overload calls CreateNewElement(), so it is not always necessary to override it. The GetElementKey method returns the value of the provided configuration element and returns the value that uniquely identifies it. In the case of our example, the key is the Name property that will be defined on our ThingElement. Finally, you may have noticed that the Properties collection was overridden. The reasons for this are not very apparent unless you delve deep into the .NET Framework source code available here, but suffice it to say that it is a performance optimization.

Our collection is not quite done yet, as we need something to collect. For our example, that something is the ThingElement. This is another ConfigurationElement class, similar to our NestedElement class above.

C#
public class ThingElement: ConfigurationElement
{
        #region Constructors
        /// <summary>
        /// Predefines the valid properties and prepares
        /// the property collection.
        /// </summary>
        static ThingElement()
        {
            // Predefine properties here
            s_propName = new ConfigurationProperty(
                "name",
                typeof(string),
                null,
                ConfigurationPropertyOptions.IsRequired
            );

            s_propType = new ConfigurationProperty(
                "type",
                typeof(string),
                "Normal",
                ConfigurationPropertyOptions.None
            );

            s_propColor = new ConfigurationProperty(
                "color",
                typeof(string),
                "Green",
                ConfigurationPropertyOptions.None
            );

            s_properties = new ConfigurationPropertyCollection();
            
            s_properties.Add(s_propName);
            s_properties.Add(s_propType);
            s_properties.Add(s_propColor);
        }
        #endregion

        #region Static Fields
        private static ConfigurationProperty s_propName;
        private static ConfigurationProperty s_propType;
        private static ConfigurationProperty s_propColor;

        private static ConfigurationPropertyCollection s_properties;
        #endregion
         
        #region Properties
        /// <summary>
        /// Gets the Name setting.
        /// </summary>
        [ConfigurationProperty("name", IsRequired=true)]
        public string Name
        {
            get { return (string)base[s_propName]; }
        }

        /// <summary>
        /// Gets the Type setting.
        /// </summary>
        [ConfigurationProperty("type")]
        public string Type
        {
            get { return (string)base[s_propType]; }
        }

        /// <summary>
        /// Gets the Type setting.
        /// </summary>
        [ConfigurationProperty("color")]
        public string Color
        {
            get { return (string)base[s_propColor]; }
        }

        /// <summary>
        /// Override the Properties collection and return our custom one.
        /// </summary>
        protected override ConfigurationPropertyCollection Properties
        {
            get { return s_properties; }
        }
        #endregion
}

Our ThingElement is very simple, only providing a name, a type and a color. You should notice now that we have overridden the Properties collection on all of our configuration classes so far. This is not a necessary step, but it can improve the performance and efficiency of your configuration sections. More detail about why is outlined in the advanced topics section. We can make this collection accessible in our ExampleSection by adding another ConfigurationProperty, implemented the same way the NestedElement was. Only this time, replace the term NestedElement with ExampleThingElementCollection. Use the element name "things." An example of the configuration file that is now supported by our code looks like this:

XML
<configuration>
  <configSections>
    <section name="example" type="Examples.Configuration.ExampleSection, 
                                     Examples.Configuration" />
  </configSections>

  <example
    stringValue="A sample string value."
    boolValue="true"
    timeSpanValue="5:00:00"
  >
    <nestedElement
      dateTimeValue="10/16/2006"
      integerValue="1"
    />
    <things>
      <add name="slimy" type="goo" />
      <add name="metal" type="metal" color="silver" />
      <add name="block" type="wood" color="tan" />
    </things>
  </example>
</configuration>

Advanced Element Collections

In the previous section, you learned how to create a standard ARC map type of collection, which is the default. There are a total of four types of collections, two variations of two types: AddRemoveClearMap and AddRemoveClearMapAlternate, and BasicMap and BasicMapAlternate. You know how an AddRemoveClearMap works, from the previous section. A BasicMap is more restrictive than an ARC map in that it does not allow web.config files at a lower level to modify anything inherited from its parent, but it does allow element names other than <add>. The alternate versions of the two main types simply sort elements differently, adding all inherited items so that they are listed last.

In the previous example, we created a collection to list things and each thing was added with an <add> element. For our purposes, full cascaded merging support is probably not necessary and it would be nice to use <thing> as the item element name, rather than <add>. We can use one of a variety of ways to accomplish this, but we will use the most common to modify our original ExampleThingElementCollection class to be a BasicMap and use the element name <thing>:

C#
[ConfigurationCollection(typeof(ThingElement), AddItemName="thing", 
      CollectionType=ConfigurationElementCollectionType.BasicMap)]
public class ExampleThingElementCollection: ConfigurationElementCollection
{
    #region Constructors
    // ...
    #endregion

    #region Fields
    // ...
    #endregion

    #region Properties
    // ...

    public override ConfigurationElementCollectionType CollectionType
    {
        get { return ConfigurationElementCollectionType.BasicMap; }
    }

    protected override string ElementName
    {
        get { return "thing"; }
    }
    #endregion

    #region Indexers
    // ...
    #endregion

    #region Methods
    // ...
    #endregion
    
    #region Overrides
    // ...
    #endregion
}

These simple changes will update our collection class to a BasicMap that expects an element named <thing> in the .config file to add each item, rather than <add>. We can now modify our configuration file like so, which is nicer and clearer than the previous version:

XML
<configuration>
  <configSections>
    <section name="example" type="Examples.Configuration.ExampleSection,
                                     Examples.Configuration" />
  </configSections>

  <example
    stringValue="A sample string value."
    boolValue="true"
    timeSpanValue="5:00:00"
  >
    <nestedElement
      dateTimeValue="10/16/2006"
      integerValue="1"
    />
    <things>
      <thing name="slimy" type="goo" />
      <thing name="metal" type="metal" color="silver" />
      <thing name="block" type="wood" color="tan" />
    </things>
  </example>
</configuration>

Sometimes, when you have a configuration hierarchy and settings cascade from a parent web.config to a child web.config, you need to control which elements appear first in a collection. By default, all elements will be added to a collection in the order in which they are encountered. This order is not particularly well defined when working with multiple web.config files that have been merged. By using an alternate map type, you are given some control over the order of items by forcing all inherited elements to be listed last. A cascade of three web.config files will list the items in the order they are read from each file, starting with the lowest level web.configs first, followed by the parent web.configs and ending with the root web.configs last. Both ARC maps and basic maps support alternates that behave this way, with the added caveat that basic maps prevent child web.config files from modifying items added by their parents.

Custom Configuration Section Groups

Depending on the kind of projects you work on or the specific configuration needs of your application, you may find it useful to group configuration sections. The .NET 2.0 configuration features provide a facility to accomplish this, which we touched on before in our discussion of the <system.web> configuration group for ASP.NET. Creating a configuration section group is simpler than creating a section, but using and accessing them is slightly trickier. Assuming we have two ConfigurationSection classes called ExampleSection and AnotherSection, we could write a ConfigurationSectionGroup like so:

C#
public sealed class ExampleSectionGroup: ConfigurationSectionGroup
{
    #region Constructors
    public ExampleSectionGroup()
    {
    }
    #endregion

    #region Properties
    [ConfigurationProperty("example")]
    public ExampleSection Example
    {
        get { return (ExampleSection)base.Sections["example"]; }
    }

    [ConfigurationProperty("another")]
    public AnotherSection Another
    {
        get { return (AnotherSection)base.Sections["another"]; }
    }
    #endregion
}

Once we have our section group coded, we need to define it in our .config file. This is done very similarly to defining a normal section, only with an added level of hierarchy. It is important to note that the configuration sections ExampleSection and AnotherSection must now be defined as children of our section group:

XML
<configuration>
  <configSections>
    <sectionGroup name="example.group" 
        type="Examples.Configuration.ExampleSectionGroup, 
              Examples.Configuration">
      <section name="example" type="Examples.Configuration.ExampleSection,
                                       Examples.Configuration" />
      <section name="another" type="Examples.Configuration.AnotherSection,
                                       Examples.Configuration" />
    </sectionGroup>
  </configSections>

  <example.group>

    <example
      stringValue="A sample string value."
      boolValue="true"
      timeSpanValue="5:00:00"
    >
      <nestedElement
        dateTimeValue="10\16\2006"
        integerValue="1"
      />
      <things>
        <thing name="slimy" type="goo" />
        <thing name="metal" type="metal" color="silver" />
        <thing name="block" type="wood" color="tan" />
      </things>
    </example>

    <another value="someValue" />

  </example.group>
</configuration>

This configuration section group does just that; it groups two configuration sections together. In addition, it provides a central point of access to those sections. Once we have a reference to our ConfigurationSectionGroup object, we can access the ExampleSection and AnotherSection sections without having to call ConfigurationManager.GetSection() again. However, getting the initial reference to our ExampleSectionGroup isn't quite as simple as getting a single section. Delving a little deeper into the .NET 2.0 configuration classes, we'll find the Configuration class. This class directly represents an application's configuration as defined in its .config file. Like the ConfigurationManager class, Configuration has a GetSection() method and adds the GetSectionGroup() method, as well. You can access a configuration section group, and the sections within it, like so:

C#
private string m_string;
private bool m_bool;
private TimeSpan m_timespan;
private DateTime m_datetime;
private int m_int;

void GetExampleSettings()
{
    Configuration config = 
      ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None);
    ExampleSectionGroup group = config.GetSectionGroup("example.group") 
                                as ExampleSectionGroup;

    ExampleSection section = group.Example;
    m_string = section.StringValue;
    m_bool = section.BooleanValue;
    m_timespan = section.TimeSpanValue;
    m_datetime = section.Nested.DateTimeValue;
    m_int = section.Nested.IntegerValue;

    AnotherSection section2 = group.Another;
}

While the above code is nothing near complicated, requesting the Configuration object to gain access to the GetSectionGroup() method is not very apparent. Once you have the Configuration object, however, you will find that it has several additional useful features. In the next section, we will discuss using the Configuration object to save changes to configuration files, provided that saving support was coded in.

Saving Configuration Changes

So far, we have looked at using the .NET 2.0 configuration features to define and load configuration settings. For many applications, this is sufficient. However, there are many times when configuration must be saved, as well as used. The Configuration object, which we used in the previous section, provides a way for developers to save changes to their configuration sections programmatically. The only prerequisite is writing configuration objects that allow changes to be made. Let's revisit our original ExampleSection class and make it modifiable:

C#
#region Properties
/// <summary>
/// Gets the StringValue setting.
/// </summary>
[ConfigurationProperty("stringValue", IsRequired=true)]
public string StringValue
{
    get { return (string)base[s_propString]; }
    set { base[s_propString] = value; }
    // Allows setting to be changed
}

/// <summary>
/// Gets the BooleanValue setting.
/// </summary>
[ConfigurationProperty("boolValue")]
public bool BooleanValue
{
    get { return (bool)base[s_propBool]; }
    set { base[s_propBool] = value; }
    // Allows setting to be changed
}

/// <summary>
/// Gets the TimeSpanValue setting.
/// </summary>
[ConfigurationProperty("timeSpanValue")]
public TimeSpan TimeSpanValue
{
    get { return (TimeSpan)base[s_propTimeSpan]; }
    set { base[s_propTimeSpan] = value; }
    // Allows setting to be changed
}

/// <summary>
/// Override the Properties collection and return our custom one.
/// </summary>
public override ConfigurationPropertyCollection Properties
{
    get { return s_properties; }
}
#endregion

As you can see, the change is rudimentary compliments of the Object-Model configuration provided by the .NET 2.0 framework. Simply adding setters to the configuration properties of an element allows them to be changed in code. The ConfigurationElement, which is ultimately at the root of every configuration class you may write, handles all the underlying complexity of making sure the changes are validated, converted and ultimately saved to the .config file. To make a ConfigurationElementCollection modifiable, you must add methods to edit the collection. Our previous ExampleThingElementCollection class can be made modifiable by adding a few more methods:

C#
#region Methods
public void Add(ThingElement thing)
{
    base.BaseAdd(thing);
}

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

public void Remove(ThingElement thing)
{
    base.BaseRemove(GetElementKey(thing));
}

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

public void RemoveAt(int index)
{
    base.BaseRemoveAt(index);
}

public string GetKey(int index)
{
    return (string)base.BaseGetKey(index);
}
#endregion

Make sure you remember to add setters to your ConfigurationElement that is contained by the ConfigurationElementCollection. It's also useful to add some public constructors to simplify creation and population of your collected ConfigurationElements. Once you have made the necessary changes to allow your configuration settings to be modified at run time, you will be able to call the Save() method of the Configuration class. The Save() method has three different overloads, allowing you to control what exactly is saved and to force a save even if there are no changes:

  • Configuration.Save() - Saves only modified values if any changes exist
  • Configuration.Save(ConfigurationSaveMode) - Saves the specified level of changes, if any changes exist
  • Configuration.Save(ConfigurationSaveMode, bool) - Saves the specified level of changes, forcing a save to take place if the second parameter is true

The ConfigurationSaveMode enumeration has the following values:

  • Full - Saves all configuration properties, whether they have changed or not
  • Modified - Saves properties that have been modified, even if the current value is the same as the original
  • Minimal - Saves only properties that have been modified and have different values than the original

The Configuration object also has a SaveAs() method with the same basic overloads as the Save() method. The SaveAs methods all require a filename as the first parameter, representing the path the configuration file should be saved to.

Configuration Tips and Tricks

In my time researching and experimenting with configuration sections, I've learned a few tricks that can make using them easier. Some aspects of custom configuration sections can be tedious, such as calling ConfigurationManager.GetSection("someSectionName") as SomeSectionClass; all the time. To alleviate the tediousness, I try to implement the following pattern in each of my ConfigurationSection classes:

C#
public class SomeConfigurationSection
{
    static SomeConfigurationSection()
    {
        // Preparation...
    }
    
    // Properties...
    
    #region GetSection Pattern
    private static SomeConfigurationSection m_section;
    
    /// <summary>
    /// Gets the configuration section using the default element name.
    /// </summary>
    public static SomeConfigurationSection GetSection()
    {
        return GetSection("someConfiguration");
    }
    
    /// <summary>
    /// Gets the configuration section using the specified element name.
    /// </summary>
    public static SomeConfigurationSection GetSection(string definedName)
    {
        if (m_section == null)
        {       
            m_section = ConfigurationManager.GetSection(definedName) as 
                        SomeConfigurationSection;
            if (m_section == null)
                throw new ConfigurationException("The <" + definedName + 
                      "> section is not defined in your .config file!");
        }    
        
        return m_section;
    }
    #endregion
}

The pattern above adds a static GetSection() method to each custom ConfigurationSection class. The overload that takes a string parameter allows you to define a different name for the element in the .config file if desired. Otherwise, the default overload can be used. This pattern works great for configuration sections that are used by a standard application (.exe). However, if your configuration section might be used in a web.config file, you will need to use the following code:

C#
using System.Web;
using System.Web.Configuration;

public class SomeConfigurationSection
{
    static SomeConfigurationSection()
    {
        // Preparation...
    }
    
    // Properties...
    
    #region GetSection Pattern
    private static SomeConfigurationSection m_section;
    
    /// <summary>
    /// Gets the configuration section using the default element name.
    /// </summary>
    /// <remarks>
    /// If an HttpContext exists, uses the WebConfigurationManager
    /// to get the configuration section from web.config.
    /// </remarks>
    public static SomeConfigurationSection GetSection()
    {
        return GetSection("someConfiguration");
    }
    
    /// <summary>
    /// Gets the configuration section using the specified element name.
    /// </summary>
    /// <remarks>
    /// If an HttpContext exists, uses the WebConfigurationManager
    /// to get the configuration section from web.config.
    /// </remarks>
    public static SomeConfigurationSection GetSection(string definedName)
    {
        if (m_section == null)
        {
            string cfgFileName = ".config";
            if (HttpContext.Current == null)
            {
                m_section = ConfigurationManager.GetSection(definedName) 
                            as SomeConfigurationSection;
            }
            else
            {
                m_section = WebConfigurationManager.GetSection(definedName) 
                            as SomeConfigurationSection;
                cfgFileName = "web.config";
            }
                
            if (m_section == null)
                throw new ConfigurationException("The <" + definedName + 
                  "> section is not defined in your " + 
                  cfgFileName + " file!");
        }    
        
        return m_section;
    }
    #endregion
}

As you can see, accessing configuration sections under ASP.NET requires the use of System.Web.Configuration.WebConfigurationManager, rather than the System.Configuration.ConfigurationManager. Checking for the current HttpContext is sufficient to determine which manager needs to be used to get the configuration section. There is one more enhancement we can make to this pattern, which will allow changes to be saved. We know the Configuration object is required to save changes, since the manager classes do not provide a Save() method. We could create a Configuration object from within our GetSection method, but ultimately that would be inflexible and possibly inefficient. In the final and full pattern, I do the following, which takes a Configuration object as a parameter:

C#
using System.Web;
using System.Web.Configuration;

public class SomeConfigurationSection
{
    static SomeConfigurationSection()
    {
        // Preparation...
    }
    
    // Properties...
    
    #region GetSection Pattern
    // Dictionary to store cached instances of the configuration object
    private static Dictionary<string, 
            SomeConfigurationSection> m_sections;
    
    /// <summary>
    /// Finds a cached section with the specified defined name.
    /// </summary>
    private static SomeConfigurationSection 
            FindCachedSection(string definedName)
    {
        if (m_sections == null)
        {
            m_sections = new Dictionary<string, 
                             SomeConfigurationSection>();
            return null;
        }
        
        SomeConfigurationSection section;
        if (m_sections.TryGetValue(definedName, out section))
        {
            return section;
        }
        
        return null;
    }
    
    /// <summary>
    /// Adds the specified section to the cache under the defined name.
    /// </summary>
    private static void AddCachedSection(string definedName, 
                   SomeConfigurationSection section)
    {
        if (m_sections != null)
            m_sections.Add(definedName, section);
    }
    
    /// <summary>
    /// Removes a cached section with the specified defined name.
    /// </summary>
    public static void RemoveCachedSection(string definedName)
    {
        m_sections.Remove(definedName);
    }
    
    /// <summary>
    /// Gets the configuration section using the default element name.
    /// </summary>
    /// <remarks>
    /// If an HttpContext exists, uses the WebConfigurationManager
    /// to get the configuration section from web.config. This method
    /// will cache the instance of this configuration section under the
    /// specified defined name.
    /// </remarks>
    public static SomeConfigurationSection GetSection()
    {
        return GetSection("someConfiguration");
    }
    
    /// <summary>
    /// Gets the configuration section using the specified element name.
    /// </summary>
    /// <remarks>
    /// If an HttpContext exists, uses the WebConfigurationManager
    /// to get the configuration section from web.config. This method
    /// will cache the instance of this configuration section under the
    /// specified defined name.
    /// </remarks>
    public static SomeConfigurationSection GetSection(string definedName)
    {
        if (String.IsNullOrEmpty(definedName))
            definedName = "someConfiguration";
            
        SomeConfigurationSection section = FindCachedSection(definedName);
        if (section == null)
        {
            string cfgFileName = ".config";
            if (HttpContext.Current == null)
            {
                section = ConfigurationManager.GetSection(definedName) 
                          as SomeConfigurationSection;
            }
            else
            {
                section = WebConfigurationManager.GetSection(definedName) 
                          as SomeConfigurationSection;
                cfgFileName = "web.config";
            }
                
            if (section == null)
                throw new ConfigurationException("The <" + definedName + 
                   "> section is not defined in your " + cfgFileName + 
                   " file!");
                
            AddCachedSection(definedName, section);
        }
        
        return section;
    }
    
    /// <summary>
    /// Gets the configuration section using the default element name 
    /// from the specified Configuration object.
    /// </summary>
    /// <remarks>
    /// If an HttpContext exists, uses the WebConfigurationManager
    /// to get the configuration section from web.config.
    /// </remarks>
    public static SomeConfigurationSection GetSection(Configuration config)
    {
        return GetSection(config, "someConfiguration");
    }
    
    /// <summary>
    /// Gets the configuration section using the specified element name 
    /// from the specified Configuration object.
    /// </summary>
    /// <remarks>
    /// If an HttpContext exists, uses the WebConfigurationManager
    /// to get the configuration section from web.config.
    /// </remarks>
    public static SomeConfigurationSection GetSection(Configuration config, 
                                           string definedName)
    {
        if (config == null)
            throw new ArgumentNullException("config", 
                  "The Configuration object can not be null.");
            
        if (String.IsNullOrEmpty(definedName))
            definedName = "someConfiguration";
            
        SomeConfigurationSection section = config.GetSection(definedName) 
                                           as SomeConfigurationSection;
                
        if (section == null)
            throw new ConfigurationException("The <" + definedName + 
                  "> section is not defined in your .config file!");
        
        return section;
    }
    #endregion
}

By passing in the Configuration object, a savable instance of the configuration section can be retrieved for a section in the XML with the defined name. This brings me to another important tip regarding configuration sections. The name used for the configuration element does not necessarily have to be fixed, nor can there be only a single instance of a configuration section. Each configuration section can be defined and implemented multiple times in a .config file, simply by giving each instance a different name:

XML
<configuration>
  <configSections>
    <section name="example1" type="Examples.Configuration.ExampleSection,
                                      Examples.Configuration" />
    <section name="example2" type="Examples.Configuration.ExampleSection, 
                                      Examples.Configuration" />
    <section name="example3" type="Examples.Configuration.ExampleSection, 
                                      Examples.Configuration" />
  </configSections>
  
  <example1 />
  <example2 />
  <example3 />
</configuration>

Section groups can also be defined multiple times in the same way. This allows a common custom configuration structure to be specified that can be used in multiple ways at the same time in the same application, and gives custom configuration reusability. Because it is possible to define a configuration section multiple times in a .config file, the final pattern implemented above includes a simple instance cache. Each time GetSection(string) is called with a different definedName, a different configuration section object will be stored in the cache and returned. Subsequent calls with the same name will return the same cached instance. Another important aspect of this pattern is the lack of caching for the two new versions of GetSection that take a Configuration object as a parameter. There is much more overhead involved when using a Configuration object rather than ConfigurationManager or WebConfigurationManager. Calling GetSection() on a configuration manager class will perform full caching on the section, whereas calling it on the Configuration object will cause the section to be reparsed each time. Generally speaking, the Configuration object should only be used to perform a save of configuration changes. A configuration manager class should be used to access sections for reading. This will ensure the best performance when using configuration settings.

One last tip while we are on the subject of performance. Unless you implement an advanced configuration section or element that includes event notifications, caching configuration settings in variables is generally discouraged. More on this will be discussed in the advanced section below, but first consider the following very simple scenario:

C#
public class SomeProgram
{
    static Main()
    {
        s_config = MyConfig.GetSection();
        s_duration = s_config.Duration;
        
        s_timer = new Timer(
            new TimerCallback(),
            null,
            TimeSpan.Zero
            s_duration
        );
        
        Console.ReadKey();
        s_timer.Dispose();
    }
    
    private static MyConfig s_config;
    private static Timer s_timer;
    private static TimeSpan s_duration;
    
    private static void WriteCurrentTime(object data)
    {
        Console.WriteLine("The current time is " + DateTime.Now.ToString());
        
        if (s_duration != s_config.Duration)
        {
            s_duration = s_config.Duration;
            s_timer.Change(TimeSpan.Zero, s_duration);
        }
    }
}

In the application above, we wish for the timer to change its interval if the configuration file is updated. The instance of our configuration section, s_config, will always be up-to-date. So if the .config file changes while the application is running, any changes will be found and loaded into memory. If you implement your configuration sections as I have in this article, by overriding the static constructor and replacing the Properties collection, then your collections will be about as performant as they can be. This makes accessing a configuration property a relatively cheap operation, so the above code could be rewritten as follows:

C#
public class SomeProgram
{
    static Main()
    {
        s_config = MyConfig.GetSection();
        
        s_timer = new Timer(
            new TimerCallback(),
            null,
            TimeSpan.Zero
            s_config.Duration
        );
        
        Console.ReadKey();
        s_timer.Dispose();
    }
    
    private static MyConfig s_config;
    private static Timer s_timer;
    private static TimeSpan s_duration;
    
    private static void WriteCurrentTime(object data)
    {
        Console.WriteLine("The current time is " + 
                          DateTime.Now.ToString());       
        s_timer.Change(TimeSpan.Zero, s_config.Duration);
    }
}

If this example is too simple to reveal the significance of using configuration settings directly, then imagine a more complex scenario where a configuration value is cached in a variable. That variable is subsequently passed through a chain of calls and later used in a loop. If the desired outcome is for any changes to the configured timeout to be immediately noticed and responded to, then caching the value in a variable will not work. You must access the configuration property directly, without caching it in a variable. Configuration settings are accessible globally, anywhere in an application. This means that you can access a configuration property anywhere in your code without needing to cache a value in a variable and pass a variable around. This makes for cleaner code, since you require fewer parameters. If top performance is an absolute necessity, it is possible to write a configuration section with events that can notify consumers when its configuration data has been changed on disk. However, this is a more advanced topic and will be discussed at a later time.

Advanced Configuration Topics

The information outlined in this article provides a thorough introduction to the configuration features available in the .NET 2.0 Framework. However, this is by no means a comprehensive documentation and there are a variety of more complex uses for configuration sections. Additional information will be available in subsequent articles:

Appendices

Appendix A: The Configuration Collection Cascade

In an ASP.NET application, web.config files may be created for any IIS "application." In the event that the virtual folder of an application is the child of another application, web.config files from the parent application will be merged with the web.config file of the child application. Since applications in IIS can be nested to any number of levels deep, a configuration cascade is created when web.config files for child applications are loaded.

Assume we have a site set up in IIS with the following hierarchy and that each web.config file contains a common collection:

  \wwwroot
      web.config
      \firstapp
          web.config
      \anotherapp
          web.config
          \childapp
              web.config
      \finalapp
          web.config

<!-- \wwwroot\web.config -->
<configuration>
    <commonCollection>
        <add key="first"  value="C98E4F32123A" />
        <add key="second" value="DD0275C8EA1B" />
        <add key="third"  value="629B59A001FC" />
    </commonCollection>
</configuration>

<!-- \wwroot\firstapp\web.config -->
<configuration>
    <commonCollection>
        <remove key="first" />        
        <add key="first"  value="FB54CD34AA92" />
        
        <add key="fourth" value="DE67F90ACC3C" />
    </commonCollection>
</configuration>

<!-- \wwroot\anotherapp\web.config -->
<configuration>
    <commonCollection>
        <add key="fourth" value="123ABC456DEF" />
        <add key="fifth"  value="ABC123DEF456" />
        <add key="sixth"  value="0F9E8D7C6B5A" />
    </commonCollection>
</configuration>

<!-- \wwroot\anotherapp\childapp\web.config -->
<configuration>
    <commonCollection>
        <remove key="second" />
        <remove key="fourth" />
        <remove key="sixth" />

        <add key="seventh" value="ABC123DEF456" />
        <add key="ninth"  value="0F9E8D7C6B5A" />
    </commonCollection>
</configuration>

<!-- \wwroot\lastapp\web.config -->
<configuration>
    <commonCollection>
        <clear />
        
        <add key="first"  value="AABBCCDDEEFF" />
        <add key="second" value="112233445566" />
        <add key="third"  value="778899000000" />
        <add key="fourth" value="0A0B0C0D0E0F" />
    </commonCollection>
</configuration>

If we examined the collection in each application, the results would be as follows:

  • \wwwroot\web.config
    • first = C98E4F32123A
    • second = DD0275C8EA1B
    • third = 629B59A001FC
  • \wwwroot\firstapp\web.config
    • first = FB54CD34AA92
    • second = DD0275C8EA1B
    • third = 629B59A001FC
    • fourth = DE67F90ACC3C
  • \wwwroot\anotherapp\web.config
    • first = C98E4F32123A
    • second = DD0275C8EA1B
    • third = 629B59A001FC
    • fourth = 123ABC456DEF
    • fifth = ABC123DEF456
    • sixth = 0F9E8D7C6B5A
  • \wwwroot\anotherapp\childapp\web.config
    • first = C98E4F32123A
    • third = 629B59A001FC
    • fifth = ABC123DEF456
    • seventh = ABC123DEF456
    • ninth = 0F9E8D7C6B5A
  • \wwwroot\lastapp\web.config
    • first = AABBCCDDEEFF
    • second = 112233445566
    • third = 778899000000
    • fourth = 0A0B0C0D0E0F

I hope this simple illustration of how settings are inherited by web.config files in a child application, and how those settings can be overridden, is sufficient. It may not be often that you encounter such a situation, but understanding what happens and how to override configuration settings from a parent web.config should help alleviate config file issues for ASP.NET developers.

Appendix B: Including External Configuration Files

Despite all the greatness to be found in .NET 2.0's configuration features, there is one drawback. When working on a single project across multiple environments, managing configuration can become a nightmare. The process of managing multiple versions of a configuration file for multiple environments -- i.e., development, testing, staging and production -- at my current job involves manual comparisons of .config files whenever changes are deployed to one environment or another, with a manual merging process. I spent months trying to find a better way and eventually found one. Enter one of those oh-so beloved "undocumented" -- or in this case, just poorly documented -- features that Microsoft is so famous for: configSource. I only came across this little gem when I was digging through the .NET 2.0 configuration source code with Reflector, wonderful little tool.

Each configuration section, when parsed and loaded by the .NET configuration classes, is assigned a SectionInformation object. The SectionInformation object contains meta information about a configuration section and allows some management of how sections override each other when defined in a child config file (ASP.NET). For now, we will ignore the majority of what SectionInformation has to offer, save the ConfigSource property. By adding a configSource attribute to the root element of any ConfigurationSection, you can specify an alternate, external source from which the configuration settings will be loaded.

XML
<!-- SomeProgram.exe.config -->
<configuration>
  <connectionStrings configSource="externalConfig/connectionStrings.config"/>
</configuration>

<!-- externalConfig/connectionStrings.config -->
<connectionStrings>
  <add name="conn" connectionString="blahblah" />
</connectionStrings>

In the configuration file above, the <connectionStrings> section has been sourced from a file called externalConfig/connectionStrings.config. All of the application's connection strings will be loaded from the specified file. Now that the connection strings are loaded from an external resource, it is a relatively simple matter to create a connectionStrings.config file in each environment at the same relative location. Hence, the externalConfig/ part of the connectionStrings.config path. The beauty here is that we can define connection strings properly for each environment once. We do not have to worry about accidentally overriding those settings during a deployment where a config file was either merged improperly or not merged at all. This can be a huge boon when deploying changes in an application to a production environment, where it is critical that the correct database connection strings exist. The downfall of using the configSource attribute is that it requires all configuration settings to be placed in the external file. No inheritance or overriding is possible, which in some cases makes it useless. All external configuration files used with the configSource attribute must also reside in a relative child path to the main .config file. I believe this is in regards to security concerns with storing the file in a relative parent path in a web environment.

Something else to note is that the <appSettings> section has a better alternative to using configSource, called file. If you use the file attribute rather than configSource with the <appSettings> section, you can define settings in both the root .config file and in the referenced file. Settings from the root .config file may also be overridden in the referenced file, simply by adding something with the same key. Sadly, the file attribute is only available on the <appSettings> section and is not built into the configuration framework. It is possible to implement a similar attribute in your own configuration sections. This will be discussed in a future installment of advanced configuration topics, after several prerequisite installments ;).

Article Revision History

  • 1.0 [11.21.2006]
    • Original article, complete with all its glorious spelling errors, misnomers and unintentional omissions ;)
  • 1.1 [11.22.2006]
    • Added Appendix B discussing how to modularize configuration using include files
  • 1.2 [06.22.2007] 
    • Improved article content
    • Made sure all examples compile
    • Added downloadable example project

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Architect
United States United States
Jon Rista has been programming since the age of 8 (first Pascal program), and has been a programmer since the age of 10 (first practical program). In the last 21 years, he has learned to love C++, embrace object orientation, and finally enjoy the freedom of C#. He knows over 10 programming languages, and vows that his most important skill in programming is creativity, even more so than logic. Jon works on large-scale enterprise systems design and implementation, and employs Design Patterns, C#, .NET, and SQL Server in his daily doings.

Comments and Discussions

 
NewsSeparate .config file for system.diagnostics settings. Additional example for Appendix B. Pin
Nick Alexeev14-Dec-18 19:20
professionalNick Alexeev14-Dec-18 19:20 
QuestionWow Awesome Article Pin
MarkWardell2-Jul-17 2:51
MarkWardell2-Jul-17 2:51 
GeneralMy vote of 5 Pin
Robert_Dyball22-Jun-17 12:08
professionalRobert_Dyball22-Jun-17 12:08 
QuestionHow to use an unsigned int or UInt32 within the custom ConfigurationElement Pin
YLMaxwell19-Oct-16 11:13
YLMaxwell19-Oct-16 11:13 
QuestionUnderstanding object model Pin
R3turnz26-May-16 0:56
R3turnz26-May-16 0:56 
QuestionProperty Collection question Pin
Member 1242119128-Mar-16 5:37
Member 1242119128-Mar-16 5:37 
PraiseGreat Explanation Pin
Member 121917068-Dec-15 2:33
Member 121917068-Dec-15 2:33 
PraiseWonderful article(s) Pin
YLMaxwell12-Nov-15 11:36
YLMaxwell12-Nov-15 11:36 
QuestionOne important note Pin
rosj918-Oct-15 18:54
rosj918-Oct-15 18:54 
QuestionRE: Excellent read so far but i have a question Pin
rosj917-Oct-15 17:28
rosj917-Oct-15 17:28 
Questiona thorny and interesting problem.... Pin
Member 1179891227-Aug-15 22:08
Member 1179891227-Aug-15 22:08 
AnswerRe: a thorny and interesting problem.... Pin
Jon Rista28-Aug-15 7:23
Jon Rista28-Aug-15 7:23 
GeneralRe: a thorny and interesting problem.... Pin
Member 1179891228-Aug-15 15:25
Member 1179891228-Aug-15 15:25 
GeneralRe: a thorny and interesting problem.... Pin
Jon Rista30-Aug-15 5:25
Jon Rista30-Aug-15 5:25 
QuestionAdd new entry in datagrid view produces error Pin
jayyers31-May-15 8:26
jayyers31-May-15 8:26 
QuestionThank You! Pin
Sling Blade 228-Feb-15 0:35
professionalSling Blade 228-Feb-15 0:35 
GeneralAwesome and thank you Pin
Avi Farah24-Aug-14 17:02
Avi Farah24-Aug-14 17:02 
GeneralGreat post Pin
Lucas A3-Jul-14 12:20
Lucas A3-Jul-14 12:20 
GeneralMy vote of 5 Pin
RMuesi10-May-14 18:42
RMuesi10-May-14 18:42 
QuestionCan I modify collection entries when merging config files? Pin
Member 409051825-Mar-14 4:46
Member 409051825-Mar-14 4:46 
GeneralAddition to: Can I modify collection entries when merging config files? Pin
Member 409051826-Mar-14 0:13
Member 409051826-Mar-14 0:13 
QuestionAny new features since .NET 2.0? Pin
Member 409051822-Feb-14 23:36
Member 409051822-Feb-14 23:36 
AnswerRe: Any new features since .NET 2.0? Pin
Jon Rista23-Feb-14 9:10
Jon Rista23-Feb-14 9:10 
QuestionCan you use WebConfigurationManager also for "exe" applications? Pin
Member 409051822-Feb-14 23:30
Member 409051822-Feb-14 23:30 
AnswerRe: Can you use WebConfigurationManager also for "exe" applications? Pin
Jon Rista23-Feb-14 9:09
Jon Rista23-Feb-14 9:09 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.