|
|||||||||||||||||||||
|
|||||||||||||||||||||
|
Announcements
Want a new Job?
Chapters
Services
Feature Zones
|
IntroductionOne of the wonderful features of .NET has been its XML configuration features. In the days of .NET 1.x, common application settings, database connection strings, ASP.NET web server configuration and basic custom configuration data could be stored in a .config file. Custom configuration sections could use a few basic but custom structures, allowing a small variety of information to be stored in a .config file. More complex configuration, however, was most often accomplished with custom XML structures and custom parsing code. Such code could become quite complex, though, and there are a variety of ways of differing performance to accomplishing the same thing. With .NET 2.0, the days of writing your own (probably complicated, poorly-performing and tedious) code to manage custom XML configuration structures are over. The custom configuration capabilities of the built-in XML configuration subsystem in .NET 2.0 have been greatly revamped, boasting some extremely useful and time saving features. With relative ease, just about any XML configuration structure you might need is possible with a relatively minimal amount of work. In addition, deserialization of the XML in a .config file can always be overridden. This allows any XML structure necessary to be supported without losing the other advanced features of .NET 2.0 configuration. The seriesThis is the first article in a series. All of the core concepts and important bits of knowledge required to begin using .NET 2.0 configuration sections is provided in this first article. However, the configuration framework available with .NET 2.0 is extensive. It has many capabilities and hidden features that can fill in the gaps and oversights in the framework that may seem apparent from this article. To continue learning about the .NET 2.0 configuration framework, additional articles can be found at the following links:
The mysteryI have been using .NET 2.0 for a little over a year now and sadly enough, have spent many a sleepless night tinkering with custom configuration models. Custom XML configuration handling is the ever-persistent performance sink, requiring a new model for each application, depending on what kind of configuration is being stored and how it needs to be accessed (and from where). It wasn't until about eight months ago, while digging through .NET 2.0 Framework assemblies with Reflector, that I came across a curious little class: Suffice it to say, there ultimately is minimal information on the internet about the true capabilities of custom configuration sections. Through internet research, hours of digging through framework code in Reflector, and continual experimentation and use of custom configuration sections, I've finally learned what I needed to know. It's been a life saver. Finally, creating and managing custom XML configuration is simple to write, simple to use, simple to manage and performs wonderfully... and I'm here to share this holy grail with all of you, my fine readers. The only thing I ask in return is to send anyone and everyone you know who uses .config files -- which should be pretty much anyone writing .NET code -- to this page and save them months of digging through code, internet forums and blog posts to learn it all. :) Topics of configurationThe goal of this article is to cover all the major aspects of .NET 2.0 configuration, as well as expose some of the more closely guarded (read: undocumented... for whatever reasons) secrets that could save you a lot of hassle and time. We'll start with an overview of the core namespace that exposes all of this custom configuration madness. Then we'll move on to specific implementations and uses for custom configuration. The topics of discussion are as follows:
The namespace: System.ConfigurationThe heart of the new .NET 2.0 configuration gem is the The Beyond the The base types
The support types
The validation types
The converter types
Pre-made configuration sections
Premade configuration collections *4
Notes:
In addition to all of the classes described above, you will also find other support types used by The Object-Model configuration conceptBefore we continue on to creating some custom configuration goodness, it is important to learn about the concept of object-model configuration. The .NET 2.0 configuration system ultimately provides a set of objects that represent your configuration settings structure and provide strongly-typed access to your configuration data. This contrasts with the more common methods of storing and retrieving configuration in XML files, which usually entails reading values through the use of a DOM or streaming reader into variables, and writing changes back through the use of a DOM or streaming writer. In the more advanced configuration systems I personally have written, some caching facilities were included to speed up reading and writing configuration values. Making a configuration file highly customizable while maintaining good performance was always a point of difficulty. Such monolithic methods of handling XML configuration data are no longer needed with .NET 2.0's configuration object model. As a simple example, take the ConnectionStringSettingsCollection -> [implicitly created] ConnectionStringSettings -> <add name="MyConnection" connectionString="blahblah"> Accessing the connection string MyConnection is as simple as the following code:
string myConnectionString =
ConfigurationManager.ConnectionStrings["MyConnection"].ConnectionString;
The SystemWebSectionGroup -> <system.web>AuthenticationSection -> <authentication>AuthorizationSection -> <authorization>CustomErrorsSection -> <customErrors>CustomErrorsCollection -> [implicitly created]CustomError -> <error statusCode="404" redirect="...">HttpModulesSection -> <httpModules>HttpModuleActionCollection -> [implicitly created]HttpModuleAction -> <add name="myModule" type="...">HttpHandlersSection -> <httpHandlers>HttpHandlerActionCollection -> [implicitly created]HttpModuleActionCollection -> <add verb="*" type="..." path="...">This is only a small subset of the full set of configuration sections available for the The .NET 2.0 configuration system handles the parsing, validation, security and population of this object model for you. Aside from writing a custom configuration section, which is fairly simple, this completely removes the need to think about XML when you consume your configuration in an application. Not only that, but this nicely packaged, strongly-typed, secure object model wrapped around your configuration is directly accessible anywhere in your application without needing to worry about finding or storing file paths to custom XML configuration files in places like the registry. Gone are the days of hassling with inconsistent, inflexible or poorly performing custom configuration managers; hopefully those days are gone for good. Writing a basic configuration sectionIf everything before this has intimidated you, don't worry. Writing the code to provide a custom configuration section to your application is very straightforward and simple. The vast bulk of dealing with XML ugliness, security checks, type conversion, etc., is handled by existing code in the .NET 2.0 Framework. Thanks to the set of base classes available in #region Using Statements
using System;
using System.Configuration;
#endregion
namespace Examples.Configuration
{
/// <summary>
/// An example configuration section class.
/// </summary>
public class ExampleSection: ConfigurationSection
{
#region Constructors
static ExampleSection()
{
// Predefine properties here
}
#endregion
// Declare static property fields here
// Declare expose properties here
}
}
Once you have the start of a configuration section class, you will need to define the valid configuration properties. A configuration property, represented by the Let's start filling in the code to make our little example section function. Start by defining static property fields, then create those fields in the static class constructor. Finally, expose the configuration data through C# code properties. The full source code for the custom configuration section should look something like this when it's done: #region Using Statements
using System;
using System.Configuration;
#endregion
namespace Examples.Configuration
{
/// <summary>
/// An example configuration section class.
/// </summary>
public class ExampleSection: ConfigurationSection
{
#region Constructors
/// <summary>
/// Predefines the valid properties and prepares
/// the property collection.
/// </summary>
static ExampleSection()
{
// Predefine properties here
s_propString = new ConfigurationProperty(
"stringValue",
typeof(string),
null,
ConfigurationPropertyOptions.IsRequired
);
s_propBool = new ConfigurationProperty(
"boolValue",
typeof(bool),
false,
ConfigurationPropertyOptions.None
);
s_propTimeSpan = new ConfigurationProperty(
"timeSpanValue",
typeof(TimeSpan),
null,
ConfigurationPropertyOptions.None
);
s_properties = new ConfigurationPropertyCollection();
s_properties.Add(s_propString);
s_properties.Add(s_propBool);
s_properties.Add(s_propTimeSpan);
}
#endregion
#region Static Fields
private static ConfigurationProperty s_propString;
private static ConfigurationProperty s_propBool;
private static ConfigurationProperty s_propTimeSpan;
private static ConfigurationPropertyCollection s_properties;
#endregion
#region Properties
/// <summary>
/// Gets the StringValue setting.
/// </summary>
[ConfigurationProperty("stringValue", IsRequired=true)]
public string StringValue
{
get { return (string)base[s_propString]; }
}
/// <summary>
/// Gets the BooleanValue setting.
/// </summary>
[ConfigurationProperty("boolValue")]
public bool BooleanValue
{
get { return (bool)base[s_propBool]; }
}
/// <summary>
/// Gets the TimeSpanValue setting.
/// </summary>
[ConfigurationProperty("timeSpanValue")]
public TimeSpan TimeSpanValue
{
get { return (TimeSpan)base[s_propTimeSpan]; }
}
/// <summary>
/// Override the Properties collection and return our custom one.
/// </summary>
protected override ConfigurationPropertyCollection Properties
{
get { return s_properties; }
}
#endregion
}
}
Once you are done, that's it. This custom configuration section is ready to go. If you prefer to write less code, you could drop the static constructor and the static fields and store each value in the default properties collection by a string key. This would lead to less code required for the configuration section itself, but would be less defined in requirements overall. The properties above could be replaced with the following, and the static fields and constructor could be removed. That, by the way, is the purely declarative method: #region Properties
/// <summary>
/// Gets the StringValue setting.
/// </summary>
[ConfigurationProperty("stringValue", IsRequired=true)]
public string StringValue
{
get { return (string)base["stringValue"]; }
}
/// <summary>
/// Gets the BooleanValue setting.
/// </summary>
[ConfigurationProperty("boolValue")]
public bool BooleanValue
{
get { return (bool)base["boolValue"]; }
}
/// <summary>
/// Gets the TimeSpanValue setting.
/// </summary>
[ConfigurationProperty("timeSpanValue")]
public TimeSpan TimeSpanValue
{
get { return (TimeSpan)base["timeSpanValue"]; }
}
#endregion
A quick note about what a Using a custom configuration sectionNow that you have a class written for your custom configuration section, you need to define the custom section in an App.config file. You must and add the necessary XML, so that it can be parsed by the .NET 2.0 configuration system. The App.config changes required to make this <configuration>
<configSections>
<section name="example" type="Examples.Configuration.ExampleSection,
Examples.Configuration" />
</configSections>
<example
stringValue="A sample string value."
boolValue="true"
timeSpanValue="5:00:00"
/>
</configuration>
A quick explanation of the Finally, you can consume your custom configuration in code using the private string m_string;
private bool m_bool;
private TimeSpan m_timespan;
void GetExampleSettings()
{
ExampleSection section = ConfigurationManager.GetSection("example")
as ExampleSection;
if (section != null)
{
m_string = section.StringValue;
m_bool = section.BooleanValue;
m_timespan = section.TimeSpanValue;
}
}
A most important note here is the name chosen to represent the root element of a custom configuration section. In the example App.config file, the section was defined with the name "example." Since the code that loads the section looks for the name 'example' in the call to Adding custom elementsBy default, all configuration properties defined in a custom configuration section will be represented by attributes in a .config file. This will not always be sufficient, however, and a more complex XML structure consisting of a mix of attributes and elements may be required. Fear not: the .NET 2.0 configuration system fully supports custom configuration elements, each with their own configuration properties that can be attributes or more nested elements. To create a custom configuration element, simply write a class that derives from Let's continue by nesting a custom element inside of our #region Using Statements
using System;
using System.Configuration;
#endregion
namespace Examples.Configuration
{
/// <summary>
/// An example configuration element class.
/// </summary>
public class NestedElement: ConfigurationElement
{
#region Constructors
/// <summary>
/// Predefines the valid properties and prepares
/// the property collection.
/// </summary>
static NestedElement()
{
// Predefine properties here
s_propDateTime = new ConfigurationProperty(
"dateTimeValue",
typeof(DateTime),
null,
ConfigurationPropertyOptions.IsRequired
);
s_propInteger = new ConfigurationProperty(
"integerValue",
typeof(int),
0,
ConfigurationPropertyOptions.IsRequired
);
s_properties = new ConfigurationPropertyCollection();
s_properties.Add(s_propDateTime);
s_properties.Add(s_propInteger);
}
#endregion
#region Static Fields
private static ConfigurationProperty s_propDateTime;
private static ConfigurationProperty s_propInteger;
private static ConfigurationPropertyCollection s_properties;
#endregion
#region Properties
/// <summary>
/// Gets the DateTimeValue setting.
/// </summary>
[ConfigurationProperty("dateTimeValue", IsRequired=true)]
public DateTime StringValue
{
get { return (DateTime)base[s_propDateTime]; }
}
/// <summary>
/// Gets the IntegerValue setting.
/// </summary>
[ConfigurationProperty("integerValue")]
public int IntegerValue
{
get { return (int)base[s_propInteger]; }
}
/// <summary>
/// Override the Properties collection and return our custom one.
/// </summary>
protected override ConfigurationPropertyCollection Properties
{
get { return s_properties; }
}
#endregion
}
}
Adding this element to the public class ExampleSection: ConfigurationSection
{
#region Constructors
/// <summary>
/// Predefines the valid properties and prepares
/// the property collection.
/// </summary>
static ExampleSection()
{
// Create other properties...
s_propElement = new ConfigurationProperty(
"nestedElement",
typeof(NestedElement),
null,
ConfigurationPropertyOptions.IsRequired
);
s_properties = new ConfigurationPropertyCollection();
// Add other properties...
s_properties.Add(s_propElement);
}
#endregion
#region Static Fields
private static ConfigurationProperty s_propElement;
// Other static fields...
#endregion
#region Properties
// ...
/// <summary>
/// Gets the NestedElement element.
/// </summary>
[ConfigurationProperty("nestedElement")]
public NestedElement Nested
{
get { return (NestedElement)base[s_propElement]; }
}
// ...
#endregion
}
Finally, using the element in our XML config file is as simple as adding a single <configuration>
<configSections>
<section name="example" type="Examples.Configuration.ExampleSection,
Examples.Configuration" />
</configSections>
<example
stringValue="A sample string value."
boolValue="true"
timeSpanValue="5:00:00"
>
<nestedElement
dateTimeValue="10/16/2006"
integerValue="1"
/>
</example>
</configuration>
Using the new nested element is again extremely simple, as a private string m_string;
private bool m_bool;
private TimeSpan m_timespan;
private DateTime m_datetime;
private int m_int;
void GetExampleSettings()
{
ExampleSection section = ConfigurationManager.GetSection("example")
as ExampleSection;
if (section != null)
{
m_string = section.StringValue;
m_bool = section.BooleanValue;
m_timespan = section.TimeSpanValue;
m_datetime = section.Nested.DateTimeValue;
m_int = section.Nested.IntegerValue;
}
}
Each configuration section can be composed of any number of attributes and elements, nested as deep as required to meet the needs of your application. It's always a good idea to conform to the same XML best practices with custom configuration sections as it is for any other use of XML. As a general rule, data sets or large volumes of information should not be stored in a custom configuration section. We will discuss the reasons for this in the advanced sections. These are "configuration" sections and should be used to store structured application configuration information. Adding element collectionsIn the previous section, we created a nested element inside of a configuration section element. The nested element is limited to only one instance and it must appear in the element that defines it in code. Creating collections or lists of elements requires a different and slightly more complex approach. To create a collection or list of configuration elements in a configuration file, you must create a class that derives from Anyone who has used the Since ARC map collections are the default type, let's create one and add it to our example configuration section from above. The code to create an element collection is slightly more complicated than that of a configuration section or single element, but is still very simple over all. The element collection code below follows a standard pattern used in most element collections in the .NET 2.0 Framework: [ConfigurationCollection(typeof(ThingElement),
CollectionType=ConfigurationElementCollectionType.AddRemoveClearMap)]
public class ExampleThingElementCollection: ConfigurationElementCollection
{
#region Constructors
static ExampleThingElementCollection()
{
m_properties = new ConfigurationPropertyCollection();
}
public ExampleThingElementCollection()
{
}
#endregion
#region Fields
private static ConfigurationPropertyCollection m_properties;
#endregion
#region Properties
protected override ConfigurationPropertyCollection Properties
{
get { return m_properties; }
}
public override ConfigurationElementCollectionType CollectionType
{
get { return ConfigurationElementCollectionType.AddRemoveClearMap; }
}
#endregion
#region Indexers
public ThingElement this[int index]
{
get { return (ThingElement)base.BaseGet(index); }
set
{
if (base.BaseGet(index) != null)
{
base.BaseRemoveAt(index);
}
base.BaseAdd(index, value);
}
}
public ThingElement this[string name]
{
get { return (ThingElement)base.BaseGet(name); }
}
#endregion
#region Overrides
protected override ConfigurationElement CreateNewElement()
{
return new ThingElement();
}
protected override object GetElementKey(ConfigurationElement element)
{
return (element as ThingElement).Name;
}
#endregion
}
It is generally necessary to provide an indexer accessible by numeric index. An indexer accessible by an element's key is also a useful convenience. In the case of this example, the key is a string name. The two overrides, Our collection is not quite done yet, as we need something to collect. For our example, that something is the public class ThingElement: ConfigurationElement
{
#region Constructors
/// <summary>
/// Predefines the valid properties and prepares
/// the property collection.
/// </summary>
static ThingElement()
{
// Predefine properties here
s_propName = new ConfigurationProperty(
"name",
typeof(string),
null,
ConfigurationPropertyOptions.IsRequired
);
s_propType = new ConfigurationProperty(
"type",
typeof(string),
"Normal",
ConfigurationPropertyOptions.None
);
s_propColor = new ConfigurationProperty(
"color",
typeof(string),
"Green",
ConfigurationPropertyOptions.None
);
s_properties = new ConfigurationPropertyCollection();
s_properties.Add(s_propName);
s_properties.Add(s_propType);
s_properties.Add(s_propColor);
}
#endregion
#region Static Fields
private static ConfigurationProperty s_propName;
private static ConfigurationProperty s_propType;
private static ConfigurationProperty s_propColor;
private static ConfigurationPropertyCollection s_properties;
#endregion
#region Properties
/// <summary>
/// Gets the Name setting.
/// </summary>
[ConfigurationProperty("name", IsRequired=true)]
public string Name
{
get { return (string)base[s_propName]; }
}
/// <summary>
/// Gets the Type setting.
/// </summary>
[ConfigurationProperty("type")]
public string Type
{
get { return (string)base[s_propType]; }
}
/// <summary>
/// Gets the Type setting.
/// </summary>
[ConfigurationProperty("color")]
public string Color
{
get { return (string)base[s_propColor]; }
}
/// <summary>
/// Override the Properties collection and return our custom one.
/// </summary>
protected override ConfigurationPropertyCollection Properties
{
get { return s_properties; }
}
#endregion
}
Our <configuration>
<configSections>
<section name="example" type="Examples.Configuration.ExampleSection,
Examples.Configuration" />
</configSections>
<example
stringValue="A sample string value."
boolValue="true"
timeSpanValue="5:00:00"
>
<nestedElement
dateTimeValue="10/16/2006"
integerValue="1"
/>
<things>
<add name="slimy" type="goo" />
<add name="metal" type="metal" color="silver" />
<add name="block" type="wood" color="tan" />
</things>
</example>
</configuration>
Advanced element collectionsIn the previous section, you learned how to create a standard ARC map type of collection, which is the default. There are a total of four types of collections, two variations of two types: In the previous example, we created a collection to list things and each thing was added with an [ConfigurationCollection(typeof(ThingElement), AddItemName="thing",
CollectionType=ConfigurationElementCollectionType.BasicMap)]
public class ExampleThingElementCollection: ConfigurationElementCollection
{
#region Constructors
// ...
#endregion
#region Fields
// ...
#endregion
#region Properties
// ...
public override ConfigurationElementCollectionType CollectionType
{
get { return ConfigurationElementCollectionType.BasicMap; }
}
protected override string ElementName
{
get { return "thing"; }
}
#endregion
#region Indexers
// ...
#endregion
#region Methods
// ...
#endregion
#region Overrides
// ...
#endregion
}
These simple changes will update our collection class to a <configuration>
<configSections>
<section name="example" type="Examples.Configuration.ExampleSection,
Examples.Configuration" />
</configSections>
<example
stringValue="A sample string value."
boolValue="true"
timeSpanValue="5:00:00"
>
<nestedElement
dateTimeValue="10/16/2006"
integerValue="1"
/>
<things>
<thing name="slimy" type="goo" />
<thing name="metal" type="metal" color="silver" />
<thing name="block" type="wood" color="tan" />
</things>
</example>
</configuration>
Sometimes, when you have a configuration hierarchy and settings cascade from a parent web.config to a child web.config, you need to control which elements appear first in a collection. By default, all elements will be added to a collection in the order in which they are encountered. This order is not particularly well defined when working with multiple web.config files that have been merged. By using an alternate map type, you are given some control over the order of items by forcing all inherited elements to be listed last. A cascade of three web.config files will list the items in the order they are read from each file, starting with the lowest level web.configs first, followed by the parent web.configs and ending with the root web.configs last. Both ARC maps and basic maps support alternates that behave this way, with the added caveat that basic maps prevent child web.config files from modifying items added by their parents. Custom configuration section groupsDepending on the kind of projects you work on or the specific configuration needs of your application, you may find it useful to group configuration sections. The .NET 2.0 configuration features provide a facility to accomplish this, which we touched on before in our discussion of the public sealed class ExampleSectionGroup: ConfigurationSectionGroup
{
#region Constructors
public ExampleSectionGroup()
{
}
#endregion
#region Properties
[ConfigurationProperty("example")]
public ExampleSection Example
{
get { return (ExampleSection)base.Sections["example"]; }
}
[ConfigurationProperty("another")]
public AnotherSection Another
{
get { return (AnotherSection)base.Sections["another"]; }
}
#endregion
}
Once we have our section group coded, we need to define it in our .config file. This is done very similarly to defining a normal section, only with an added level of hierarchy. It is important to note that the configuration sections <configuration>
<configSections>
<sectionGroup name="example.group"
type="Examples.Configuration.ExampleSectionGroup,
Examples.Configuration">
<section name="example" type="Examples.Configuration.ExampleSection,
Examples.Configuration" />
<section name="another" type="Examples.Configuration.AnotherSection,
Examples.Configuration" />
</sectionGroup>
</configSections>
<example.group>
<example
stringValue="A sample string value."
boolValue="true"
timeSpanValue="5:00:00"
>
<nestedElement
dateTimeValue="10\16\2006"
integerValue="1"
/>
<things>
<thing name="slimy" type="goo" />
<thing name="metal" type="metal" color="silver" />
<thing name="block" type="wood" color="tan" />
</things>
</example>
<another value="someValue" />
</example.group>
</configuration>
This configuration section group does just that; it groups two configuration sections together. In addition, it provides a central point of access to those sections. Once we have a reference to our private string m_string;
private bool m_bool;
private TimeSpan m_timespan;
private DateTime m_datetime;
private int m_int;
void GetExampleSettings()
{
Configuration config =
ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None);
ExampleSectionGroup group = config.GetSectionGroup("example.group")
as Exam | ||||||||||||||||||||