The .NET configuration subsystem is used throughout the framework. Entire subsystems rest on top of it including security policies, application settings and runtime configuration. ASP.NET uses the configuration subsystem heavily. Applications can take advantage of the subsystem as well by creating their own configuration sections. Unfortunately it is not straightforward. This article will discuss how to create new configuration sections in .NET.
A point of clarification is needed. Application settings, while relying on the configuration subsystem, are not related to configuration sections. This article will not discuss the creation or usage of application settings. Application settings, for purposes of this article, would be though settings that you can define in a project’s property pages.
History
Since the configuration subsystem is used throughout the framework it has been available since the initial release of .NET. In v1.x you could extend the configuration subsystem by implementing the IConfigurationSectionHandler. This interface boiled down to parsing XML elements. While usable it was a little much to implement. It also didn’t allow much more than reading in XML attributes and translating them into usable values.
In v2.0 the v1.x interface was wrapped in a more complete, complex set of classes. The v2.0 subsystem allows for declarative or programmatic declaration of strongly type configuration properties, nested configuration settings and validation. The newer subsystem also allows reading and writing configurations. Additionally the need to manually parse XML elements has been all but removed. While this makes it much easier to define configuration sections it also makes it much harder to deviate from the default behavior. Adding in sparse documentation and confusing examples results in a lot of postings in the forums about how to get things to work.
This article will focus exclusively on the v2.0 classes. While we will not discuss the v1.x interface any it is important to remember that it still exists and resides under the hood.
Are We there Yet?
Rather than building up an example as we go along we are going to first take a look at the configuration that we ultimately want to be able to read. We will then spend the rest of the time getting it all set up. This is a typical design approach for configurations. You know what you want. All you need to do is get the subsystem to accept it.
For this article we are going to build a configuration section for a hypothetical test engine. The test engine runs one or more tests configured in the configuration file. The engine simply enumerates the configured tests and runs them in order. Here is a typical section that we will want to support. This will be refered to as the target XML throughout this article.
<tests version=”1.0” logging=”True“>
<test name=”VerifyWebServer”
type=”TestFramework.Tests.ServerAvailable“
failureAction=”Abort“>
<parameter name=”url” value=”http:\www.myserver.com“>
</test>
<test name=”CheckService1”
type=”TestFramework.Tests.WebServiceInvoke”
async=”true” timeOut=”120“>
<parameter name=”url”
value=”http:\www.myserver.comservice1.asmx” />
<parameter name=”parameter_1” value=”hello” />
<parameter name=”returns” value=”HELLO” />
</test>
</tests>
Each test is represented by an XML element and all tests are contained in a parent tests element. Each test must have a unique name and a type attribute. The type attribute is used by the engine to create the appropriate test class to run. A test can have some optional attributes as well. The failureAction attribute specifies what should happen if the test fails (continue, abort, alert). The default is to continue. The async attribute is a boolean value indicating whether the test should be run synchronously or asynchronously. The default is false. The timeOut attribute specifies how long to wait for the test to complete (in seconds) and is only meaningful for async tests. The default is 10 seconds.
Configuration Files
The configuration subsystem must be able to map each XML element to a .NET type in order to be able to read it. The subsystem refers to a top level XML element as a configuration section. Generally speaking each configuration section must map to a section handler. This is where the v1.x configuration interface mentioned earlier comes in. When creating a new configuration you must define the configuration section that the subsystem will load. Let’s take a look at a standard application configuration for a moment. This will be refered to as the example XML throughout this article.
<?
xml version=”
1.0”
encoding=”
utf-8” ?>
<
configuration>
<
configSections>
<
section name=”
tests”
type=”
TestFramework.Configuration.TestsSection,ConfigurationTest” />
</
configSections>
<tests>
</tests>
</configuration>
All XML elements will reside inside the configuration root element. In the above file the application has defined a new section for the tests and created an empty tests element where the tests will go. Every section inside the configuration element must have a section handler defined for it.
To define a custom section in the configuration file we have to use the configSections element. This element might already exist in the file. It is a best practice to always put it as the first section in the file. In the above example the element tells the subsystem that whenever it finds a tests element it should create an instance of the TestFramework.Configuration.TestsSection class and then pass the XML on for processing. The type is the full typename (including namespace) followed by the assembly containing the type. It can be a full or partial assembly name. If the subsystem cannot find a section handler for an XML element or the type is invalid then the configuration subsystem will throw an exception.
“So I have to define a section handler for every XML element? That’s nuts, forget it.” Well, not exactly. Firstly the configuration subsystem only cares about the top level XML elements (anything direct under configuration). Child elements do not need a configuration section (but they do need a backing class as we’ll discuss later). Secondly the subsystem supports section groups. Section groups can be used to group sections together without requiring a handler. The framework itself generally separates sections by the namespace they are associated with. For example ASP.NET configuration sections generally reside in the system.web section group. You define a section group in the configSections element like so.
<?xml version=”1.0” encoding=”utf-8” ?>
<configuration>
<configSections>
<sectionGroup name=”testFramework“>
<section name=”tests”
type=”TestFramework.Configuration.TestsSection,TestFramework.Configuration” />
</sectionGroup>
</configSections>
<testFramework>
<tests>
…
</tests>
</testFramework>
</configuration>
Section groups are useful for grouping together related sections. Groups can be nested inside other groups. We will not discuss section groups further as they have no impact on section handlers. A section handler does not care about anything above it in the XML tree.
A question you may be wondering about is why you do not see any section definitions for the framework sections. That is because the subsystem actually looks for section handlers in the application, machine and domain configuration files. The machine and domain configurations reside in the framework directory and can be configured by an administrator to control .NET applications. If you were to look into these files you will eventually find section handler definitions for each of the pre-defined sections. Furthermore you can look into the system assemblies and find their corresponding handler classes. There is nothing special about the pre-defined sections.
A final note about configuration files if you have never worked with them. When you add an application configuration file to your project it will be called app.config. The runtime expects the configuration file to be named after the program executable (so prog1.exe would be prog1.exe.config). The IDE will automatically copy and rename the app.config project item to the appropriate name and store it with the binary during a build. Any changes you make to the configuration file (in the output directory) will be lost when you rebuild.
Mapping XML to .NET
Before diving into the .NET code we need to understand how the subsystem will map the XML data to .NET. The subsystem uses sections, elements and properties to map from XML elements and attributes.
A configuration element is a class that derives from ConfigurationElement. This class is used to represent an XML element. Every XML element will ultimately map to a configuration element. XML elements are normally used to house data that is too complex for a simple attribute value. Elements are also used for holding collections of child elements. Configuration elements will be used, therefore, to represent complex objects and/or parent objects.
A configuration property is ultimately represented by a ConfigurationProperty. Normally, however, we apply an attribute to a class property to define them so the actual declaration will be hidden. Configuration properties represent XML attributes. Every XML attribute associated with an XML element will map to a configuration property on the corresponding configuration element. As we will discuss later properties can be optional, have default values and even do validation.
A configuration section is a special type of configuration element. A section derives from ConfigurationSection, which itself derives from ConfigurationElement. For our purposes the only distinction is whether the element is a top level element or not. If the element is a top level element that is defined in the configSections of the configuration file then it will be a configuration section (and derive from the appropriate class). For all other purposes it works like an element.
The configuration subsystem requires that every XML element and attribute be defined by either a configuration element/section or configuration property. If the subsystem cannot find a mapping then an exception will occur. We will discuss later how we can have some control over this.
To start things off we will define the configuration section for our example. Following the precedence set up by the framework we will isolate our configuration classes to a Configuration subnamespace. We will name configuration sections as -Section and elements as -Element. The beginning name will match the XML element name but using Pascal casing.
Here is how we would define our configuration section.
using System;
using System.Configuration;
namespace TestFramework.Configuration
{
public class TestsSection : ConfigurationSection
{
…
}
}
We will fill this class in as we progress. At this point we have discussed enough to get the subsystem to load our example XML and return an instance of the TestsSection class.
Configuration Manager
In v2+ you will use the ConfigurationManager static class to interact with the configuration subsystem. This class provides some useful functionality but the only one we care about right now is GetSection(). This method requires the name of a section and, upon return, will give us back an instance of the associated configuration section class with the configuration data. Here is how our test engine would get the list of tests to run.
using System;
using System.Configuration;>
using TestFramework.Configuration;
namespace TestFramework
{
class TestEngine
{
public void LoadTests ( )
{
TestsSection section =
ConfigurationManager.GetSection(“tests”) as TestsSection;
…
}
}
}
The subsystem will only parse a section once. Subsequent calls will return the same data. If the file is changed externally then the changes will not be seen without restarting the application. You can force the subsystem to reload the data from disk by using ConfigurationManager.RefreshSection.
ConfigurationManager is in the System.Configuration namespace of the same named assembly. This assembly is not automatically added as a reference so you will need to add it manually. For those of you familiar with the v1.x subsystem, especially AppSettings, note that most of the classes are obsolete. You should use ConfigurationManager for all new development.
As an aside almost all errors in the subsystem will cause an exception of type ConfigurationException or a derived class. It can be difficult to tell from the exception what went wrong. Badly formed XML or a missing section handler, for example, will generate a generic error saying the subsystem failed to initialize. Use exception handling around all access to the subsystem but do not expect to be able to generate useful error messages from the resulting exception.
Configuration Properties
XML elements normally have one or more attributes associated with them. In the target XML the tests element has a version and logging attribute. The version is used for managing multiple versions of the engine and must be specified. The logging attribute specifies that the engine should log all test runs. It defaults to false. Modify the example XML to include these attributes. Trying to load the tests at this point will cause an exception because the attributes are not supported by the configuration section.
For each attribute on a section/element a configuration property must be defined. Configuration properties can be defined either declaratively through attributes or programmatically. Declarative properties use an attribute on public properties to identify configuration properties. Programmatically declaring configuration properties requires that a configuration property field be created for each attribute. Declarative properties are easier to write and understand but run slightly slower than programmatic properties. Otherwise either approach can be used or they can be used together.
Declaratively
To define a property declaratively do the following.
- Declare a public property in the section/element class.
- Define a getter and, optionally, a setter for the property.
- Add a ConfigurationProperty attribute to the property.
The public property name will generally match the XML attribute but use Pascal casing. The type will be a standard value type such as bool, int or string. Use the type most appropriate for the property.
There are no fields to back the properties. The subsystem is responsible for managing the property values. The base class defines an indexed operator for the section/element that accepts a string parameter. The parameter is assumed to be the name of an XML attribute. The base class will look up the attribute and get or set the value as needed. The properties are assumed to be objects so casting will be necessary.
The ConfigurationProperty only requires the name of the XML attribute. There are several optional parameters that can be specified as well.
Parameter |
Default |
Meaning |
DefaultValue |
None |
If the attribute is not specified then the corresponding property will have the specified value. |
IsDefaultCollection |
False |
Used for collections. |
IsKey |
False |
Used for collections. |
IsRequired |
False |
True to specify that the attribute is required. |
Options |
None |
Additional options. |
The DefaultValue parameter should be used to give a property a default value. Since the base class manages property values rather than using fields it is not necessary to worry about this parameter in code.
The IsRequired parameter specifies that the attribute must be specified. If it is not then an exception will occur. This parameter should be used for properties that can have no reasonable default value. The IsRequired parameter for the attribute does not work when applied to a child element. The subsystem will automatically create a new instance of any child elements when it reflects across the configuration properties. Later when the subsystem tries to verify that all required properties have received a value it cannot tell a difference between default initialized elements and those that were contained in the configuration file. To use the IsRequired parameter with a child element you must programmatically declare the property instead.
Here is the modified TestsSection class with the configuration properties declaratively defined. After modifying the code run it and verify the attribute values are correct. Try removing each attribute from the example XML and see what happens.
public class TestsSection : ConfigurationSection
{
[ConfigurationProperty(
“logging”, DefaultValue=
false)]
public bool Logging
{
get {
return (
bool)
this[“logging”]; }
set {
this[“logging”] = value; }
}
[ConfigurationProperty(“version”, IsRequired=true)]
public string Version
{
get { return this[“version”] as string; }
set { this[“version”] = value; }
}
}
Programmatically
To define a property programmatically do the following.
- Create a field for each attribute of type ConfigurationProperty.
- Add each field to the Properties collection of the base class.
- Declare a public property for each attribute.
- Define a getter and, optional, a setter for each property.
The configuration field that is created for each attribute is used in lieu of an attribute. The constructor accepts basically the same set of parameters. Once the fields have been created they must be associated with the section/element. The configuration properties associated with a section/element are stored in the Properties collection of the base class. When using declarative programming the properties are added automatically. In the programmatic approach this must be done manually. The properties cannot be changed once created so it is best to add the configuration properties to the collection in the constructor.
A public property for each attribute is created in a similar manner as with declarative programming. The only difference is the lack of an attribute on the property. Since the configuration property is a field in the class you can use the field rather than the property name if desired.
Here is a modified TestsSection class using the programmatic approach.
public class TestsSection : ConfigurationSection
{
public TestsSection ()
{
Properties.Add(m_propLogging);
Properties.Add(m_propVersion);
}
public bool Logging
{
get { return (bool)this[m_propLogging]; }
set { this[m_propLogging] = value; }
}
public string Version
{
get { return (string)this[m_propVersion]; }
set { this[m_propVersion] = value; }
}
private ConfigurationProperty m_propLogging =
new ConfigurationProperty(“logging”, typeof(bool), false);
private ConfigurationProperty m_propVersion =
new ConfigurationProperty(“version”, typeof(string),
null, ConfigurationPropertyOptions .IsRequired);
}
The programmatic approach can be optimized by using static fields and static constructors. But that requires more advanced changes so we won’t cover that today.
Validation
The subsystem will ensure that an XML attribute can be converted to the type of the property. If it cannot then an exception will occur. There are times though that you want to do even more validation. For example you might want to ensure a string is in a certain format or that a number is within a certain range. For that you can apply the validator attribute to the property as well. There are several different validators available and you can define your own. The following table defines some of them.
The type name given is the name of the underlying validator class that is used. You can, if you like, create an instance of the type and do the validation manually. More likely though you’ll apply the attribute of the same name instead. Here is a modified version of the version attribute to ensure that it is of the form x.y.
[ConfigurationProperty(“version”, IsRequired = true]
[RegexStringValidator(@”(d+(.d+)?)?”)]
public string Version
{
get { return this[“version”] as string; }
set { this[“version”] = value; }
}
If you are good with regular expressions you might have noticed that the expression allows for an empty string. This is a peculiarity of the subsystem. It will call the (at least the Regex) validator twice. The first time it passes an empty string. The validator must treat an empty string as valid otherwise an exception will occur.
Child Elements
Now that we can define sections and properties all we have to do is add support for child elements and we’re done. A child element, as already mentioned, is nothing more than a configuration element. In fact all that is needed to support child elements is a new configuration element class with the child configuration properties. Remember that a configuration section is just a top-level configuration element. Everything we have discussed up to now applies to configuration elements as well.
Here is the declaration for the configuration element to back the test XML element. The properties are included.
public class TestElement : ConfigurationElement
{
[ConfigurationProperty(
“async”, DefaultValue=
false)]
public bool Async
{
get {
return (
bool)
this[
“async”]; }
set {
this[
“async”] = value; }
}
[ConfigurationProperty(“failureAction”, DefaultValue=“Continue”)]
public FailureAction FailureAction
{
get { return (FailureAction)this[“failureAction”]; }
set { this[“failureAction”] = value; }
}
[ConfigurationProperty(“name”, IsKey=true, IsRequired=true)]
public string Name
{
get { return this[“name”] as string; }
set { this[“name”] = value; }
}
[ConfigurationProperty(“timeOut”, DefaultValue=120)]
[IntegerValidator(MinValue=0, MaxValue=300)]
public int TimeOut
{
get { return (int)this[“timeOut”]; }
set { this[“timeOut”] = value; }
}
[ConfigurationProperty(“type”, IsRequired = true)]
public string Type
{
get { return this[“type”] as string; }
set { this[“type”] = value; }
}
}
public enum FailureAction { Continue = 0, Abort, }
A couple of things to note. FailureAction is actually an enumeration. This is perfectly valid. The only restriction is that the attribute value must be a, properly cased, member of the enumeration. Numbers are not allowed.
The second thing to note is the IsKey parameter applied to Name. Only one property can have this parameter set. It is used to uniquely identify the element within a collection of elements. We will discuss it shortly.
The configuration section needs to be modified to expose the element as a child. Here is the modified class definition.
public class TestsSection : ConfigurationSection
{
…
[ConfigurationProperty(“test”)]
public TestElement Test
{
get { return this[“test”] as TestElement; }
}
}
Notice that we did not add a setter here since the user will not be adding the test explicitly. Modify the example XML to include (only) one of the elements from the target XML. Comment out any parameter elements for now. Compile and run the code to confirm everything is working properly.
As an exercise try creating the parameter child element yourself. Modify the example XML to include (only) one of the elements from the target XML and verify it is working properly. Do not forget to update the TestElement class to support the parameter.
Collections
The final piece of the configuration puzzle is collections. To support a collection of elements a configuration collection class must be created. This collection is technically just a class deriving from ConfigurationElementCollection. What makes collections so frustrating is that there are quite a few things that have to be done to use them in a friendly manner. Add to that confusing documentation and incorrect examples and it is easy to see why people are confused.
To keep things simple for now we are going to temporarily modify our example XML to fall in line with the default collection behavior. We will then slowly morph it into what we want. We will add support for multiple tests in the section first. Here are the steps for adding a collection with default behavior to a section/element.
- Create a new collection class deriving from ConfigurationElementCollection.
- Add a ConfigurationCollection attribute to the class.
- Override the CreateNewElement method to create an instance of the appropriate type.
- Override the GetElementKey method to return the key property of an element.
- In the parent element modify the public property to return an instance of the collection type.
Here is boilerplate code for an element collection. In fact you can create a generic base class if you want. You’ll see why that is a good idea later.
[ConfigurationCollection(
typeof(TestElement))]
public class TestElementCollection : ConfigurationElementCollection
{
protected override ConfigurationElement CreateNewElement ()
{
return new TestElement(); }
protected override object GetElementKey (
ConfigurationElement element )
{ return ((TestElement)element).Name; }
}
CreateNewElement is called when the subsystem wants to add a new element to the collection as it is parsing. The GetElementKey method is used to map an element to a unique key. This is where the IsKey parameter comes in. These are the only methods that have to be implemented but it is generally advisable to add additional methods for adding, finding and removing elements if the collection can be written to in code.
Now that the collection is defined it needs to be hooked up to the parent element. Here is the updated TestsSection class property definition. Notice that the property name and the XML element name were changed to clarify that it is a collection.
[ConfigurationProperty(“tests”)]
public TestElementCollection Tests
{
get
{
return this[[“tests”] as TestElementCollection;
}
}
As an aside the ConfigurationCollection element can be applied to the public property in the parent class rather than on the collection type itself. This might be useful when a single collection type can be used in several different situations. In general though apply the attribute to the collection type.
The final changes we need to make are to the example XML itself. The collection of tests need to be contained in a child element rather than directly in the section because that is how the collection is defined. Additionally the default collection behavior is to treat the collection as a dictionary where each element maps to a unique key. Elements in the collection can be added or removed or the entire collection cleared using the XML elements: add, remove and clear; respectively. Here is the (temporary) updated example XML fragment.
<tests version=”1.0” logging=”True“>
<tests>
<add name=”VerifyWebServer”
type=”TestFramework.Tests.ServerAvailable”
failureAction=”Abort“>
<parameter name=”url” value=”http:\www.myserver.com“>
</add>
<add name=”CheckService1”
type=”TestFramework.Tests.WebServiceInvoke”
async=”true” timeOut=”120“>
<parameter name=”url”
value=”http:\www.myserver.comservice1.asmx” />
<!–<parameter name=”parameter_1” value=”hello” />
<parameter name=”returns” value=”HELLO” />–>
</add>
</tests>
</tests>
Altering Nameses
The first thing that you will likely want to change is the name used to add new items to the collection. It is standard to use the element name when adding new items. To change the element name for adding, removing and clearing items use the optional parameters on the ConfigurationCollection
[ConfigurationCollection(typeof(TestElement), AddItemName=“test”)]
public class TestElementCollection : ConfigurationElementCollection
{
…
}
<tests version=”1.0” logging=”True“>
<tests>
<test name=”VerifyWebServer”
type=”TestFramework.Tests.ServerAvailable” failureAction=”Abort“>
<parameter name=”url” value=”http:\www.myserver.com” />
</test>
…
</tests>
</tests>
Default Collectionon
Having a parent element for a collection is necessary when you are dealing with multiple collections inside a single parent element. Normally however this is not the case. You can eliminate the need for an element around the collection children by using the default collection option on the configuration property.
Modify the configuration property that represents the default collection to include the IsDefaultCollection parameter. Set the name of the property to an empty string. If this is not done then the subsystem will fail the request. During parsing any element that is found that does not match an existing configuration property will automatically be treated as a child of the default collection. There can be only one default collection per element.
Here is the TestsSection modified to have Tests be the default collection. The example XML follows.
public class TestsSection : ConfigurationSection
{
…
[ConfigurationProperty(“”, IsDefaultCollection=true)]
public TestElementCollection Tests
{
get { return this[“”] as TestElementCollection; }
}
}
<tests version=”1.0” logging=”True“>
<test name=”VerifyWebServer”
type=”TestFramework.Tests.ServerAvailable”
failureAction=”Abort“>
<!–<parameter name=”url” value=”http:\www.myserver.com” />–>
</test>
<test name=”CheckService1”
type=”TestFramework.Tests.WebServiceInvoke”
async=”true” timeOut=”120“>
<!–<parameter name=”url” value=”http:\www.myserver.comservice1.asmx” />
<parameter name=”parameter_1” value=”hello” />
<parameter name=”returns” value=”HELLO” />–>
</test>
</tests>
Collection Options
The default collection type allows for elements to be added, removed or the entire list cleared. This is often not what is desired. The alternative collection type allows for new elements to be added only. To tell the subsystem that the collection should not allow changes to the existing elements and to allow only new elements it is necessary to overload a couple properties in the collection class.
The CollectionType property specifies the type of the collection being used. The default is AddRemoveClearMap which specifies a modifiable collection. The alternative is BasicMap which allows only additions. In the case of a basic map the add, remove and clear item names are not used. Instead it is necessary to override the ElementName property to specify the name of child elements.
[ConfigurationCollection(
typeof(TestElement))]
public class TestElementCollection : ConfigurationElementCollection
{
protected override string ElementName
{
get {
return “test”; }
}
public override ConfigurationElementCollectionType CollectionType
{
get { return ConfigurationElementCollectionType.BasicMap; }
}
…
}
A warning about basic maps. The collection type is a parameter to the ConfigurationCollection attribute. However it does not appear to work properly when using a basic map. Stick with overriding the property instead.
Now that you have seen how to add support for collections try updating the TestElement class to support multiple parameters using a default basic map collection. At this point everything has been covered to create the code to read the target XML from the beginning of the article.
Updating Configurations
One of the features added in v2.x of the configuration subsystem was the ability to modify and save the configuration data. While application configurations should remain read-only (for security purposes), user configuration files can be modified. The example code is going to be modified to allow new tests to be added and saved.
To support modification of a configuration section/element the properties must support setters. In our example code we made all the properties settable so we do not need to make any changes. Some properties can have setters and others not. It is all dependent upon what the configuration section needs to support. The exception to the rule is collections. By default a collection does not expose any methods to modify the collection elements. It is necessary to manually add the appropriate methods if configuration collections can be modified. Additionally the property IsReadOnly method must be overloaded to allow modifying the collection.
The following modifications need to be made to the test collection to support adding new tests, removing existing tests and clearing the collection.
[ConfigurationCollection(
typeof(TestElement))]
public class TestElementCollection : ConfigurationElementCollection
{
…
public override bool IsReadOnly ()
{
return false; }
public void Add ( TestElement element )
{ BaseAdd(element); }
public void Clear ()
{ BaseClear(); }
public void Remove ( TestElement element )
{ BaseRemove(element.Name); }
public void Remove ( string name )
{ BaseRemove(name); }
}
For test purposes the engine will be modified to generate a new test section (with a dummy test) if none can be found in the configuration file.
public void LoadTests ()
{
m_Section = ConfigurationManager.GetSection(
“tests”)
as TestsSection;
if ((m_Section ==
null) ||
!m_Section.ElementInformation.IsPresent)
{
System.Configuration.Configuration cfg =
ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None);
if (cfg.Sections[“tests”] == null)
cfg.Sections.Add(“tests”, new TestsSection());
m_Section = cfg.GetSection(“tests”) as TestsSection;
TestElement test = new TestElement();
test.Name = “Dummy Test”;
test.Type = “DummyTest”;
m_Section.Tests.Add(test);
m_Section.SectionInformation.ForceSave = true;
cfg.Save();
};
}
Let’s walk through the code. The engine first tries to get the section (1). If it fails to get the section then it will create a new one. The configuration subsystem (contrary to documentation) seems to always return an instance of the section handler even if the actual section does not exist in the file. The example code checks to determine if the section actually exists or not.
The subsystem uses the Configuration class (not the namespace) to represent a configuration file. ConfigurationManager maintains an instance internally for the application configuration but this field is not exposed. Instead it is necessary to explicitly open the configuration file and modify it. Earlier it was mentioned that the data is only parsed once and that remains true. However multiple instances of the section class are returned. Changes made in one instance of a section are not visible in another.
The engine next (2) opens the configuration file explicitly. The engine then (3) creates a new section in the off chance that it did not exist yet. Now the engine (4) creates a dummy test and adds it to the section. Finally (5) the updated section is saved back to disk.
Temporarily comment out the tests element in the XML file and run the code. Look at the XML file and confirm the new test was created. What! It wasn’t? Actually it was. The problem is that the debugger is getting in the way. By default the vshost process is used to run the program. As a result the actual configuration file is <app>.vshost.exe.config. Additionally this file is overwritten when debugging starts and ends. Hence you are likely to miss the change. Place a breakpoint at the end of the LoadTests method and run it again. Now examine the configuration file to confirm the changes were made.
There are many more things that can be done to update the configuration file. You can save the file elsewhere, save only some changes or even modify other files. The preceding discussion should be sufficient to get you started though.
Dynamic Sections
The configuration subsystem is based upon deterministic parsing. At any point if the subsystem cannot match an XML element/attribute to a configuration element/property it will throw an exception. Configuration elements/sections expose two overridable methods (OnDeserializeUnrecognizedAttribute and OnDeserializeUnrecognizedElement) that are called if the parse finds an unknown element/attribute during parsing. These methods can be used to support simple dynamic parsing.
For unknown attributes the method gets the name and value that was parsed. If the method returns true then the subsystem assumes the attribute was handled otherwise an exception is thrown. The following method (added to TestElement) silently ignores a legacy attribute applied to a test. Notice that the element is compared using case sensitivity. Since XML is case sensitive comparisons should be as well.
protected override bool OnDeserializeUnrecognizedAttribute (
string name,
string value )
{
if (String.Compare(name,
“baseType”,
StringComparison.Ordinal) ==
0)
return true;
return base.OnDeserializeUnrecognizedAttribute(name, value);
}
For unknown elements the method must parse the XML manually and return true to avoid an exception. The important thing to remember about this method is that all child elements must be parsed otherwise the subsystem will not recognize the element and call the method again. The following method (added to TestElement) silently ignores a legacy child element that contained some initialization logic. In this particular case the child elements are not important (or parsed) so they are skipped.
protected override bool OnDeserializeUnrecognizedElement (
string elementName, System.Xml.XmlReader reader )
{
if (String.Compare(elementName, “initialize”,
StringComparison.Ordinal) == 0)
{
reader.Skip();
return true;
};
return base.OnDeserializeUnrecognizedElement(elementName, reader);
}
A word of caution is in order when using collections. If a collection’s item name properties have been modified (for example from add to test) then the method is called for each item. The underlying collection overrides this method to handle the item name overrides. Therefore do not assume that just because this method is called a truly unknown element has been found.
Before getting any wild ideas of how to get around the subsystem’s restrictions on element contents be aware that you cannot use the above methods to parse certain XML elements including CDECLs and element text. These XML entities will always cause the subsystem to throw an exception.
Standard Sections
The v1.x subsystem supported several standard section types that continue to be useful. They allow for storing custom settings without creating a custom section handler. The only downside is that they cannot be configured.
DictionarySectionHandler can be used to store a set of key-value pairs in a section. The following example demonstrates such a section.
<
configSections>
<
section name=”
settings”
type=”
System.Configuration.DictionarySectionHandler” />
</
configSections>
<settings>
<add key=”Setting1” value=”1” />
<add key=”Setting2” value=”2” />
<add key=”Setting3” value=”3” />
</settings>
Here is how it would be used. Notice that the return value is Hashtable rather than a section handler instance.
Hashtable settings = ConfigurationManager.GetSection(“settings”) as Hashtable;
The NameValueSectionHandler works identically to DictionarySectionHandler except the returned value is NameValueCollection.
The SingleTagSectionHandler is used to store a single element with attribute-value pairs. The returned value is a Hashtable where the attribute names are the keys.
The three legacy section handlers can be used in lieu of creating custom section handlers when simple dictionaries or attribute-value pairs are needed. As a tradeoff they do not support any of the advanced functionality of the subsystem including modification, validation or default values.