Click here to Skip to main content
15,880,392 members
Articles / Web Development / ASP.NET
Article

Enhanced AppSettings Configuration Handler

Rate me:
Please Sign up or sign in to vote.
4.68/5 (22 votes)
29 Aug 200311 min read 186.2K   1.7K   62   41
Demonstrates how to implement IConfigurationSectionHandler to enhance appSettings for multiple environments.

Introduction

All .NET applications are designed to read configuration from either the app.config (Winform or console apps) or web.config (web apps) file. Typically for the developer this means adding configuration to the appSettings section.

XML
<appSettings>
    <add key="myKey" value="myValue" />
</appsettings>

This can be accessed like:

C#
NameValueCollection nvc = 
     (NameValueCollection)ConfigurationSettings.GetConfig("appSettings");
foreach (string s in nvc.Keys)
{
    Console.Write(s +": " + nvc[s] + "\n");
}

This works just fine for most standalone applications while you only need one set of configuration. However there are instances where you need to handle multiple sets of configuration and have the correct set active for the current environment. For example, when you move to enterprise applications, they typically have dependencies on other applications, services and infrastructure; you start to require different configuration for different environments (e.g. development, system testing, user testing, production). This normally means taking one of the following approaches:

  • maintain different appSetting sections, and comment them in/out as appropriate at deployment time
  • maintain different .config files and apply the applicable one depending on the environment at deployment time
  • maintain your own custom configuration format somewhere else

Each of these has associated pitfalls. The first two require an extra step in the deployment process that can either be forgotten with potentially disastrous results, or needs extra work to add it to your deployment scripts. The third option normally works fine for configuration accessed by your own code, but what about configuration used by components you can't or don't want to change? Take for example a dynamic web service reference. When you instantiate the web service proxy object created by VS.NET, it will look in the appSettings to discover the URL for the service, defaulting to wherever it got the original reference from, if the configuration is missing.

C#
string urlSetting = 
     System.Configuration.ConfigurationSettings.AppSettings["MyService"];
if ((urlSetting != null)) {
    this.Url = string.Concat(urlSetting, "");
}
else {
    this.Url = "http://localhost/myservice/myservice.asmx";
}

While it is possible to hand craft this proxy to change this behavior, any update to the web reference will undo your hard work. Also this is only one example of configuration that is not yours to control where it is read from, there is normally no option to change third party components.

So what we really want is to be able to enhance the appSetting section of the configuration files to allow for different environments, while maintaining compatibility for previously written components to read and if necessary add their own configuration to the section.

Solving the problem

Enter IConfigurationSectionHandler, the interface you need to implement to write your own handlers for sections in the .config files. By implementing our own section handler we obviously have complete control over the structure of the XML within the section and the way in which we process that XML. IConfigurationSectionHandler has only one method we need to implement, public virtual object Create(object parent,object configContext,XmlNode section). Of the arguments Create takes, we are only interested in the last, the XmlNode, this is the entire node from the .config file. It is then up to us to process and return an object, typically a collection containing the configuration. So by implementing our own handler we can customize the format of the appSettings node to solve our problem. At this point it is perhaps important to decide and state the requirements for our new handler.

  • backward compatible with the normal appSettings from the point of view of any code accessing the configuration
  • backward compatible with the normal appSettings from the point of view of any code trying to append items in the design environment e.g. dynamic web service references
  • ability to define named sets of configuration
  • ability to map configuration sets to a hostname (or other unique feature of the different environments)
  • specify and defined, predictable order of processing to allow add, remove and clear elements to work as normal
  • sets of configuration to recursively include other sets to minimize duplication in the configuration
  • allow components we code to read configuration as normal, allowing them to work in either a normal appSettings environment or our new enhanced one transparently.

These give us 3 constraints:

  • we must return a ReadOnlyNameValueCollection
  • we must be able to have add, remove, clear elements directly inside the appSettings element to maintain compatibility with components that add settings at design time - these will be applied to all environments unless we move them
  • we must use appSettings as our root element

