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.
<appSettings>
<add key="myKey" value="myValue" />
</appsettings>
This can be accessed like:
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.
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
<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.
<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
<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:
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.