Unraveling the Mysteries of .NET 2.0 Configuration

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 loosing the other advanced features 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, and 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 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 this custom configuration madness, and then 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 (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 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)

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 the 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 the 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 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:

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 -> <error statusCode="404" redirect="...">
  HttpModulesSection -> <httpModules>
    HttpModuleActionCollection -> [implicitly created]
      HttpModuleAction -> <add name="myModule" type="...">
  HttpHandlersSection -> <httpHandlers>
    HttpHandlerActionCollection -> [implicitly created]
      HttpModuleActionCollection -> <add verb="*" type="..." path="...">

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 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, this nicely packaged, strongly-typed, secure object model wrapped around your configuration is directly accessible anywhere in your application, without the need 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, and hopefully 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 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:

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

namespace Examples.Configuration
{
/// <summary>
/// An example configuration section class.
/// </summary>
publicclass 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 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:

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

namespace Examples.Configuration
{
/// <summary>
/// An example configuration section class.
/// </summary>
publicclass 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
privatestatic ConfigurationProperty s_propString;
privatestatic ConfigurationProperty s_propBool;
privatestatic ConfigurationProperty s_propTimeSpan;

privatestatic ConfigurationPropertyCollection s_properties;
#endregion


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

/// <summary>
/// Gets the BooleanValue setting.
/// </summary>
[ConfigurationProperty("boolValue")]
publicbool 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>
publicoverride 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 (which, by the way, is the purely declarative method):

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

/// <summary>
/// Gets the BooleanValue setting.
/// </summary>
[ConfigurationProperty("boolValue")]
publicbool 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 of custom configuration elements, 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, and add the necessary XML so 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):

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

privatestring m_string;
privatebool 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, possibly through the use of another configuration section, is written to accommodate such a thing.

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

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

namespace Examples.Configuration
{
/// <summary>
/// An example configuration element class.
/// </summary>
publicclass 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),
false,
ConfigurationPropertyOptions.IsRequired
);

s_properties = new ConfigurationPropertyCollection();

s_properties.Add(s_propDateTime);
s_properties.Add(s_propInteger);
}
#endregion

#region Static Fields
privatestatic ConfigurationProperty s_propDateTime;
privatestatic ConfigurationProperty s_propInteger;

privatestatic 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")]
publicint IntegerValue
{
get { return (int)base[s_propInteger]; }
}

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

To add 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:

Collapse
publicclass 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
privatestatic 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:

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

privatestring m_string;
privatebool m_bool;
private TimeSpan m_timespan;
private DateTime m_datetime;
privateint 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, reasons for which we will discuss 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 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:

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

public ExampleThingElementCollection()
{
}
#endregion

#region Fields
privatestatic ConfigurationPropertyCollection m_properties;
#endregion

#region Properties
publicoverride ConfigurationPropertyCollection Properties
{
get { return m_properties; }
}

publicoverride 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 { returnbase.BaseGet(name); }
}
#endregion

#region Methods
publicvoid Add(ThingElement thing)
{
base.BaseAdd(thing);
}

publicvoid Remove(string name)
{
base.BaseRemove(name);
}

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

publicvoid Clear()
{
base.BaseClear();
}

publicvoid RemoveAt(int index)
{
base.BaseRemoveAt(index);
}

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

#region Overrides
protectedoverride ConfigurationElement CreateNewElement()
{
returnnew ThingElement();
}

protectedoverrideobject GetElementKey(ConfigurationElement element)
{
return (element as ThingElement).Name;
}
#endregion
}

Some common themes to the pattern implemented above include writing the methods Add, Remove (by a key value), RemoveAt, and Clear. Two additional methods, Remove (by an item instance), and GetKey, are common, but not always necessary, depending on your needs. It is also normal to provide both an indexer accessible by numeric index, as well as an indexer accessible by an element's key (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 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, 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.

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

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

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

s_properties = new ConfigurationPropertyCollection();

s_properties.Add(s_propName);
s_properties.Add(s_propType);
}
#endregion

#region Static Fields
privatestatic ConfigurationProperty s_propName;
privatestatic ConfigurationProperty s_propType;
privatestatic ConfigurationProperty s_propColor;

privatestatic ConfigurationPropertyCollection s_properties;
#endregion


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

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

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

/// <summary>
/// Override the Properties collection and return our custom one.
/// </summary>
publicoverride 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 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:

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

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

#region Fields
// ...
#endregion

#region Properties
// ...

