The Complete Guide to Custom Configuration Sections

For the past several years, I’ve worked at a company where we write a support a developer framework. As part of that work, I’ve come to understand some of the challenges of writing a component, as opposed to writing an application.

One main thing that is different, is you have to make your component easy to use. You do that by using XML code comments and publishing component document. You also use those XML code comments to give robust and complete Intellisense. But, what about config files?

If you are writing a framework, at some point <appSettings> simply won’t be enough. Even aside from that, it’s not really a great idea to even use <appSettings> for your framework, because now you are messing up a section specifically intended for the end-developer.

However, you will likely not want to hard-code values in your code – and you’d instead want to get values from a config file – so how do we do that? The answer of course is to create your own configuration sections – just like <system.web> or <system.diagnostic> But how do you do that?

This article will go into excruciating detail on just that topic. We will cover all the typical ways you would structure your configuration, and even talk about how to easily give your developers Intellisense inside Visual Studio when they go use your configuration.

I’ve had to learn, and then re-learn this two full times now, so for my own benefit, I’m including a lot of detail. If you (assuming YOU are not me, Future Robert), ran across this page, this is absolutely everything you need, including sample code, to create complete configuration for your component.

Get Your Framework Project Ready:
I have a complete, working sample application (see attachment at the end). I’ve created a fictitious “SederSoftware.Components” library that I want developers to use. This component requires some fictitious configuration.

When you have a section in your configuration file like <system.web>, that isn’t just some random XML, there is a data structure, a class, that matches up to every one of those elements and attributes that correspond to properties of that class.

So, we need to start by creating data structures for each chunk of data that we want to store. I have found it’s best to start by brainstorming what you want the developer experience to be. If you write a developer framework, you already know you MUST put forethought into every design decision you make. For example, you might not think twice and just decide to put something like this in your configuration:

<contoso.framework>
<activeDirectory userName="" password="" domain=""/>
</contoso.framework>

the problem is what about when you have a user that needs to connect to more than one domain? Uh-oh. So instead, you could use an AddRemoveClearMap for example that supports <add …/>, <remove name=””/> and <clear /> – which is sort of a config-file-way of storing a proper collection:

<contoso.framework>
<provider>
<add name="domainName1" userName="" password=""/>
<add name="domainName2" userName="" password=""/>
</provider>
</contoso.framework>

All my point is here is spend the time to figure out what you want the user-experience to be. If you have to change your config in a future release, it’s a pain for everyone. Developers are going to be confused and annoyed, you are going to get more calls, and have to support 2 different documentation streams – one from “the old way we did it” and “the new way we do it”. So, take the time and get the structure right, if you can.

As a matter of convention, files related to having custom configuration, should go in a “Configuration” folder and matching namespace. In the example above, the convention would have me assume that this is configuration for Contoso.Framework.dll, and that the underlying configuration classes are probably in the Contoso.Framework.Configuration namespace.

IMPORTANT: You need to add a reference to System.Configuration in order to find the classes that are going to talk about next. This concept and the reference to System.Configuration has been the same since at least 2.0 and still works in 4.0 – so just add whatever the current version is that your project is using.

Different Kinds of Sections/Collections:
Let’s say that you actually have your configuration. In the sample project attached, I came up with this example for a specific reason:

<sederSoftware.components>
<generalSettings enabled="false"/>
<contextSettings>
<settings>
<clear />
<add name="test1" enabled="true" partOfDay="Morning"/>
<add name="test2" enabled="false" partOfDay="Evening"/>
</settings>
</contextSettings>
</sederSoftware.components>

This contains 3 of the most common types of sections, which should cover a vast majority of needs. These are:

  • Section Group – inherits from System.Configuration.ConfigurationSectionGroup, can only contain sections, can’t have any attributes (as far as I know).
  • Section – inherits from System.Configuration.ConfigurationSection, the workhorse. This is an XML element that has attributes, but it can’t contain sub-elements (as far as I know)
  • Element Collection – inherits from System.Configuration.ConfigurationElementCollection. This let’s you have those AddRemoveClearMaps – like above, that <settings> element.

