The registry is dead! Long live the registry! In the shiny new world of .NET-based applications with xcopy deployment and smart clients,
we can no longer use the registry to hold application configuration information.
Instead, we're supposed to use XML-based configuration files accessed via the
CLR's pluggable configuration framework, System.Configuration, which lives
in the System assembly. This article will introduce you to the configuration
system and show you how to extend it using custom configuration section handlers.
To start, let's take a look at how the configuration system works and for what it was
intended. The configuration system works by reading in settings from specially-named XML files.
For console and WinForms applications, the file has the same name as the executable, plus a
.config extension. For example, if you had an application named MyApp.exe,
the config file would be named MyApp.exe.config. For ASP.NET applications,
the config file is always named web.config. Either way, configuration files
are meant to be read-only, and you'll find nothing in the configuration framework that
helps you write to configuration files.
Configuration files are meant for application-level
settings that rarely change after installation, not user-level settings like window
placement or favorite color. Per-user settings
should be stored elsewhere, preferably in an application-specific folder under
the user's Application Data folder, inside of Isolated Storage, or even inside of the
dreaded registry (under HKEY_CURRENT_USER/Software/YourCompany/). That said,
if we did need to modify one, configuration files are just XML files.
We can use the standard facilities present in the System.Xml assembly
to do whatever it is we need to do. Be warned, this may not always work out. For applications
deployed using HTTP (like Chris Sells' Wahoo app), we would not have any way to write out the
configuration file. Also, if we do write settings back to our config file from inside of an
ASP.NET application, the ASP.NET application will be restarted.
Moving right along, let's take a look at how a configuration file is structured.
Configuration files are broken up into two main parts. The first part, contained within a
<configSections> element, is really metadata that the framework
uses to determine how to parse the remainder of the file.
Let's take a look at a sample layout:
<configuration>
<configSections>
<sectionGroup name="blowery.org">
<section name="basics"
type="BasicConfigSample.SectionHandler, BasicConfigSample"/>
</sectionGroup>
</configSections>
<blowery.org>
<basics>
<firstName>Jack</firstName>
<lastName>Hoya</lastName>
</basics>
</blowery.org>
</configuration>
Here we have one <sectionGroup> which contains one
<section>. The <section> has a
name and a type. The name specifies
the name of the XML element containing the configuration section;
the type specifies the class that will be
used to parse the configuration section. The <sectionGroup>
can group different sections together under a common parent element.
In the sample above, we're stating that we have a base element,
<blowery.org>, and within that base we have a section, <basics>.
When the framework parses the <basics> section, it should use the
BasicConfigSample.SectionHandler class from the BasicConfigSample
assembly.
To kick off the parser and access the settings for a configuration section, we call
ConfigurationSettings.GetConfig("sectionName"). For the example above,
the call would be ConfigurationSettings.GetConfig("blowery.org/basics").
Notice that we have to specify the path down to the section when we ask for the settings.
Given this little snippet of configuration file, let's go over what's happening when
we call ConfigurationSettings.GetConfig("blowery.org/basics").
First, the configuration system parses the <configSections>
looking for <section> tags. For each <section> found,
the configuration system creates an instance of the type specified in the
type attribute and associates the configuration path with the instance.
The type must implement the IConfigurationSectionHandler interface.
In this case, the framework instantiates a BasicConfigSample.SectionHandler,
casts it to a IConfigurationSectionHandler, and
associates the instance with the path blowery.org/basics. When we call
GetConfig(), Create() is called and the framework passes
on the return value to our calling program.
If any nodes are found that don't match an IConfigurationSectionHandler
instance, a ConfigurationException is thrown, and processing halts.
We could catch this exception, but we generally wouldn't want to. The error may or may not
be due to a misconfiguration of your portion of the configuration file, so catching it and
eating it wouldn't really be safe. If we do catch it, we should at least rethrow it.
Now we need to create a simple object that implements IConfigurationSectionHandler
and use it from our code. First, here's the definition of the interface:
public interface IConfigurationSectionHandler {
object Create(object parent,
object configContext,
XmlNode section);
}
To implement the interface, all we need is a Create() method that takes
three parameters. That seems pretty easy. Before we implement it, let's go over the
parameters and the return value.
The first parameter, parent, is typed as System.Object.
The parent is used when we need to support chains of configuration files,
which we're not going to do until later. For now, we'll ignore this one. The second
parameter, configContext, is currently only used when the
IConfigurationSectionHandler is being used by an ASP.NET application. When
the handler is called by ASP.NET, this instance will be an HttpConfigurationContext.
Again, we're going to ignore this for the time being. The last parameter, section,
is an XmlNode that represents the configuration section. In our case,
the XmlNode will point to the <basics> element. Finally, when
we're done, we need to return an object that represents our configuration settings. We should
define a type for our settings (we'll use BasicSettings) and document that our
implementation returns that type. Here's our implementation:
using System;
using System.Configuration;
using System.Xml;
namespace BasicConfigSample
{
public class SectionHandler : IConfigurationSectionHandler
{
/// <summary>Returns a BasicSettings instance</summary>
public object Create(object parent,
object context,
XmlNode section) {
string f = section["firstName"].InnerText;
string l = section["lastName"].InnerText;
return new BasicSettings(f,l);
}
}
public class BasicSettings
{
internal BasicSettings(string first, string last) {
FirstName = first;
LastName = last;
}
public readonly string FirstName;
public readonly string LastName;
public override string ToString() {
return FirstName + " " + LastName;
}
}
}
As stated before, to use the section handler, we need to ask the
ConfigurationSettings object for the proper config section:
using System;
using System.Configuration;
namespace BasicConfigSample
{
class EntryPoint
{
const string mySection = "blowery.org/basics";
[STAThread]
static void Main(string[] args) {
BasicSettings settings;
settings = (BasicSettings)ConfigurationSettings.GetConfig(mySection);
Console.WriteLine("The configured name is {0}", settings);
}
}
}
And there we have it, our very own custom section handler, an equal citizen with all of the other section handlers supplied by the framework.
So far, we've only talked about the case where everything exists and is configured properly. What happens if the configuration settings are not present in the config file? To illustrate, what happens if we have a config file that looks like this?
<configuration>
<configSections>
<sectionGroup name="blowery.org">
<section name="basics"
type="BasicConfigSample.SectionHandler, BasicConfigSample"/>
</sectionGroup>
</configSections>
<!-- missing section!! -->
</configuration>
|
If the configuration system cannot find a node that matches the path we asked for,
it does not call Create() on the section handler and
ConfigurationSettings.GetConfig() simply returns null.
Returning null is a bit of a pain. Any place we call GetConfig(),
we'll have to check the return value for null and do the right thing, loading defaults if necessary.
That's rather error-prone, but we can wrap this up to make it easier to use.
A factory method on the BasicSettings class that checked for null and
loaded a default, if necessary, would do the trick. We'll move the code that
grabs the settings object from the EntryPoint to our new factory method and rewrite the
EntryPoint to use the new factory method:
using System;
using System.Configuration;
using System.Xml.Serialization;
using cs = System.Configuration.ConfigurationSettings;
namespace BasicConfigSample
{
public class BasicSettings
{
/* same as before */
private BasicSettings() {
FirstName = "<<not";
LastName = "set>>";
}
const string section = "blowery.org/basics";
public static BasicSettings GetSettings() {
BasicSettings b = (BasicSettings)cs.GetConfig(section);
if(b == null)
return new BasicSettings();
else
return b;
}
}
class EntryPoint
{
[STAThread]
static void Main(string[] args) {
BasicSettings settings = BasicSettings.GetSettings();
Console.WriteLine("The configured name is {0}", settings);
}
}
/* SectionHandler stays the same */
}
The astute reader might notice that the default instance looks like a prime
candidate for becoming a Singleton. Luckily, the configuration framework
already caches the result of the call to IConfigurationSectionHandler.Create(),
so it's one less piece we have to implement.
So far, we've covered how to implement a very simple section handler and
how to wrap up the calls to GetConfig() to get around the
null return problem. Next, we're going to dive into configuration
parenting and discuss how it affects our custom section handler.
Remember how an element in the configuration file
that has no matching <section> tag causes the
configuration system to thrown a ConfigurationException? If
you've played around with web.config files, you may be wondering how the
<system.web> section works; no
<configSections> or <section> elements
are present, so how does the configuration system know which
IConfigurationSectionHandler to use? The answer lies in
configuration file parenting.
When the configuration system parses our configuration file, it also
parses a master configuration file, stored in a file called machine.config, which
lives in the Config folder of your framework install directory. Open the file up;
contained within is a long list of <sectionGroup> and
<section> tags at the top of the file.
When the configuration system can't find a section handler in your configuration file, it
walks up to machine.config and checks there. If you decide to register your
section handler in machine.config, you should seriously consider strongly naming
the assembly and registering it with the GAC. That way, anyone who looks in machine.config
can use your configuration handler. Strictly speaking, you don't have to register your assembly
in the GAC, but it's a good idea.
The machine.config file can also hold machine-wide default settings.
If you search machine.config for <system.web>, you'll
find all of the defaults used by ASP.NET. Changes to this file would affect all of the ASP.NET applications
running on that machine.
So what does all this mean to the lowly developer implementing IConfigurationSectionHandler?
Simply, it means that we may have to parse and merge settings from different config files.
In fact, for ASP.NET applications, Create() can be called many times, once for each
directory above the ASP.NET page in question that defines a web.config file, plus possibly once more for
machine.config. For example, if we defined our configuration section handler in
machine.config and had the IIS layout shown below, our configuration handler would be called
four times.
![]() |
| Multiple web.config file diagram |
A couple interesting things about the ASP.NET implementation:
First, if a web.config in the hierarchy doesn't contain settings, it will be
skipped, and the next config file in the hierarchy will be checked. Second, there's a discrepancy between the
ASP.NET configuration system and the DefaultConfigurationSystem used by console and WinForms applications. If a section is redefined in a child configuration file, ASP.NET deals with it and doesn't throw an error. However, a console or WinForms app will throw a ConfigurationException, stating that the section in question has already been defined. I rather like the ASP.NET approach; it supports xcopy deployment (I don't have to know
if the section handler is already registered) and just does what I would expect. At the very least,
it would be nice if the framework teams resolved this difference before v1.1 gets released.
Anyway, back to how parenting affects implementing the interface.
When the configuration system finds a configuration element in machine.config and in
your local config file, it first calls Create() using the XmlNode in
machine.config, then calls Create() using the XmlNode in
our config file. When it calls Create() for our local file, it passes in the
object returned from the call to Create() on machine.config's XmlNode.
We are expected to do the right thing when it comes to merging the current node with the parent settings.
The chaining always starts with machine.config and walks down the directory tree.
Our little section handler from before isn't well-suited for interesting override behavior, so
let's write a new one. This one will sum the value attribute of a <sum>
element. Also, instead of looking for blowery.org/code,
we'll look for blowery.org/sum.
using System;
using System.Configuration;
using System.Xml.Serialization;
using cs = System.Configuration.ConfigurationSettings;
namespace ParentingSample
{
public class Settings
{
const string section = "blowery.org/sum";
private int sum;
internal Settings(int start) {
sum = start;
}
private Settings() {
sum = 0;
}
public int Total {
get { return sum; }
}
internal int Add(int a) {
return sum += a;
}
public override string ToString() {
return Total.ToString();
}
public static Settings GetSettings() {
Settings b = (Settings)cs.GetConfig(section);
if(b == null)
return new Settings();
else
return b;
}
}
class SectionHandler : IConfigurationSectionHandler
{
public object Create(object parent,
object context,
XmlNode section)
{
int num = int.Parse(section.Attributes["value"].Value);
if(parent == null)
return new Settings(num);
Settings b = (Settings)parent;
b.Add(num);
return b;
}
}
}
Notice the new code in the SectionHandler. If parent is not null, we cast it to
a BasicSettings and call Add() with the parsed value. Here, we handle merging
the current node with the parent settings. Otherwise, we start the chain by
creating a new BasicSettings initialized with the first number.
To test this code, we'll need the setting in two config files. In machine.config, we'll register the section handler and base setting like this:
<configuration>
<configSections>
<sectionGroup name="blowery.org>
<section
name="sum"
type=""ParentingSample.SectionHandler, ParentingSample"/>
</sectionGoup>
<!-- other sections -->
</configSections>
<blowery.org>
<sum value="10"/>
</blowery.org>
<!-- other settings -->
</configuration>
In our local application config file, we'll set up another value like this:
<configuration>
<!-- section already registered in machine.config -->
<blowery.org>
<sum value="5"/>
</blowery.org>
</configuration>
Now, if we ran the program, we should see a result of 15. Pretty neat. This example is pretty simple, but it does show you the basics of how to grab the parent settings and merge them with the local settings. The most difficult thing here was deciding how our settings should merge with their parents.
We've covered quite a bit about implementing a
custom configuration section handler. There are some other techniques that come in handy when working with configuration
files that we have not outlined here. For example, the System.Xml.Serialization namespace in the
System.Xml assembly can radically simplify the parsing code for a configuration section.
Also, take a good look at machine.config for examples of how to structure your configuration to support
parenting and overrides in a flexible, robust manner. ASP.NET does a wonderful job of this and a lot can be learned by
studying how it handles parenting and overrides between machine.config and a web.config file.
Thanks for reading, and I hope you learned a lot from the article. If you'd like to download the accompanying
sample code for this article, you can grab the .zip file from
www.blowery.org/code/ConfigurationSamples.zip.
If you have questions, feel free to contact me via email.
Ben Lowery is a developer at FactSet Research Systems, where he works on all things great and small.
Return to ONDotnet.com
Copyright © 2009 O'Reilly Media, Inc.