29 January 2007

Custom configuration with nested collections in .Net 2.0

I am writing this as a brain dump.

Has been played around with .NET 2.0 strong type configuration this weekend (a bit late, I know). After some uneven drive-thanks to the lack of documentation- I finally produce a custom configuration file like the one below.

(For my work colleagues at Talis, yeah you - I know you will check my blog – If you are wondering why I am not using the time on our project for RDF Java configuration. Honestly, I do. I mean you absorb and learn from a competing technology, getting ideas from patterns, even the naming convention it uses – particularly there is Reflector for sneaking around.)

Here is the custom configuration setting with nested collection:
<section name="workflowConfig" type="Jingye.Workflow.Exe.WorkflowConfigSectionHandler,Workflow" />


<workflowConfig seperateProcesses="true" processWaitTime="50000">
<setup workingPath="c:\temp\" />
<workflow name="WorkflowOne" interval="15" dailyRun="true" startTime="09">
<task name="task1" onError="Abort"/>
<task name="task2" onError="Log" />
</workflow>
<workflow name="WorkflowTwo" interval="9999" dailyRun="true" startTime="08">
<task name="anotherTask1" onError="Abort"/>
<task name="anotherTask2" onError="Log" />
</workflow>
</workflowConfig>

Some well-known bits and bobs
1. Where is my config file?
For web app, this setting is in web.config. It will be picked up by ConfigutationManager as the default config file.
For console app or win service, you need to tell the Configuration about this file.
1) The short form. You app.exe.config file should be side by side with app.exe. Framework will ‘intelligently’ figure out the path and suffix ‘.config’ to the exe.
Configuration config = ConfigurationManager.OpenExeConfiguration("Workflow.exe");
WorkflowConfigSectionHandler scheduler = (WorkflowConfigSectionHandler)config.GetSection("workflowConfig");

2) The long form. The short form wraps this long form for convenient sake, but if you want to load a config from a different location or even pass in as command line argument, these are the lines to use

ExeConfigurationFileMap fileMap = new ExeConfigurationFileMap();
// relative path names possible
fileMap.ExeConfigFilename = @"c:\temp\exe\ConfigTest.exe.config";
// Open another config file
Configuration config =
ConfigurationManager.OpenMappedExeConfiguration(fileMap,
ConfigurationUserLevel.None);

//read/write from it as usual
ConfigurationSection mySection = config.GetSection("workflowConfig");

2. Your configuration section - the root derives from System.Configuration.ConfigurationSection. Back in .Net 1.x era, this is traditionally called XXXXHandler as it implements IConfigurationSectionHandler interface.

3. Each configuration element is declared as class derives from ConfigurationElement. Configuration element can have child elements, again derives from ConfigurationElement.

4. Configration elements take attributes/properties - decorates class properties with ConfigurationProperty. However configuration element doesn’t take CDATA text as body.

[ConfigurationProperty("seperateProcesses", DefaultValue = "true", IsRequired = false, IsDefaultCollection = true)]

5. Assign a key property in ConfigurationPropertyAttribute decoration. Such like ‘name’. You will need this to locate your configuration element in a MAP.

6. If a config element is a collection such like this, you also need to implement a façade to manage access to each member, this MyConfigElementCollection class is derived from ConfigurationElementCollection class.

Less well-known bits about MyConfigElementCollection class

1. By default, MyConfigElementCollection uses AddRemoveClearMap as its merging semantics – meaning how machine.config and your web.config or app.exe.config should merge together.
You can override this to be BasicMap or AddRemoveClearMapAlternate or BasicMapAlternate. Mark Gabarra discusses each of this merging semantic in his blog “.Net Configuration Default Behaviour”, worth a read if you are wondering ‘how could I stop downstream config changes my setting?’

public override ConfigurationElementCollectionType CollectionType
{
get{ return ConfigurationElementCollectionType.BasicMap;}
}


2. Override ElementName, if you are not using AddRemoveClearMap or just find another ‘<add>’ being confusing to the guy actually uses your app.
By default, ElementName is “” (String.Empty), because in AddRemoveClearMapAlternate semantic, elements should be view as ‘operation instructions’ – add, remove or clear an element. The real name of the element is passed in the strong typing context, hence anonymous is acceptable. Nevertheless, it is less self-describing to the user.
protected override string ElementName
{ get { return "workflow"; } }

3. In the parent element, where an instance of this element or a collection of this elements is declared, give the default name – “” as the name in ConfigurationProperty.
[ConfigurationProperty("", IsRequired = false, IsDefaultCollection = true)]
public WorkflowConfigElementCollection Workflows
{
get
{ return (WorkflowConfigElementCollection)base[""]; }
set
{ base[""] = value; }
}

I find this is confusing at the beginning, but if link back to point 2, it all make sense on how reflection and serialization works.

Reference:
Mark Gabarra, blog “.Net Configuration Default Behaviour
Jason Diamond blogs on using call back custom validator: Custom Configuration Validator Weirdness
Alois Kraus: Read/Write App.Config with NET 2.0