I don’t know if this will help, but here is that same config, with some comments as to what XML elements in the configuration, correspond to which types of classes:

<sederSoftware.components>                                    <!--Section Group-->
<generalSettings enabled="false"/> <!--Section-->
<contextSettings> <!--Section-->
<settings> <!--Element Collection-->
<clear /> <!--(built-in)-->
<remove name="test3"/> <!--(built-in)-->
<add name="test1" enabled="true" partOfDay="Morning"/> <!--Element-->
<add name="test2" enabled="false" partOfDay="Evening"/> <!--Element-->
</settings>
</contextSettings>
</sederSoftware.components>

For most cases (that I can think of), this should really cover what you need. You can use a ConfigurationSectionGroup or ConfigurationElementCollection as a “container” and you can use ConfigurationSection or ConfigurationElement as data structures to hold the actual values that you want to store.

Creating a Chain of Configuration:
OK, so at this point, we have an idea of what kind of config we want to support, and what kind of data structure we need to create (and what to inherit from), so how do we actually create this? Using the sample config above, let me show each piece. I will start at the top, with <sederSoftware.components> – that XML is represented by a class like this:

public class SederSoftwareSectionGroup : ConfigurationSectionGroup
{
[ConfigurationProperty("generalSettings", IsRequired=false)]
public GeneralSettingsSection GeneralSettings
{
get { return (GeneralSettingsSection)base.Sections["generalSettings"]; }
}

[ConfigurationProperty("contextSettings", IsRequired = false)]
public ContextSection ContextSettings
{
get { return (ContextSection)base.Sections["contextSettings"]; }
}
}

If we walk down into GeneralSettings (or <generalSettings>), that is it’s own data structure too. That looks like this:

public class GeneralSettingsSection : ConfigurationSection
{
[ConfigurationProperty("enabled", IsRequired = false)]
public Boolean Enabled
{
get { return (Boolean)this["enabled"]; }
set { this["enabled"] = value; }
}
}

The properties of these translate to attributes in the configuration file. So, the Enabled property of the class corresponds to <generalSettings enabled=””/> attribute.

Now, the other element at the same level of <generalSettings>, is a little trickier. I haven’t found a better way to do this. You need a container first, then you can create that AddRemoveClearMap. So, I have a Section called <contextSettings>, and then I point to the ConfigurationElementCollection below that. So, that container <contextSettings> looks like this:

public class ContextSection : ConfigurationSection
{
[ConfigurationProperty("settings", IsRequired = false)]
public ContextSettingElementCollection Settings
{
get { return (ContextSettingElementCollection)base["settings"]; }
}
}

This points to that ContextSettingsElementCollection, which is the actual AddRemoveClearMap. That class, I think needs this – at it’s most simplest:

[ConfigurationCollection(typeof(ContextSettingElementCollection),
CollectionType=ConfigurationElementCollectionType.AddRemoveClearMap)]
public class ContextSettingElementCollection : ConfigurationElementCollection
{
public override ConfigurationElementCollectionType CollectionType
{
get
{
return ConfigurationElementCollectionType.AddRemoveClearMap;
}
}

protected override ConfigurationElement CreateNewElement()
{
return new ContextSettingElement();
}

protected override object GetElementKey(ConfigurationElement element)
{
return ((ContextSettingElement)element).Name;
}

public ContextSettingElement this[int index]
{
get { return (ContextSettingElement)base.BaseGet(index); }
set
{
if (base.BaseGet(index) != null)
{
base.BaseRemoveAt(index);
}
base.BaseAdd(index, value);
}
}

public ContextSettingElement this[String name]
{
get
{
if (String.IsNullOrEmpty(name))
{
throw new InvalidOperationException("Indexer 'name' cannot be null or empty.");
}
foreach (ContextSettingElement element in this)
{
if (element.Name.Equals(name, StringComparison.InvariantCultureIgnoreCase))
{
return element;
}
}
throw new InvalidOperationException("Indexer 'name' specified cannot be found in the collection.");
}
}
}

and then lastly, the actual ContextSettingElement looks something like this:

