Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Custom configuration, from soup to nuts

0.00/5 (No votes)
2 Jul 2012 1  
From a concept of custom configs to intellisense in VS 2010.

Sample Image

Introduction

This article provides everything you need for creating a custom configuration group. I will go through not only the mechanics of what you must do, but also some of the fine points I've seen many articles miss. In the end you should be able to easily jump in and create your own configurations.

Background

Far too often I've found pieces of information here or there on creating custom configuration files. This is meant to be a soup to nuts article covering everything you want to know about creating your own custom configuration section.

The Configuration Section

This is where all the configuration settings begin. You must define a section which contains all of your configuration settings. Before you even begin creating this, I suggest you make a rough layout of what your settings will look like. The reason for this is you need to put together a good set of names, you need to know if your section contains properties or just a collection, and finally it becomes a template for your code. So here is the example we will do for this article:

<applicationSection name= bork  active= true >
    <parameters>
        <add trigger= eventSent  type= myeventType, global.solutions.events  action= post  />
        <add trigger= eventComplete  type= myeventType, global.solutions.events  action= notify  />
        <add trigger= eventNotify  type= myeventType, global.solutions.events  action= send  />
    </parameters>
    <notifyLists>
        <add list= error  group= admins  server= mymail.global.solutions.com />
        <add list= warn  group= appGroup  server= mymail.globabl.solutions.com />
        <add list= friendly  group= customers  server= external.global.solutions.com />
  </notifyLists>
</applicationSection>

It isn't important how these parameters will be used in a program. It is only important how the classes will be named as well as the structures. We have our ConfigurationSection at the head of it all. It is the primary container for all of your parameters. It will have two properties and two collections. In the example implementation, we define our class named to match what we have in our example.

public class applicationSection:ConfigurationSection
{
}

You can immediately see the relationship between your configuration and your class. Before defining this class further, the two collection classes need to be defined.

[ConfigurationCollection( typeof( parameter ),
        CollectionType = ConfigurationElementCollectionType.AddRemoveClearMap )]
public class parameters : ConfigurationElementCollection
{
    protected override ConfigurationElement CreateNewElement ( )
    {
        return new parameter( );
    }

    protected override object GetElementKey ( ConfigurationElement element )
    {
        parameter internalElement = element as parameter;
        if ( internalElement != null )
            return internalElement.Trigger;
        else return null;
    }
}
[ConfigurationCollection( typeof( notification ),
    CollectionType = ConfigurationElementCollectionType.AddRemoveClearMap )]
public class notifyLists : ConfigurationElementCollection
{
    protected override ConfigurationElement CreateNewElement ( )
    {
        return new notification( );
    }

    protected override object GetElementKey ( ConfigurationElement element )
    {
        notification internalElement = element as notification;
        if ( internalElement != null )
            return internalElement.Key;
        else return null;
    }
}

Implementing the abstract class, you get your two methods. It is up to you to determine what you want as your Dictionary key. My example displays two ways of providing that information. The first method CreateNewElement() returns an instance of the collections containing class and the second GetElementKey() defines what you will use as a key. If your element does not have a field that is a natural key, you can just use the GetHashCode method. ConfigurationElementCollectionType states that it will be a collection that automatically exposes the ability to Add, Replace, and Clear the collection. Now the next step involves getting rid of the errors on these collections so let us define the class for each element. The elements are the atomic level of the configuration file and contain only properties. Each property is uniquely defined by the decorator on them.

public class parameter : ConfigurationElement
{
    [ConfigurationProperty( "trigger", IsKey = true, IsRequired = true )]
    public string Trigger
    {
        get
        {
            return (string)this[ "trigger" ];
        }
        set
        {
            this[ "trigger" ] = value;
        }
    }
    [ConfigurationProperty( "type", IsKey = false, IsRequired = true )]
    public string TriggerType
    {
        get
        {
            return (string)this[ "type" ];
        }
        set
        {
            this[ "type" ] = value;
        }
    }
    [ConfigurationProperty( "action", IsKey = false, IsRequired = true )]
    public string TriggerAction
    {
        get
        {
            return (string)this[ "action" ];
        }
        set
        {
            this[ "action" ] = value;
        }
    }

}
public class notification : ConfigurationElement
{
    [ConfigurationProperty( "list", IsKey = false, IsRequired = true )]
    public string MailList
    {
        get
        {
            return (string)this[ " list " ];
        }
        set
        {
            this[ " list " ] = value;
        }
    }
    [ConfigurationProperty( "group", IsKey = false, IsRequired = false )]
    public string MailGroup
    {
        get
        {
            return (string)this[ "group" ];
        }
        set
        {
            this[ "group" ] = value;
        }
    }
    [ConfigurationProperty( "server", IsKey = false, IsRequired = true )]
    public string MailServer
    {
        get
        {
            return (string)this[ "server" ];
        }
        set
        {
            this[ "server" ] = value;
        }
    }
}

The configuration system will handle populating your properties with Reflection so you don't have to worry about anything but having your properties properly decorated. So now that everything is defined, just how the heck do I access the stuff? In your program, still using the System.Configuration namespace, you will create two lines of code:

Configuration config = ConfigurationManager.OpenExeConfiguration( ConfigurationUserLevel.None );
applicationSection section = config.Sections[ applicationSection ] as applicationSection;

The first line gives you access to the ConfigurationHandler built into the framework. This is going to populate itself with all elements it sees in the configuration file. The second line shows you the importance of predefined sections. The configuration manager can populate each section and it is ready to hand it over to any consumer of its data. If you run into an issue in your definition, this is more likely the place where you will have an exception. If you do not properly define your section, the second line will return a null object or a missing key violation. If you have it defined properly but screwed up your object definitions, then the first line will throw an exception.

In the sample application, you will see that I created a var called rawSection so that I could interrogate the returned object and discover any source of issues with my objects. I should also note that the first line of code will also throw an exception if you fail to supply a field that was tagged as Required in the metadata. In a normal application you would catch that exception, interrogate it, and return a message to the developer/user what part of the configuration was missing. It should also be noted that if you add data validation to your configuration, this is where you will be notified via an exception as well.

This now provides you with the configuration section and you are ready to process it and enter your application execution phase.

The Schema

Since we began with a representation of our configuration settings, it is very easy to generate a schema from this. For schema generation, I like to use http://www.xmlforasp.net/codebank/system_xml_schema/buildschema/buildxmlschema.aspx. It has its limitations such as no optional fields, but it is free and does a decent job! So the first step was getting a generic schema:

< xml version="1.0" encoding="utf-8" >
<xsd:schema attributeFormDefault="unqualified" 
   elementFormDefault="qualified" version="1.0" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <xsd:element name="applicationSection">
    <xsd:complexType>
      <xsd:sequence>
        <xsd:element name="parameters">
          <xsd:complexType>
            <xsd:sequence>
              <xsd:element maxOccurs="unbounded" name="add">
                <xsd:complexType>
                  <xsd:attribute name="trigger" type="xsd:string" />
                  <xsd:attribute name="type" type="xsd:string" />
                  <xsd:attribute name="action" type="xsd:string" />
                </xsd:complexType>
              </xsd:element>
            </xsd:sequence>
          </xsd:complexType>
        </xsd:element>
        <xsd:element name="notifyLists">
          <xsd:complexType>
            <xsd:sequence>
              <xsd:element maxOccurs="unbounded" name="add">
                <xsd:complexType>
                  <xsd:attribute name="list" type="xsd:string" />
                  <xsd:attribute name="group" type="xsd:string" />
                  <xsd:attribute name="server" type="xsd:string" />
                </xsd:complexType>
              </xsd:element>
            </xsd:sequence>
          </xsd:complexType>
        </xsd:element>
      </xsd:sequence>
      <xsd:attribute name="name" type="xsd:string" />
      <xsd:attribute name="active" type="xsd:boolean" />
    </xsd:complexType>
  </xsd:element>
</xsd:schema>

The next step is to pull this into Visual Studio and add any valid values, edits, limits you wish. My final schema looks almost the same but with group being optional and list having an enumerated restriction.

< xml version="1.0" encoding="utf-8" >
<xs:schema id="applicationSchema"
    elementFormDefault="qualified"
    xmlns:xs="http://www.w3.org/2001/XMLSchema">
  <xs:element name="applicationSection">
    <xs:complexType>
      <xs:sequence>
        <xs:element name="parameters">
          <xs:complexType>
            <xs:sequence>
              <xs:element maxOccurs="unbounded" name="add">
                <xs:complexType>
                  <xs:attribute name="trigger" type="xs:string" />
                  <xs:attribute name="type" type="xs:string" />
                  <xs:attribute name="action" type="xs:string" />
                </xs:complexType>
              </xs:element>
            </xs:sequence>
          </xs:complexType>
        </xs:element>
        <xs:element name="notifyLists">
          <xs:complexType>
            <xs:sequence>
              <xs:element maxOccurs="unbounded" name="add">
                <xs:complexType>
                  <xs:attribute name="list" >
                    <xs:simpleType >
                      <xs:restriction base="xs:string">
                        <xs:enumeration value="error"/>
                        <xs:enumeration value="warn" />
                        <xs:enumeration value="failure"/>
                        <xs:enumeration value="friendly" />
                      </xs:restriction>
                    </xs:simpleType>
                  </xs:attribute>
                  <xs:attribute name="group" type="xs:string" use="optional"/>
                  <xs:attribute name="server" type="xs:string" />
                </xs:complexType>
              </xs:element>
            </xs:sequence>
          </xs:complexType>
        </xs:element>
      </xs:sequence>
      <xs:attribute name="name" type="xs:string" />
      <xs:attribute name="active" type="xs:boolean" />
    </xs:complexType>
  </xs:element>
</xs:schema>

It has been hit and miss trying to get intellisense working. Once you create a reference to your library defining your schema and your class libraries, you can select a blank area on your app.config, open Properties, and in the schema dropdown, you should see your http://tempuri.org link for your schema. While adding this to the properties page is supposed to give you intellisense, I failed to experience this. Another option is to place your schema in the folder with the other Visual Studio schemas (VisualStudio10/xml/schemas) then add a new line to the catalog.xml file:

<Association extension= config  schema= %InstallRoot%/xml/schemas/applicationSchema.xsd  
   condition="starts-with($TargetFrameworkMoniker, '.NETFramework,Version=v4.') or $TargetFrameworkMoniker = '' />

What I found finally worked for me was to copy the schema to the schema folder and add it to the list in the app.config properties page, then pick it from the unqualified list at the top of the list of schemas. The down side to this approach is you must edit the schema in your project, save it, then copy and paste it into the Visual Studio folder. But I am getting intellisense! I recommend you add comments in your solution so that other developers know what to do as well. To test this out, pull down the sample application, copy the XSD file to your schemas folder, and add it to the schema property of app.config. (If it is not already present in the project) when you add a new notifyList element, list= will give you a list of valid values to choose from. :)

Points of interest

So this article has begun with a rough idea of what our configuration settings would look like, then we created a class structure to match the configuration settings, then we implemented it in a second project and finally we added intellisense to the management of the configuration file. I hope you found this of value.

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