publicoverride ConfigurationElementCollectionType CollectionType
{
get { return ConfigurationElementCollectionType.BasicMap; }
}

publicoverridestring 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:

<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.config's first, followed by the parent web.config's, ending with the root web.config's 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 to 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:

publicsealedclass 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:

Collapse
<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, 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:

privatestring m_string;
privatebool m_bool;
private TimeSpan m_timespan;
private DateTime m_datetime;
privateint 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 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:

Collapse
        #region Properties
/// <summary>
/// Gets the StringValue setting.
/// </summary>
[ConfigurationProperty("stringValue", IsRequired=true)]
publicstring 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")]
publicbool 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>
publicoverride 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. Interestingly enough, the collection classes we wrote in previous sections are already updatable, thanks to their Add and Remove methods, and the indexers setter. Making a collection read-only simply requires you to not implement any methods that allow the collection to be modified.

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:

Collapse
publicclass SomeConfigurationSection
{
static SomeConfigurationSection()
{
// Preparation...
}

// Properties...

#region GetSection Pattern
privatestatic SomeConfigurationSection m_section;

/// <summary>
/// Gets the configuration section using the default element name.
/// </summary>
publicstatic SomeConfigurationSection GetSection()
{
return GetSection("someConfiguration");
}

/// <summary>
/// Gets the configuration section using the specified element name.
/// </summary>
publicstatic SomeConfigurationSection GetSection(string definedName)
{
if (m_section == null)
{
m_section = ConfigurationManager.GetSection(definedName) as
SomeConfigurationSection;
if (m_section == null)
thrownew 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:

Collapse
using System.Web;
using System.Web.Configuration;

publicclass SomeConfigurationSection
{
static SomeConfigurationSection()
{
// Preparation...
}

// Properties...

#region GetSection Pattern
privatestatic 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>
publicstatic 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>
publicstatic 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)
thrownew 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:

Collapse
using System.Web;
using System.Web.Configuration;

publicclass SomeConfigurationSection
{
static SomeConfigurationSection()
{
// Preparation...
}

// Properties...

#region GetSection Pattern
// Dictionary to store cached instances of the configuration object
privatestatic Dictionary<string,
SomeConfigurationSection> m_sections;

/// <summary>
/// Finds a cached section with the specified defined name.
/// </summary>
privatestatic SomeConfigurationSection
FindCachedSection(string definedName)
{
if (m_sections == null)
{
m_sections = new Dictionary<string,
SomeConfigurationSection>();
returnnull;
}

SomeConfigurationSection section;
if (m_sections.TryGetValue(definedName, out section))
{
return section;
}

returnnull;
}

/// <summary>
/// Adds the specified section to the cache under the defined name.
/// </summary>
privatestaticvoid 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>
publicstaticvoid 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>
publicstatic 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>
publicstatic 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)
thrownew 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>
publicstatic 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>
publicstatic SomeConfigurationSection GetSection(Configuration config,
string definedName)
{
if (config == null)
thrownew 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)
thrownew 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:

<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 the ConfigurationManager (or WebConfigurationManager). Calling GetSection() on a configuration manager class will perform full caching on the section, where as 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, and 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 (more on this in the advanced section below), caching configuration settings in variables is generally discouraged. Consider the following very simple scenario:

Collapse
publicclass 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();
}

privatestatic MyConfig s_config;
privatestatic Timer s_timer;
privatestatic TimeSpan s_duration;

privatestaticvoid 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:

publicclass SomeProgram
{
static Main()
{
s_config = MyConfig.GetSection();

s_timer = new Timer(
new TimerCallback(),
null,
TimeSpan.Zero
s_config.Duration
);

Console.ReadKey();
s_timer.Dispose();
}

privatestatic MyConfig s_config;
privatestatic Timer s_timer;
privatestatic TimeSpan s_duration;

privatestaticvoid 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, and 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, which means 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 added to this article in the future, outlining how to write faster, safer, more strict configuration sections.

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
Collapse
<!-- \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 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 SessionInformation 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.

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

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

In the configuration file above, the <connectionStrings> section has been sourced from a file called ../../connectionStrings.config. All of the applications 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 ../../ part of the connectionStrings.config path). The beauty here is that we can define connection strings properly for each environment once, and 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, were 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.

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, as well as 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, and this will be discussed in a future installment of advanced configuration topics (after several prerequisite installments ;)).

原文地址:https://www.cnblogs.com/amylis_chen/p/3111756.html