public class ContextSettingElement : ConfigurationElement
{
[ConfigurationProperty("name", IsRequired = true, IsKey=true)]
public String Name
{
get { return (String)this["name"]; }
set { this["name"] = value; }
}

[ConfigurationProperty("enabled", IsRequired = false)]
public Boolean Enabled
{
get { return (Boolean)this["enabled"]; }
set { this["enabled"] = value; }
}

[ConfigurationProperty("partOfDay", IsRequired = false)]
public PartOfDayOptions PartOfDay
{
get { return (PartOfDayOptions)this["partOfDay"]; }
set { this["partOfDay"] = value; }
}

public override string ToString()
{
return String.Format("Name="{0}"; Enabled="{1}"; PartOfDay="{2}";", Name, Enabled, PartOfDay);
}
}

understanding that I have an enum defined for PartOfDay, as this:

public enum PartOfDayOptions
{
Unknown = 0,
Morning,
Noon,
Evening
}

It’s worth mentioning that I have an extra layer in-between – and that is because I don’t think you can add a ConfigurationElementCollection directly to a Section Group. So, that element collection needs to be a child of a section, which is a child of the section group.

I don’t know why they did that, that just seems to be how it works.

Let’s put all of this together. All we’ve done is create some data structures (classes) that represent the hierarchal data. So, if I want access to that <generalSettings enabled=”true”/> value, I could do something like this in code:

SederSoftwareSectionGroup sectionGroup;
// TODO: Get a handle to the configuration file...

Boolean isEnabled = sectionGroup.GeneralSettings.Enabled;

From that top-level section group, I can walk into those properties and collection and get access to all of the configuration values, in a strongly-typed way. There is no XPath to deal with or anything like that, I just reference these data structures.

But wait, we have data structures to hold the settings, we sort of “chained” them together to match the same hierarchy of the config file – but do we ACTUALLY read those value? We’ll cover that below. First, we need to actually create a valid configuration file.

Define <configSections> in the Consuming Application:
I know what you’re saying “Hey Rob, you dum-dum, we already did that, my custom configuration is already in my app.config!”. That’s great, but that’s not all there is to it.

In order for you to adapt these XML settings into strongly-typed objects later, you need to tell the .NET run-time WHICH classes handle which XML elements. You do this with a <configSections> at the top of the configuration file. If you have a <configSections>, it MUST be the first element under <configuration>. Here is how I defined my sample:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<configSections>
<sectionGroup name="sederSoftware.components"
type="SederSoftware.Components.Configuration.SederSoftwareSectionGroup, SederSoftware.Components, Version=1.0.0.0">
<section name="generalSettings"
type="SederSoftware.Components.Configuration.GeneralSettingsSection, SederSoftware.Components, Version=1.0.0.0"/>
<section name="contextSettings"
type="SederSoftware.Components.Configuration.ContextSection, SederSoftware.Components, Version=1.0.0.0"/>
</sectionGroup>
</configSections>

As you can see, there are a couple of important things here:

  • Use <sectionGroup> for a ConfigurationSectionGroup, use <section> for ConfigurationSection
  • The “name” attribute value must match your XML element. In the case above, I have name=”generalSettings” – if that element has a different name, this will fail.
  • These are version-specific! More on that below…

If you are developing a framework, this is important: you might think “this is kind of ugly, why don’t I just add my <configSections> to the machine.config and be done with it?”. First, I agree, this <configSections> is kind of ugly, but this is the approach will allow your developers to use multiple versions of your components with multiple projects.

Imagine your developer has version 3.1, and 3.5 of your component framework installed. As part of the install, you modify the machine.config and add in these <configSections>. So, when you installed 3.5, it replaced the 3.1 definition. When the developer has an app open that uses 3.1, the <configSections> defined in the machine.config will point to 3.5 and you will get a run-time error that says that you can’t have two versions of your component loaded at the same time.

For this reasons, you MUST do this in the web.config/app.config, to give the developer that flexibility to use multiple versions of your component on the same machine.

Using / Reading the Config Values:
As a checkpoint, we’ve:

  • Created a skeleton of the config that we want to use, imagining how the developer might use it.
  • We created data structures to support those configuration elements.
  • We’ve defined the <configSections> in the consuming configuration file, so that the run-time knows how to deserialize the section.

