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

Decoding the Mysteries of .NET 2.0 Configuration

Rate me:
Please Sign up or sign in to vote.
4.90/5 (71 votes)
10 Dec 2006CPOL18 min read 226.4K   242   32
Mysteries of .NET 2.0 Configuration framework
In this article, we will continue delving into the mysteries of the powerful .NET 2.0 Configuration framework, and learn how to write validated, type-safe, and performant 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 accomplish 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 Continuing Mystery

In the first installment of the Mysteries of .NET 2.0 Configuration, I covered some of the basics of the configuration framework. Creation of custom configuration sections, section groups, elements, and collections encompass the core features of the configuration framework, but are only the beginning. In this article, we will begin decoding some of the more interesting pieces of the .NET 2.0 configuration puzzle such as type conversion and validation. This article will also cover some of the performance and usability concerns of the .NET 2.0 Configuration framework, and how to improve or overcome them.

This article is a continuation of the Mysteries of .NET 2.0 Configuration series. The first article, "Unraveling the Mysteries of .NET 2.0 Configuration", can be found here. If you are unfamiliar with what the .NET 2.0 Configuration framework has to offer, and do not know how to write and use custom ConfigurationSection classes, it is recommended that you read the first installment of this series before continuing.

Topics of Configuration

  1. Validating Configuration Data
    1. Using Premade Validators
    2. The Callback Validator
    3. Writing Your Own Validator
  2. Maintaining Type Safety
    1. Using Premade Converters
    2. Writing Your Own Converters
  3. Concerning Performance
  4. Best Configuration Practices
  5. Article History

Validating Configuration Data

As we learned in "Unraveling the Mysteries", writing custom configuration sections is quite simple and straightforward. The benefits of writing a .NET 2.0 ConfigurationSection are efficient, globally accessible, type-safe, and validated configuration. Type safety and validation are two topics that were briefly discussed in the original article, but which were never elaborated on. These two aspects of configuration, while not always critical, can be very important to an application's health and correctness when you expect specific ranges and types of data.

Validating the correctness of data in your custom configuration is very straightforward. The .NET 2.0 Configuration framework actually includes several premade validators that should suffice for most validation needs. In the event that one of the premade validators is not sufficient, there are two additional methods of validating your configuration data. It is important to note that only one validator at a time can be applied to a configuration property. Sometimes you may find yourself needing to use two validators, and the simplest solution to this is to create your own wrapper validator, using the two premade solutions within your custom validator class.

Using Premade Validators

Using premade validators in your custom configuration classes is a simple matter of applying them to the properties you wish to be validated. There are two ways of applying validators, one imperative and one declarative. If you choose to explicitly create your configuration properties and override the Properties collection of your ConfigurationSection and ConfigurationElements, you must use the imperative method. Otherwise, you may declaratively apply a validator to each property using attributes. As you will see from the list below, all configuration validator classes have a matching attribute class.

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
  • 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

For the most part, these validators are self-explanatory. Validating ints, longs, and TimeSpans is a fairly trivial matter that requires little in the way of examples, but they do make for a simple introduction. There are some additional features of these validators that also warrant discussion. Let's assume we have a configuration section like the one below:

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

  <example myTimeSpan="8:15:00" myInt="10" myLong="6018427387904" />
</configuration>

We can write a validated configuration section that ensures the data typed into each of the three properties is indeed valid TimeSpan, int, and long data, and that it falls within the range we desire. In the event that the data is not correct, an ArgumentException will be thrown during serialization or deserialization. (Note: Without a validator applied to a property, the exception thrown could be one of a few, including ConfigurationErrorsException, NullReferenceException, or InvalidCastException. By using a validator, we know what exception to look for in the event some configuration data is invalid.) Take special note that we can restrict the range of values with our validators. Our configuration section should look something like this:

C#
public class ValidatedExampleSection: ConfigurationSection
{
    #region Constructor
    static ValidatedExampleSection()
    {
        s_propMyTimeSpan = new ConfigurationProperty(
            "myTimeSpan",
            typeof(TimeSpan),
            TimeSpan.Zero,
            null,
            new TimeSpanValidator(TimeSpan.Zero, TimeSpan.FromHours(24)),
            ConfigurationPropertyOptions.IsRequired
        );

        s_propMyInt = new ConfigurationProperty(
            "myInt",
            typeof(int),
            0,
            null,
            new IntegerValidator(-10, 10),
            ConfigurationPropertyOptions.IsRequired
        );

        s_propMyLong = new ConfigurationProperty(
            "myLong",
            typeof(long),
            0,
            null,
            new LongValidator(Int64.MinValue, Int64.MaxValue),
            ConfigurationPropertyOptions.IsRequired
        );

        s_properties = new ConfigurationPropertyCollection();

        s_properties.Add(s_propMyTimeSpan);
        s_properties.Add(s_propMyInt);
        s_properties.Add(s_propMyLong);
    }
    #endregion

    #region Fields
    private static ConfigurationPropertyCollection s_properties;
    private static ConfigurationProperty s_propMyTimeSpan;
    private static ConfigurationProperty s_propMyInt;
    private static ConfigurationProperty s_propMyLong;
    #endregion

    #region Properties
    [ConfigurationProperty("myTimeSpan", DefaultValue=TimeSpan.Zero, 
                           IsRequired=true)]
    [TimeSpanValidator(MinValueString="0:0:0", MaxValueString="24:0:0")]
    public TimeSpan MyTimeSpan
    {
        get { return (TimeSpan)base[s_propMyTimeSpan]; }
    }

    [ConfigurationProperty("myInt", DefaultValue=0, IsRequired=true)]
    [IntegerValidator(-10, 10)]
    public int MyInt
    {
        get { return (int)base[s_propMyInt]; }
    }

    [ConfigurationProperty("myLong", DefaultValue=0, IsRequired=true)]
    [LongValidator(Int64.MinValue, Int64.MaxValue)]
    public int MyInt
    {
        get { return (int)base[s_propMyLong]; }
    }
    #endregion
}

In the event that any of the configuration data was outside of the specified ranges (i.e., myInt was -12), you could capture an ArgumentException when ConfigurationManager.GetSection() is called, and take the appropriate action. In addition to being able to check a value within a specified range, you can also specify that the range is exclusive. In this case, any values outside of the specified range are valid, and anything within the specified range is invalid, causing the exception to be thrown. Also, if it was ever desired, these validators can check for a specific single value if the resolution parameter of the full constructor is set. The validator new TimeSpanValidator(TimeSpan.MinValue, TimeSpan.MaxValue, false, 15) would require that the time span equals exactly 15 seconds.

The other premade validators may not be quite as straightforward as the ones just discussed. The StringValidator, as it turns out, does not validate a particular string against what was configured. Rather, it allows validation of the length of the string, ensuring it is between a minimum and maximum value. The StringValidator also allows a string to be specified that determines invalid characters, and throws an exception if the configured string contains any one of those characters. If you do wish to validate that a configured string meets certain formatting requirements, the RegexStringValidator is actually what you need. With this validator, you can specify any standard regular expression which will be matched against the actual configured string. There is no premade validator that will validate both the length of a string and match it against a regular expression.

The last premade validator we will discuss in this section is the SubclassTypeValidator. This handy little validator allows you to verify whether the type of object for a particular configuration property is one that derives from the specified type. This can be very useful when using a custom type converter to serialize and deserialize data from a custom class hierarchy to your configuration files, amongst other things. A more detailed overview of this validator will take place in the Writing Your Own Converter section.

The Callback Validator

Most often, when writing any piece of code, a simple solution is all that's initially needed. In the event you find yourself writing a validated configuration section and one of the premade validators is not sufficient, but your needs are not particularly complex, the CallbackValidator will usually suffice. This handy little validator takes a ValidatorCallback delegate as a parameter, which should point to a method that takes a single object as a parameter. You can perform whatever kind of validation that may be necessary in this callback function.

Let's assume we have a configuration section that expects an integer value to be the modulus of 10, between -100 and 100. The IntegerValidator and LongValidator do not support this kind of validation, so another solution is needed. If you only need to perform this kind of validation once in a single configuration class, the simplest solution is to use the CallbackValidator.

C#
public class ValidatedExampleSection: ConfigurationSection
{
    #region Constructor
    static ValidatedExampleSection()
    {
        s_propMyInt = new ConfigurationProperty(
            "myInt",
            typeof(int),
            0,
            null,
            new CallbackValidator(new 
                ValidatorCallback(ModRangeValidatorCallback)),
                ConfigurationPropertyOptions.IsRequired
        );

        s_properties = new ConfigurationPropertyCollection();

        s_properties.Add(s_propMyInt);
    }
    #endregion

    #region Fields
    private static ConfigurationPropertyCollection s_properties;
    private static ConfigurationProperty s_propMyInt;
    #endregion

    #region Properties
    [ConfigurationProperty("myInt", DefaultValue=0, IsRequired=true)]
    [CallbackValidator("Examples.Configuration.
                        ValidatedExampleSection.ModRangeValidatorCallback")]
    public int MyInt
    {
        get { return (int)base[s_propMyInt]; }
    }
    #endregion

    #region Helpers
    private void ModRangeValidatorCallback(object value)
    {
        int intVal = (int)value;
        if (intVal >= -100 && intVal <= 100)
        {
            if (intVal % 10 != 0)
                throw new ArgumentException("The integer " + 
                     "value is not a multiple of 10.");
        }
        else
        {
            throw new ArgumentException("The integer value is not" + 
                                        " within the range -100 to 100");
        }
    }
    #endregion
}

It is possible to reuse a callback for multiple properties in a ConfigurationSection or ConfigurationElement. If you need to use callbacks across multiple configuration classes, you could create a container class and group all of your callbacks into a single location. However, in the event that you need to reuse a validator for multiple properties or configuration classes, the better solution is to write a custom configuration validator class and its corresponding attribute. This will allow you to more properly encapsulate and share code in a less confusing manner.

Writing Your Own Validator

Writing a custom validator is a simple task, and only requires the class derives from ConfigurationValidatorBase. The ConfigurationValidatorBase class provides two methods that must be overridden:

  • virtual bool CanValidate(Type type) - Determines if the validator is capable of validating the Type type.
  • abstract void Validate(object value) - Validates the object value, throwing an ArgumentException if the value failed validation.

Validators require no special constructors, so you are free to create as many as you like with whatever parameters are necessary. It is interesting to note that configuration validators can be created and used in your own code, and do not necessarily need to be applied to a ConfigurationProperty to be used. This is convenient, since it allows you to wrap several validators into a single validator class. Since a ConfigurationProperty can only have a single validator applied to it, the wrapper validator pattern can be very useful in quickly applying multiple validators.

Below is an example of a custom validator. This validator is also a wrapper for both the StringValidator and the RegexStringValidator. When writing your own validators, you must make certain that you override the virtual method CanValidate. Even though it is not abstract, its default return value is false, which will always cause your validator to fail when invoked during deserialization.

C#
public class RegexStringWrapperValidator: ConfigurationValidatorBase
{
    #region Constructors
    public RegexStringWrapperValidator(string regex) : 
           this (regex, 0, 0x7fffffff)
    {
    }

    public RegexStringWrapperValidator(string regex, int minLength) : 
           this(regex, minLength, 0x7fffffff)
    {        
    }
    
    public RegexStringWrapperValidator(string regex, int minLength, 
                                       int maxLength)
    {
        m_regexValidator = new RegexStringValidator(regex);
        m_stringValidator = new StringValidator(minLength, maxLength);
    }
    #endregion

    #region Fields
    private RegexStringValidator m_regexValidator;
    private StringValidator m_stringValidator;
    #endregion

    #region Overrides
    public override bool CanValidate(Type type)
    {
        return (type == typeof(string));
    }

    public override void Validate(object value)
    {
        m_stringValidator.Validate(value);
        m_regexValidator.Validate(value);
    }
    #endregion
}

As you can see, writing a custom validator is extremely simple. However, we are not done quite yet. We still need to write a custom attribute so we can declaratively apply our validator to a ConfigurationProperty. This step is only useful if you prefer to program declaratively, and is unnecessary if you follow the imperative method proposed in the first article.

C#
public sealed class RegexStringWrapperValidatorAttribute: 
                    ConfigurationValidatorAttribute
{
    #region Constructors
    public RegexStringWrapperValidatorAttribute(string regex)
    {
        m_regex = regex;
        m_minLength = 0;
        m_maxLength = 0x7fffffff;
    }
    #endregion

    #region Fields
    private string m_regex;
    private int m_minLength;
    private int m_maxLength;
    #endregion


    #region Properties
    public string Regex
    {
        get { return m_regex; }
    }

    public int MinLength
    {
        get
        {
            return m_minLength;
        }
        set
        {
            if (m_maxLength < value)
            {
                  throw new ArgumentOutOfRangeException("value", 
                        "The upper range limit value must be greater" + 
                        " than the lower range limit value.");
            }
            m_minLength = value;

        }
    }

    public int MaxLength
    {
        get
        {
            return m_maxLength;
        }
        set
        {
            if (m_minLength > value)
            {
                  throw new ArgumentOutOfRangeException("value", 
                        "The upper range limit value must be greater " + 
                        "than the lower range limit value.");
            }
            m_maxLength = value;
        }
    }

    public override ConfigurationValidatorBase ValidatorInstance
    {
        return new RegexStringWrapperValidator(m_regex, m_minLength, 
                                               m_maxLength);
    }
    #endregion
}

Maintaining Type Safety

A reigning beauty of the .NET platform is its strict type safety. This feature helps in writing safe, secure code. The most vulnerable components of any application are those that access external resources, such as configuration. Thankfully, the .NET 2.0 Configuration framework includes some facilities to help you ensure your configuration data is type safe. Initially, all configuration data is read as a string, and must be converted to the proper type during deserialization to be used. The .NET 2.0 Configuration framework does this automatically for the most part, but there are times when the default conversion is insufficient. Configuration type converters can be used to handle custom conversion, and when used in conjunction with validators, can ensure your configuration data is type safe and secure.

Using Premade Converters

When writing a custom configuration section, type conversion is generally something that is done automatically by the .NET 2.0 Configuration framework. There are some special instances where automatic type conversion is not sufficient, such as when an integer value needs to be any int or infinite, or when a comma-delimited string needs to be converted to a collection. Numerous premade converters exist, and they can be applied declaratively or imperatively to your custom configuration classes:

The Converter Types

  • CommaDelimitedStringCollectionConverter - Converts a comma-delimited value to/from a 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 (white space trimmed from front and back)

Again, these are all self-explanatory and need little explanation, but for posterity's sake, I'll provide an example. One thing to note is that enumerations are handled automatically and quite well by the framework through the use of the GenericEnumConverter, so manually applying it does not seem to have any value. When applying a configuration type converter to a custom configuration class, you use the standard TypeConverterAttribute.

C#
public class TypeSafeExampleSection: ConfigurationSection
{
    #region Constructor
    static TypeSafeExampleSection()
    {
        s_propMyInt = new ConfigurationProperty(
            "myInt",
            typeof(int),
            "Infinite",
            new InfiniteIntConverter(),
            new IntegerValidator(-10, 10),
            ConfigurationPropertyOptions.IsRequired
        );

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

    #region Fields
    private static ConfigurationPropertyCollection s_properties;
    private static ConfigurationProperty s_propMyInt;
    #endregion

    #region Properties
    [ConfigurationProperty("myInt", DefaultValue="Infinite", IsRequired=true)]
    [IntegerValidator(-10, 10)]
    [TypeConverter(typeof(InfiniteIntConverter)]
    public int MyInt
    {
        get { return (int)base[s_propMyInt]; }
    }
    #endregion
}

Writing Your Own Converters

The true value of type converters shows when you run into a scenario where there is no direct correlation between an XML/text representation of data and the .NET classes and structures that store that data. Consider this simple structure:

C#
struct MyStruct
{
    public int Length;
    public int Width;
    public int Height;

    public double X;
    public double Y;
    public double Z;

    public override string ToString()
    {
        return "L: " + Length + ", W: " + Width + ", H: " + Height + 
               ", X: " + X + ", Y: " + Y + ", Z: " + Z;
    }
}

Using such a structure in a configuration file is not as straightforward as using an int, TimeSpan, or string. A common, human-editable way of representing this structure is needed so our .config file can be modified by hand (vs. with a configuration program). Let's assume we have chosen to format our string representation like this:

L: 20, W: 30, H: 10, X: 1.5, Y: 1.0, Z: 7.3

Let's also assume we don't care what order each pair is in, nor what capitalization or spacing there is between each component. Let's also assume that all six components are required, and that none of them can be left out. We can now write a type converter to convert between a string representation and our MyStruct structure. Note that for the ConvertTo method, we simply convert straight to a string without verifying that the type parameter is requesting a string. We also assume that the ConvertFrom method will always be receiving a string value for the data parameter. Since this is a configuration converter, it is safe to always assume these cases.

C#
public sealed class MyStructConverter: ConfigurationConverterBase
{
    public MyStructConverter() { }

    /// <summary>Converts a string to a MyStruct.</summary>
    /// <returns>A new MyStruct value.</returns>
    ///<param name=""ctx"" />The <see 
    /// cref="T:System.ComponentModel.ITypeDescriptorContext">
    /// </see> object used for type conversions.
    ///<param name=""ci"" />The <see 
    /// cref="T:System.Globalization.CultureInfo">
    /// </see> object used during conversion.
    ///<param name=""data"" />The <see cref="T:System.String">
    /// </see> object to convert.
    public override object ConvertFrom(ITypeDescriptorContext ctx, 
                                       CultureInfo ci, object data)
    {
        string dataStr = ((string)data).ToLower();
        string[] values = dataStr.Split(',');
    if (values.Length == 6)
        {
            try
            {
                MyStruct myStruct = new MyStruct();
                foreach (string value in values)
                {
                    string[] varval = value.Split(':');
                    switch (varval[0])
                    {
                        case "l": 
                          myStruct.Length = Convert.ToInt32(varval[1]); break;
                        case "w": 
                          myStruct.Width = Convert.ToInt32(varval[1]); break;
                        case "h": 
                          myStruct.Height = Convert.ToInt32(varval[1]); break;
                        case "x": 
                          myStruct.X = Convert.ToDouble(varval[1]); break;
                        case "y": 
                          myStruct.Y = Convert.ToDouble(varval[1]); break;
                        case "z": 
                          myStruct.Z = Convert.ToDouble(varval[1]); break;
                    }
                }
                return myStruct;
            }
            catch
            {
                throw new ArgumentException("The string contained invalid data.");
            }
        }

        throw new ArgumentException("The string did not contain all six, " + 
                                    "or contained more than six, values.");
    }

    /// <summary>Converts a MyStruct to a string value.</summary>
    /// <returns>The string representing the value 
    ///           parameter.</returns>
    ///<param name=""ctx"" />The <see 
    /// cref="T:System.ComponentModel.ITypeDescriptorContext">
    /// </see> object used for type conversions.
    ///<param name=""ci"" />The <see 
    /// cref="T:System.Globalization.CultureInfo">
    /// </see> object used during conversion.
    ///<param name=""data"" />The <see cref="T:System.String">
    /// </see> object to convert.
    ///<param name=""type"" />The type to convert to.
    public override object ConvertTo(ITypeDescriptorContext ctx, 
           CultureInfo ci, object value, Type type)
    {
        return value.ToString();
    }
}

Concerning Performance

Slow performance is probably one of the top killers of any application, and often the hardest aspect of an application to tune. What runs like a dream in a developer's environment can quickly become overwhelmed in high-throughput, high-use production environments. Optimization is often a long running and ongoing task for many applications. Thankfully, optimizing your custom configuration classes is a very simple matter, and requires hardly any additional work over not optimizing them. Throughout this article and its predecessor, I've written example configuration classes using two methods: declaratively through attributes, and explicitly using static variables and a static constructor. The explicit method is the optimized method, but why it is optimized is not apparent unless you dig into the source code that drives the .NET 2.0 Configuration framework.

The quickest and simplest way of writing a configuration class is to use attributes, applying ConfigurationPropertyAttribute, TypeConverterAttribute, and the various validator attributes to public properties of a simple class. This provides very rapid development, but in the long run, performance will be poor. When such a configuration class is loaded by the .NET 2.0 Configuration framework, a large amount of reflection and object construction must ensue to create the necessary ConfigurationProperty objects and their associated validators and type converters. This is done when the Properties collection of a custom configuration class is accessed, as can be seen in the code snippet below:

C#
/// <summary>Gets the collection of properties.</summary>
/// <returns>The <see 
/// cref="T:System.Configuration.ConfigurationPropertyCollection">
/// </see> collection of properties for the element.</returns>
protected virtual ConfigurationPropertyCollection Properties
{
      get
      {
            ConfigurationPropertyCollection collection1 = null;
            if (ConfigurationElement.PropertiesFromType(base.GetType(), 
                                                        out collection1))
            {
                  ConfigurationElement.ApplyInstanceAttributes(this);
                  ConfigurationElement.ApplyValidatorsRecursive(this);
            }
            return collection1;
      }
}

This is the default ConfigurationElement.Properties property. Whenever it is accessed, the static PropertiesFromType(Type, out ConfigurationPropertyCollection) function is called, which starts a process that discovers and builds any ConfigurationProperty objects that wrap the specified type in your custom configuration class. Note that it builds all ConfigurationProperty objects for the specified type, not just the one you requested. Once a property collection for a specified type is created, it is cached, improving performance on subsequent calls.

C#
private static bool PropertiesFromType(Type type, 
        out ConfigurationPropertyCollection result)
{
      ConfigurationPropertyCollection collection1 = 
       (ConfigurationPropertyCollection) 
        ConfigurationElement.s_propertyBags[type];
      result = null;
      bool flag1 = false;
      if (collection1 == null)
      {
            lock (ConfigurationElement.s_propertyBags.SyncRoot)
            {
                  collection1 = (ConfigurationPropertyCollection) 
                                 ConfigurationElement.s_propertyBags[type];
                  if (collection1 == null)
                  {
                        collection1 = 
                         ConfigurationElement.CreatePropertyBagFromType(type);
                        ConfigurationElement.s_propertyBags[type] = 
                                             collection1;
                        flag1 = true;
                  }
            }
      }
      result = collection1;
      return flag1;
}

It is also important to note that the PropertiesFromType function is static, and as per Microsoft's guidelines, all static methods must be thread-safe. Multiple requests to any property on a configuration class will be serialized by a lock, halting progress on all competing threads until all the ConfigurationProperty objects for the given type have been created and cached. In the event that the ConfigurationPropertyCollection has not been created, the ConfigurationProperty objects are discovered and built with the call to static CreatePropertyBagFromType(Type). This function starts a fairly lengthy process that incurs multiple reflection hits and property table lookups to create each ConfigurationProperty object.

Once your ConfigurationProperty objects have been created and placed in their corresponding ConfigurationPropertyCollection, they are cached for subsequent calls. However, if the underlying .config file changes, the properties must be reloaded, incurring the cost of creation again. It is possible to completely bypass this process by explicitly defining and creating your ConfigurationProperty objects using the static constructor method shown throughout these articles. The trick to understanding why this improves performance is in understanding what overriding the Properties property does. Compare the code below to the original Properties code:

C#
public override ConfigurationPropertyCollection Properties
{
    get
    {
        return s_properties;
    }
}

In the overridden property, we are completely bypassing the base code and returning our prepared properties collection. Our properties collection incurs no reflection costs due to the fact that we explicitly defined each and every ConfigurationProperty, as well as the corresponding validators and type converters. The difference in effort to create a custom configuration class that is explicitly implemented, vs. declaratively implemented, is so minimal that there is little reason not to. Writing configuration classes explicitly also helps during a configuration reload, as configuration is available to the application much quicker. (Note: During a configuration reload, it is possible to trigger a subsequent reload while the first is still in progress. When this happens, other code that relies on that configuration can crash or behave oddly. Shortening the reload time helps minimize this effect, and reduce the need to code special cases to react to it.)

Best Configuration Practices

Making the best use of the .NET 2.0 Configuration framework generally means following the same best practices you would when writing any piece of code, or when using XML for any other purpose. Best practices are best practices, so don't skimp out just because its configuration you're dealing with. Some of the best practices I use when writing configuration for my .NET apps follow.

  • Never use more than you need. If your requirements are extremely simple, use the <appSettings> section. If you need something more type-safe but don't need a complete custom configuration section, try using the project settings feature of VS2005. Don't write a custom configuration section to store a single value, unless there is a strong reason to do so.
  • Don't try to keep it too "simple". A project may start out with simple requirements, but over time, requirements often increase in complexity. Managing and maintaining hundreds or thousands of <appSettings> entries is always going to be a tedious task. Consolidating your configuration into structured configuration sections will allow you to encapsulate, modularize, and simplify your configuration. The time required to put together a set of custom configuration sections will be quickly earned back in reduced configuration maintenance costs.
  • Code for reuse. Try to keep in the back of your mind, "Reuse, reuse, reuse." The same piece of configuration is often needed for many applications. When designing and implementing your configuration sections, try to keep them as generic as possible, and whenever possible to maximize their reuse in the future. Don't code a custom configuration collection if one of the premade ones can be reused. There are several, listed in the first article in this series.
  • Always write performant configuration code. There are a few simple patterns that can be applied to custom configuration sections to maximize performance, as described in the Concerning Performance section. The performance gains from these patterns are significant, and the added complexity is very minimal over not implementing them. Never say "I'll fix performance later", by then it's too late. ;)

Article History

  • 1.0 [12.08.2006] - Original article, complete with all its glorious spelling errors, misnomers, and unintentional omissions. ;)

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

 
GeneralVery interesting and useful article series Pin
Member 40905189-Feb-14 0:59
Member 40905189-Feb-14 0:59 
GeneralMy vote of 5 Pin
woelfle9997-Dec-10 0:29
woelfle9997-Dec-10 0:29 
Generalok article Pin
Donsw23-Mar-10 10:12
Donsw23-Mar-10 10:12 
Generalmap string with configuration Pin
giammin30-Jul-08 5:05
giammin30-Jul-08 5:05 
GeneralLink to part 1 is broken Pin
FauthD18-Mar-08 22:02
FauthD18-Mar-08 22:02 
GeneralOnValidationComplete ?? [modified] Pin
Leblanc Meneses5-Dec-07 5:53
Leblanc Meneses5-Dec-07 5:53 
GeneralGenerate class from config Pin
Mikhail Diatchenko17-Oct-07 19:18
Mikhail Diatchenko17-Oct-07 19:18 
GeneralRe: Generate class from config Pin
Mikhail Diatchenko13-Mar-10 16:35
Mikhail Diatchenko13-Mar-10 16:35 
GeneralControlling serialization of DateTime Pin
MR_SAM_PIPER18-Sep-07 21:43
MR_SAM_PIPER18-Sep-07 21:43 
GeneralRe: Controlling serialization of DateTime Pin
Jon Rista19-Sep-07 20:46
Jon Rista19-Sep-07 20:46 
GeneralRe: Controlling serialization of DateTime Pin
MR_SAM_PIPER20-Sep-07 16:49
MR_SAM_PIPER20-Sep-07 16:49 
QuestionRegarding ConfigurationElementCollection.Properties property Pin
virsum1-Jun-07 5:04
virsum1-Jun-07 5:04 
AnswerRe: Regarding ConfigurationElementCollection.Properties property Pin
Jon Rista1-Jun-07 10:57
Jon Rista1-Jun-07 10:57 
GeneralOutstanding articles Pin
virsum31-May-07 5:56
virsum31-May-07 5:56 
GeneralRe: Outstanding articles Pin
Jon Rista1-Jun-07 10:53
Jon Rista1-Jun-07 10:53 
GeneralFantastic! Pin
originSH22-May-07 22:46
originSH22-May-07 22:46 
GeneralRe: Fantastic! Pin
Jon Rista1-Jun-07 10:52
Jon Rista1-Jun-07 10:52 
GeneralI too jumped down the rabbit hole, didn't like what I found... Pin
Keith Vinson17-Apr-07 11:05
Keith Vinson17-Apr-07 11:05 
GeneralRe: I too jumped down the rabbit hole, didn't like what I found... Pin
Jon Rista10-May-07 7:18
Jon Rista10-May-07 7:18 
GeneralRe: I too jumped down the rabbit hole, didn't like what I found... Pin
Keith Vinson10-May-07 7:34
Keith Vinson10-May-07 7:34 
QuestionErrors? Pin
orfix26-Mar-07 21:51
orfix26-Mar-07 21:51 
AnswerRe: Errors? Pin
Filip Zawada18-Aug-07 10:21
Filip Zawada18-Aug-07 10:21 
GeneralCode sample display is broken Pin
Math Random25-Feb-07 11:16
Math Random25-Feb-07 11:16 
GeneralThanks Jon Pin
kshashank15-Jan-07 22:28
kshashank15-Jan-07 22:28 
GeneralRe: Thanks Jon Pin
Jon Rista19-Jan-07 9:20
Jon Rista19-Jan-07 9:20 

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.