At this stage it is probably useful if you are not already familiar with the standard appSetting handler NameValueFileSectionHandler to read the MSDN documentation for the different available sub-elements of the appSettings element.

Why a ReadOnlyNameValueCollection?

Looking at the documentation for the NameValueFileSectionHandler it appears that it returns a NameValueCollection. However, the first version of this project I coded, returned a NameValueCollection and was found to be incompatible with the ConfigurationSetting.AppSettings property. Initially discovered by another user of this site, dkallen, the property was throwing invalid cast exceptions. To work out why this was, I disassembled System.dll back to IL (using ildasm) to look at the code behind the property. What I found was that, it was returning a ReadOnlyNameValueCollection a type that inherits from NameValueCollection. Looking into this new type, I discovered it was private (internal in c#) to the system.dll assembly. This presented a new problem - how do I return a type that I cannot create? The answer in this case is actually fairly simple - get the original NameValueFileSectionHandler to do it for me. Whereas in the original version of this project we built the NameValueCollection directly. In the new version we build up our own appSettings XmlNode containing only the child nodes we want (with the exception of clear nodes which we can obviously process ourselves). We then pass this XmlNode to an instance of the original handler getting back a ReadOnlyNameValueCollection that we return directly.

The new format

So now we know what we are trying to achieve, we can define a new format for the appSetting section. The easiest way to show this is with an example and an XSD representation. The example given obviously looks far more complicated than a standard appSettings block but is actually relatively simple and we will go on to describe exactly how it is processed.

Example

XML
<appSettings>
    <configMap hostname="machine1">
        <include set="set2"/>
        <include set="set3"/>
    </configMap>

    <configMap hostname="machine2">
        <include set="set2"/>
        <include set="set3"/>
    </configMap>

    <add key="key5" value="value5"/>
    <add key="key6" value="value6"/>

    <configSet name="set1">
        <add key="key1" value="value1"/>
        <add key="key2" value="value2"/>
    </configSet>

    <configSet name="set2">
        <add key="key3" value="value3"/>
        <include set="set1" />
    </configSet>

    <configSet name="set3">
        <include set="set1" />
        <remove key="key1" />
        <add key="key4" value="value4" />
    </configSet>
</appSettings>

Schema

This schema is not used in the code anywhere, it is just an easy way to define the allowed possibilities for the configuration structure. The XSD file is included in the source and can be loaded into tools such as XMLSpy for easy reading.

XML
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" 
elementFormDefault="qualified" attributeFormDefault="unqualified">
    <xs:element name="appSettings">
        <xs:complexType>
            <xs:choice>
                <xs:element ref="configSet"/>
                <xs:element ref="configMap"/>
                <xs:element ref="add"/>
                <xs:element ref="remove"/>
                <xs:element ref="clear"/>
            </xs:choice>
        </xs:complexType>
    </xs:element>
    <xs:element name="configSet">
        <xs:complexType>
            <xs:choice minOccurs="0" maxOccurs="unbounded">
                <xs:element ref="configSet"/>
                <xs:element ref="configMap"/>
                <xs:element ref="include"/>
                <xs:element ref="add"/>
                <xs:element ref="remove"/>
                <xs:element ref="clear"/>
            </xs:choice>
            <xs:attribute name="name" type="xs:string" use="required"/>
        </xs:complexType>
    </xs:element>
    <xs:element name="configMap">
        <xs:complexType>
            <xs:sequence>
              <xs:element ref="include" minOccurs="0" maxOccurs="unbounded"/>
            </xs:sequence>
            <xs:attribute name="hostname" type="xs:string" use="required"/>
        </xs:complexType>
    </xs:element>
    <xs:element name="include">
        <xs:complexType>
            <xs:attribute name="set" type="xs:string" use="required"/>
        </xs:complexType>
    </xs:element>
    <xs:element name="add">
        <xs:complexType>
            <xs:attribute name="key" type="xs:string" use="required"/>
            <xs:attribute name="value" type="xs:string" use="required"/>
        </xs:complexType>
    </xs:element>
    <xs:element name="remove">
        <xs:complexType>
            <xs:attribute name="key" type="xs:string" use="required"/>
        </xs:complexType>
    </xs:element>
    <xs:element name="clear">
        </xs:element>
</xs:schema>

The XML is recursive with the appSettings block really being an unnamed root: configSet. When a configSet / appSettings node is processed each of the child nodes are processed in the order they appear, with the exception of configSet nodes that are not processed until they are referenced. There are 6 types of nodes that can appear within a configSet, 5 of which can appear directly in the root appSettings node.

  • configSet
  • configMap
  • include (these cannot appear at the appSetting level - they don't make sense there)
  • add
  • remove
  • clear

configSet / appSettings Node

A configSet node is processed by processing each of the child nodes, with the exception of child configSet nodes, in the order they appear as described in each of the sections below.

configMap Node

A configMap node determines which configuration is used. In this implementation it uses the hostname to map one or more configSet nodes. When a configMap is processed we check to see if the specified hostname matches the System.Environment.MachineName. If it does then we process it's children. Processing the child nodes of a configMap involves processing each of the child include nodes in the order they appear. A file may contain more than one configMap for a specific hostname if the order of processing requires it. It is possible to change the behavior of these nodes by overriding the CheckConfigMap function.

include Node

An include node is an instruction to process a specified configSet. The configSet is specified by name using the set attribute of the include node. The configSet to be processed must have the same parent as the parent of the include node.

add Node

An add node works in the same way it does for the default appSettings handler. It adds an item to the collection we are building based on the specified key and value, overwriting any value already contained that has the same key.

remove Node

A remove node works in the same way it does for the default appSettings handler. It removes the item from the collection with the specified key.

clear Node

A clear node works in the same it does for the default appSettings handler. It causes the collection to be emptied.

Example process walk

At this stage it is probably helpful to walk through the example appSettings section above, to see how it is processed. We will walk through an example of how it is processed when running on machine1

Processing each of the children of the appSettings node in order

  • we process the first configMap that indicates it is linked to our host name
    • we now process each of the child include nodes causing us to find and process set2
      • process the add node, appending it to own new node
      • process the include node causes us to process set1
        • process the add node to add item key1/value1, appending it to own new node
        • process the add node to add item key2/value2, appending it to own new node
    • and then set3
      • processing the include causes us to process set1 (again)
        • process the add node to add item key1/value1, appending it to own new node
        • process the add node to add item key2/value2, appending it to own new node
      • process the remove node to remove the item key1, appending it to own new node
      • process the add node to add item key4/value4, appending it to own new node
  • we ignore the second machine map that is for machine2
  • process the add node to add item key5/value5, appending it to own new node
  • process the add node to add item key6/value6, appending it to own new node

What we end up with is, a node containing the add /remove nodes we want. Once this is processed by the NameValueFileSectionHandler we get a collection containing key2/item2, key3/item3, key4/item4, key5/item5, key6/item6

Plugging it in

So now we have our EnhancedAppSettingsHandler, how do we get .NET to use it? This requires a bit of XML at the top of the app.config / web.config

XML
<configsections>
    <remove name="appSettings"/>
    <section name="appSettings" 
        type="Haley.EnhanceAppSettings.EnhancedAppSettingsHandler,
        EnhanceAppSettings"/>
</configsections>

This performs 2 actions, it first removes the default handler that is specified in the machine.config and then adds in our own. The section element has 2 attributes, the name which is the tag name of the elements to process using this handler, and the type which defines the class and assembly for the new handler.

Points of interest

The key to this project is how simple it is to implement your own section handlers to the .config files and how doing so aligns your application's and components better with the .NET configuration architecture and reduces the need to deploy extra configuration files.

Update V2.0

(I have now integrated the changes for V2 into the main article, but have left this here for anyone who wants to know what changed)

Thanks to dkallen for pointing out that use of ConfigurationSettings.AppSettings was failing. I have fixed this in this latest release. This is an important part of maintaining backward compatibility, as failure to do so may break not only your own code but also third party components you use. The problem was that the default handler NameValueFileSectionHandler does not return a NameValueCollection as we are lead to believe by the type returned to us from ConfigurationSettings.AppSettings but rather it returns a ReadOnlyNameValueCollection. This is an internal type to System.dll that inherits from NameValueCollection.

The solution then was that, instead of our code processing the XmlNode and building up a NameValueCollection we instead build up a new XmlNode of appSettings containing only the settings we want to include for this instance of the application, in the standard format. We then create an instance of the original handler, NameValueFileSectionHandler and pass in our new XmlNode, it then passes back the correct ReadOnlyNameValueCollection object that we can then return. This done, all is well and ConfigurationSettings.AppSettings works again.

Update V3.0

I have been asked how to extend this to use a different mechanism for deciding which environment we are currently in and hence which configMap nodes we need to process. So I thought I add a few details here. The first thing I have done is to move the decision out of the ProcessConfigMap function into a dedicated function CheckConfigMap. This new function takes an XmlNode for the configMap as a parameter and returns a bool. I have marked this function as protected virtual so that new section handlers can be written that inherit from this type and override the decision making process.

I have also renamed the old machineMap nodes to be configMap nodes, it seemed to make more sense.

If for example we want to change the process so that we look at the AppDomain.BaseDirectory to decide whether or not to process a configMap element, we need to do the following:

  • add an attribute to the configMap elements called BaseDir
  • create a new class that inherits from EnhancedAppSettingsHandler
  • override CheckConfigMap to compare the value of the new attribute to AppDomain.BaseDirectory and return true or false
  • use this new type in the top of our app.config
    XML
    <configsections>
        <remove name="appSettings"/>
            <section name="appSettings" 
            type="<new type>,<new assembly>"/>
    </configsections>

History

  • 17 August 2003 - Version 1.0
  • 22 August 2003 - Version 2.0 - Now properly backward compatible
  • 25 August 2003 - Version 3.0 - How to extend to determine current environment from something other than hostname.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here


Written By
Web Developer
United Kingdom United Kingdom
Paul has a background in VB / C++ COM development and has now converted to dotNET, coding in C# for the financial markets industry for the last 3 years.

Comments and Discussions

 
GeneralRe: Is it really backward compatible? It is now Pin
dkallen23-Aug-03 3:39
dkallen23-Aug-03 3:39 
GeneralAppsettings file= Pin
bspiglejr19-Aug-03 13:07
bspiglejr19-Aug-03 13:07 
GeneralRe: Appsettings file= Pin
Paul Haley (Unused)20-Aug-03 3:22
Paul Haley (Unused)20-Aug-03 3:22 
GeneralRe: Appsettings file= Pin
bspiglejr20-Aug-03 3:59
bspiglejr20-Aug-03 3:59 
Generalmore specific than the machine name Pin
Ted_b19-Aug-03 9:50
Ted_b19-Aug-03 9:50 
GeneralRe: more specific than the machine name Pin
MikeGale19-Aug-03 11:27
MikeGale19-Aug-03 11:27 
GeneralRe: more specific than the machine name Pin
Paul Haley (Unused)19-Aug-03 11:46
Paul Haley (Unused)19-Aug-03 11:46 
GeneralRe: more specific than the machine name Pin
Paul Haley (Unused)19-Aug-03 12:04
Paul Haley (Unused)19-Aug-03 12:04 
Ted,

Sorry bit of a branstorm in the last response - there will of course be no Request in scope at the time the config gets loaded.

You should however be able to use HttpContext.Current.Server.MapPath(".") to give you something of the form c:\inetpub\wwwroot\myapp to compare to.
Another alternative would be AppDomain.CurrentDomain.BaseDirectory.
It probably needs a bit of testing to work out which path works best for you.

Paul
GeneralRe: more specific than the machine name Pin
dkallen25-Aug-03 10:32
dkallen25-Aug-03 10:32 
GeneralRe: more specific than the machine name Pin
Ted_b26-Aug-03 9:01
Ted_b26-Aug-03 9:01 

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.