Now, we are ready to read the actual values. The technique is slightly different if you have a web app versus non-web, so I’ll cover both below. I’ve found this easiest to just write all this logic in one place. Since you are already working in the Configuration namespace in your component, I typically create a ConfigurationHelper class that does this hard work for me. That looks like this:

public static class ConfigurationHelper
{
public static SederSoftwareSectionGroup GetCurrentComponentConfiguration()
{
return GetCurrentConfiguration().SectionGroups["sederSoftware.components"] as SederSoftwareSectionGroup;
}

public static System.Configuration.Configuration GetCurrentConfiguration()
{
if (GetIsWeb())
{
return WebConfigurationManager.OpenWebConfiguration("~/");
}
else
{
return ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.PerUserRoamingAndLocal);
}
}

public static Boolean GetIsWeb()
{
if (HttpContext.Current == null)
{
// NON-WEB

// Check for WCF hosted in IIS - if you have a better way, please email me!
String processName = Process.GetCurrentProcess().ProcessName;
if (processName.Equals("w3wp", StringComparison.InvariantCultureIgnoreCase) // IIS process
|| processName.Equals("WebDev.WebServer", StringComparison.InvariantCultureIgnoreCase) // Cassini: .NET 2.0-3.5
|| processName.Equals("WebDev.WebServer40", StringComparison.InvariantCultureIgnoreCase)) // Cassini: .NET 4.0
{
// This is WCF running in IIS/Cassini
return true;
}
else
{
// Regular non-web app: winforms, console, windows service, etc
return false;
}

}
else
{
// WEB
return true;
}

}
}

As you can see, most of the code is in GetIsWeb(). First, many times people will ask “Shouldn’t that be a property?” – the answer is no. There is a published convention in Framework Design Guidelines that says a property getter should never throw an exception. In this case, the business of finding out if we are in a web context has PLENTY of room of error. So above, I have a basic implementation . In real life, this would have a bunch of error handling. In the end though, since something might slip through the cracks, I am leaving it as a Get type of method.

Next, if case you didn’t read this in the comments above – when you host a WCF service in IIS, the “session” object is not available (unless you run in compatibility mode, which most don’t). So – that is a case where Session will be null, but we DO have a web.config, so I have some additional logic to check the process name. This is u-g-l-y, in my opinion, if you have a better way to detect this – please let me know!

OK, so that class gives us access to the current config (whether it’s an app.config or web.config) – and it even gives us a handle to the top-level of our configuration! I have a sample app, which has sample config – in that console app, I can now do something like this:

SederSoftwareSectionGroup sectionGroup = ConfigurationHelper.GetCurrentComponentConfiguration();

if (sectionGroup == null)
{
Console.WriteLine("sederSoftware.components - does not exist.");
}
else
{
Console.WriteLine("sederSoftware.components/generalSettings - "
+ sectionGroup.GeneralSettings.ToString());

Console.WriteLine("sederSoftware.components/contextSettings - Number of items: "
+ sectionGroup.ContextSettings.Settings.Count.ToString());

foreach (ContextSettingElement element in sectionGroup.ContextSettings.Settings)
{
Console.WriteLine("---{0}", element.ToString());
}
}

As you can see, I can very easily get a handle on the configuration and inspect the values.

image

In your case, if you writing a framework, you may not want to expose this publicly – but I just wanted to show that you can easily expose your config settings, even to your consumers, if you want.

Intellisense: Create and Apply a Schema for Your Component:
If you stopped right now, this could be considered acceptable. You have your own custom configuration, you have data structures behind it, and you have a way to read those values.

What about the poor developer using this component though? When they are typing in the configuration file, how are they supposed to know the structure? Sure, you can provide templates or you could publish documentation, but there is a better way. There is Intellisense for code and there is Intellisense for config, so why not support that?

There is a decent amount written up on messing with DotNetConfig.xsd and catalog.xml already. But the good news is, there is a far easier way to support version-specific Intellisense in your configuration files and it just requires 2 XML attributes. You make the top of your config file look something like this:

<?xml version="1.0"?>
<configuration xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="SederSoftwareIntellisense http://schema.example.com/SederSoftwareComponents-v1.0.0.0.xsd">

Note that this is not a valid location, it’s just a sample URL. You can host your config schemas on your website though. If everyone just points to the website for the Intellisense schema, then you can easily host new versions or fix bugs in old versions.

So, by simply pointing to this schema, that gives actual Intellisense in the configuration file!

image

This is great, but what does that schema look like? It’s too long to paste here, so look inside the attached sample project. In short, open up the DotNetConfig.xsd file, and start by getting the format from there – well, at least the first few lines:

<?xml version="1.0" encoding="us-ascii"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:vs="http://schemas.microsoft.com/Visual-Studio-Intellisense"
elementFormDefault="qualified" attributeFormDefault="unqualified"
vs:helpNamespace="http://schemas.microsoft.com/.NetConfiguration/v2.0">

But where do you start? Well, there is this project on CodeProject that used to work fine for .NET 2.0, but it was a train-wreck when I tried to get it to work with 4.0.

Instead, I’d recommend doing this: create a fully-fleshed out configuration with all of the options filled out. While that config file is open, open the XML menu and choose “Create Schema…”

image

This will create a good chunk of the schema for you. You will have to do a fair-amount of customizing, but it’s not unreasonable. The good part is, if you want to reproduce functionality that you see with other Intellisense, you have access to the schemas (again, check out DotNetConfig.xsd or the xsd that is included in the source project below).

A couple of things to note. To add Intellisense to any element (including any element, attribute, or enum value) – use this sort of syntax within that defining element in the schema:

<xs:annotation>
<xs:documentation>What you put here, shows up as Intellisense.</xs:documentation>
</xs:annotation>

When you generate your schema, it will be very simple. You would typically need to take something like this:

<xs:attribute name="name" type="xs:string" use="required" />

and embellish it to be something like this:

<xs:attribute name="name" type="xs:string" use="required">
<xs:annotation>
<xs:documentation>The name of the item.</xs:documentation>
</xs:annotation>
</xs:attribute>

One thing I like to do is have better control over boolean values. So, you can do something like this:

<xs:attribute name="enabled" type="xs:boolean" use="required" />

I prefer to be explicit by using a schema “restriction” like this:

<xs:attribute name="enabled" use="required">
<xs:annotation>
<xs:documentation>Enables or disables this feature.</xs:documentation>
</xs:annotation>
<xs:simpleType>
<xs:restriction base="xs:string">
<xs:enumeration value="true">
<xs:annotation>
<xs:documentation>Enable this feature.</xs:documentation>
</xs:annotation>
</xs:enumeration>
<xs:enumeration value="false">
<xs:annotation>
<xs:documentation>Disable this feature.</xs:documentation>
</xs:annotation>
</xs:enumeration>
</xs:restriction>
</xs:simpleType>
</xs:attribute>

One last note on schemas – this issue took me some time to find. I am not create with XML schema, so I didn’t know this off the top of my head. The minOccurs and maxOccurs has some limitations, which aren’t obvious.

If you have a scenario where you want to allow exactly 0 or 1 elements, within an existing element, don’t use an <xs:choice> or <xs:sequence>, use <xs:all>. By itself, it will FORCE you to have at least 1 of everything defined within. However, you can override specific elements and set minOccurs=”0″.

Again, there is a full example in the source provided – or you can look at DotNetSchema.xsd for ideas too.

Summary:
That is about it. If your component or application needs custom configuration, this article attempts to explain everything that is needed to provide effective configuration to your developers, including Intellisense. I hope that helps – and for Future Robert who is reading this, your welcome! 🙂

Source: Download ‘SederSoftware.Components_Solution.zip’


 

One comment on “The Complete Guide to Custom Configuration Sections
  1. […] Seder has a great post on writing custom configuration sections. The last paragraph explains how you can create xsd’s for your […]

    Like

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Archives
Categories

Enter your email address to follow this blog and receive notifications of new posts by email.

Join 2 other followers

%d bloggers like this: