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.