Creating Configuration Sections
Created: 11 August 2008
Updated: 26 October 2008
Download the sample code.
Contents
- History
- Are We There Yet?
- Configuration Files
- Mapping XML to .NET
- Configuration Manager
- Configuration Properties
- Child Elements
- Collections
- Updating Configurations
- Dynamic Sections
- Standard Sections
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.com\service1.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 go here -->
</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.
UPDATED: 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.
| Type |
Description |
| CallbackValidator |
Calls a method for more complex validation. |
| IntegerValidator |
Validates a numeric value including range and
precision. |
| LongValidator |
Same As IntegerValidator but applies to
longs. (Notice that MS failed to follow the naming guidelines for this
type :}) |
| PositiveTimeSpanValidator |
Validates a time duration. |
| RegexStringValidator |
Validates a string against a regular
expression. |
| StringValidator |
Validates a string for length and content. |
| SubclassTypeValidator |
Validates the type of a value. |
| TimeSpanValidator |
Validates a time duration. |
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.com\service1.asmx"
/>
<!--<parameter name="parameter_1" value="hello"
/>
<parameter name="returns" value="HELLO" />-->
</add>
</tests>
</tests>
Altering Names
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
attribute as demonstrated here.
[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 Collection
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.com\service1.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 ()
{
//1 - Try and get the section
m_Section = ConfigurationManager.GetSection("tests") as TestsSection;
if ((m_Section == null) || !m_Section.ElementInformation.IsPresent)
{
//2 - Open the configuration file
System.Configuration.Configuration cfg =
ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None);
//3 - Create the section if necessary
if (cfg.Sections["tests"] == null)
cfg.Sections.Add("tests", new
TestsSection());
m_Section = cfg.GetSection("tests") as TestsSection;
//4 - Add a dummy test
TestElement test = new TestElement();
test.Name = "Dummy Test";
test.Type = "DummyTest";
m_Section.Tests.Add(test);
//5 - Save the changes
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 known. 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 )
{
//Ignore legacy baseType attribute
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 )
{
//Ignore legacy initialize element and all its
children
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.
|