Implementing Typed Configuration
Framework and example source code for creating typed class for configuration sections
Introduction
Configuration in .NET is a very powerful tool. But mostly it is untyped. That means you have to depend on volatile key names that can potentially create bugs in your application.
For me, driving an engine based on class declarations instead of writing the actual code is an implementation method that I prefer and admire. There are some negatives but this method is extremely powerful and easy to maintain in the long run because it is implemented in a common place.
Background
I find modular abstracted programming to be a very good way for developing applications. In order to do so, Framework is required to be developed and maintained. Every functionality that can be repeated or generalized, at some point becomes part of a framework library.
As a developer, I like typed code as much as possible, so at some point I saw the need for typed classes for equivalent configuration sections.
On the attached file, there is the source code for the framework implementing the desired functionality and the source code for various examples that I will analyze below.
Framework
The uploaded code is part of a multi project framework library. So for this article, I stripped it down for the classes that are needed. There are two assemblies.
Sarafian.Framework.General
is part of a general purpose project. In this attachment, only some code for helping with Reflection is included.
Sarafian.Framework.General.Configuration
is the assembly that provides the infrastructure for this article.
In order to create frameworks with typed classes, one must understand and utilize a lot of Generics.
So, in order to understand the framework code, you must be proficient enough with Generics. If you just want to consume it, then you simply need to understand generics at the level of typed collections.
The framework basically provides five kinds of section handlers. A section handler is the class that is ultimately responsible for providing a typed representation of a configuration section.
In the using the code section of the article, all section handlers Section1Handler
, Section2Handler
, Section3Handler
and Section4Handler
derive from the base classes in this assembly. The base class is chosen based on the desired schema of the configuration section. All handlers abstract the functionality required for accessing the System.Configuration
and derive from Sarafian.Framework.General.Configuration.SectionHandler
.
If a section is not always defined in the application's configuration, then the related section handler class must specifically tell the engine about it by overriding the RequireSection
property. The SectionFound
property tells whether the section was found in the applications configuration.
Sarafian.Framework.General.Configuration.StringSectionHandler
This is based on the NameValueCollection
configuration section schema. It basically extracts each pair in a Dictionary
that can be accessed from the derived class. The derived class, need only to declare the properties with their respected types as long as a conversion is provided when the type is other System.String
.
In this case, the section won't be read and all Properties must take into account that their respected key may not be defined, thus providing a default value.
Sarafian.Framework.General.Configuration.TypeSectionHandler
This is the same as Sarafian.Framework.General.Configuration.StringSectionHandler
, but it converts all entries to System.Type
. Some additional functionally for creating instances of these types is provided.
Sarafian.Framework.General.Configuration.CustomSectionHandler
This handler is for extracting a custom schema based configuration section. In order to so, a custom export class must be created, to handle the XML of the section. That class must be provided in the section declaration in configuration file. The derived class must provide as generic parameter the type of the object that will actually represent the typed version for the section's schema.
In this framework, there are two ready export mechanisms provided that I find most commonly used.
Row Schema
This schema is like this:
<RowName Name="" Property1="" Property2=""/>
<RowName Name="" Property1="" Property2=""/>
Every class that will represent each line must be derived from
. Every attribute must be implemented as a property. If Sarafian.Framework.General.Configuration.Export.ExportRowBas
estring
can be automatically converted to the property type it will be done, otherwise a TypeConverter
must be also implemented. From my experience, only classes and structs are not automatically converted from System.String
. If a property is not found as an attribute in the XML, then its DefaultValue
attribute value will be used.
The type that can read this schema must be derived from Sarafian.Framework.General.Configuration.Export.SectionExportByRow
and must provide two generics parameters, one that is a dictionary based list of the type that represents each row and the type row. The constructor of this exporting class must only feed the base constructor with the element name in the XML, such as RowName
.
Since the output is going to be a dictionary based list of the type related to each row, the Name
property in each line must be unique.
GroupRow Schema
This schema is like the row schema, but it supports level two nesting. Group is the first level element and row is the second in the XML.
Like this:
<GroupRowName Name="">
<RowName Name="" Property1="" Property2=""/>
<RowName Name="" Property1="" Property2=""/>
</GroupRowName >
<GroupRowName Name="">
<RowName Name="" Property1="" Property2=""/>
</GroupRowName >
Every class that will represent each group line must be derived from Sarafian.Framework.General.Configuration.Export.ExportGroupRowBase
. Every attribute must be implemented as a property. For row lines, you must follow the rules of the row schema.
The type that can read this schema must be derived from Sarafian.Framework.General.Configuration.Export.SectionExportByGroupRow
and must provide three generics parameters, one that is a dictionary based list of the type that represents each group line, the type for the group line and the type row line. The constructor of this exporting class must only feed the base constructor with the group and row element names in the XML, such as GroupRowName
and RowName
.
Since the output is going to be a dictionary based list of the type related to each group row, the Name
property in each group line must be unique. Within each group level, the name
attribute must be also unique.
Children of the SectionExportByGroupRow
provide through generic parameters the types that correspond to the row lines. These types must be derived from the SectionExportByRow
.
If the type for the row lines, implements Sarafian.Framework.General.Configuration.Export.IGroupOwner
then each instance can know the instance of its level 1 group parent.
Custom Schema
For other schemas, an export class must be defined that derives from Sarafian.Framework.General.Configuration.Export.CustomSectionExport
.
Helper Functionality
Reloading Files
When any handler is instantiated, the extraction should occur once, and every data extracted within any type is stored within the handler itself. So if you change a config file and you are not under an IIS domain, then the files must be reloaded. This functionality is provided from Sarafian.Framework.General.Configuration.SectionManager.ReloadAllSections()
.
Path of the Config File
There were some projects that I needed to acquire the path of the root config file, whether it was the web.config
or an exe
one. This functionality is provided by Sarafian.Framework.General.Configuration.ConfigFile.GetConfigFile()
.
Using the Code
Example1
is the project that contains examples for various implementations of the Typed Configuration Framework.
There are four different implementations, based on four different schemas of configuration sections.
All sections are implemented within separate config files that follow the pattern of Section?.Example.Config
.
Section1 Example
Section1
uses a classic NameValueCollection
schema. So the section declaration is:
<section name="Section1" type="System.Configuration.NameValueSectionHandler"/>
The purpose here is to have a typed representation for the various pairs than can be possibly found in the file.
Section1Handler
is the section handler. It is derived from Sarafian.Framework.General.Configuration.StringSectionHandler
and basically it just needs to declare the related properties. Each property can have any type you like, as long as you implement the conversion from string
to that type in the getter method of the property.
If a property is not always found in the configuration file, then in the get
method you must first check for its existence and if not found, throw an exception or return your desired default value.
Here is the source:
public const string Property1Key = "Property1";
public string Property1
{
get
{
if (!base.ContainsName(Property1Key))
{
return "Default_Property";
}
return base[Property1Key];
}
}
public const string Property2Key = "Property2";
public string Property2
{
get
{
return base[Property2Key];
}
}
public const string Property3Key = "Property3";
public int Property3
{
get
{
return Convert.ToInt32(base[Property3Key]);
}
}
Notice how Property1
returns its default value, and how Property3
has a type other than String
. If for some reason, the requested key is used without it being provided in the config, then an exception will be thrown.
Using this sections values is as easy as:
Console.WriteLine(String.Format("Property1={0}", Section1Handler.Instance.Property1));
Console.WriteLine(String.Format("Property2={0}", Section1Handler.Instance.Property2));
Console.WriteLine(String.Format("Property3={0}", Section1Handler.Instance.Property3));
and the result is:
Property1=Value1
Property2=Value2
Property3=-1
Section2 Example
Section2
also uses a classic NameValueCollection
schema. So the section declaration is:
<section name="Section1" type="System.Configuration.NameValueSectionHandler"/>
The purpose here is to have a typed representation for the various pairs than can be possibly found in the file. In this Section though, we are interested in getting types from a Configuration.
Section2Handler
is the section handler. It is derived from Sarafian.Framework.General.Configuration.
TypeSectionHandler
and basically it just needs to declare the related properties. Each property must be of type Type
, and as Section1
, if a property is not always found in the config, it must be checked.
Here is the source:
public const string Type1Key = "Type1";
public Type Type1
{
get { return base[Type1Key]; }
}
and here is the code for using it:
Console.WriteLine(String.Format("Type1={0}", Section2Handler.Instance.Type1));
and the result is:
Type1=System.String
Section3 Example
Section3
uses a Row Schema that is handled by Example1.SectionHandlers.Export.Section3Export
. So the section declaration is:
<section name="Section3"
type="Example1.SectionHandlers.Export.Section3Export,Example1"/>
Section3
is declared as:
public class Section3Handler :
Sarafian.Framework.General.Configuration.CustomSectionHandler<Export.Section3RowsList>
and Section3Row
is used to represent each line of the configuration:
<Section3Row Name="Section3Row1" Property1="Value1" Property2="Value2"/>
<Section3Row Name="Section3Row2" Property1="Value1"/>
Section3Row
to successfully represent this XML, it must declare the following properties: public string Property1 { get; set; }
[DefaultValue("Property2_Default")]
public string Property2 { get; set; }
Using this section handler is as easy as:
foreach (Section3Row s3Row in Section3Handler.Instance.Values.Values)
{
Console.WriteLine(String.Format("Section3Row Name={0}
Property1={1} Property2={2}", s3Row.Name, s3Row.Property1, s3Row.Property2));
}
and the result is:
Section3Row Name=Section3Row1 Property1=Value1 Property2=Value2
Section3Row Name=Section3Row2 Property1=Value1 Property2=
Section4 Example
Section4
uses a Group Row Schema that is handled by Example1.SectionHandlers.Export.Section4Export
. So the section declaration is:
<section name="Section4" type="Example1.SectionHandlers.Export.Section4Export,Example1"/>
Section4
is declared as:
public class Section4Handler :
Sarafian.Framework.General.Configuration.CustomSectionHandler<Export.Section4RowsList>
and Section4GroupRow
is used to represent each group line and Section4Row
each row line of the configuration:
<Section4GroupRow Name="Section4GroupRow1">
<Section4Row Name="Section4Row11" Class1="123456789"/>
<Section4Row Name="Section4Row12" Class1="23456789"/>
</Section4GroupRow>
<Section4GroupRow Name="Section4GroupRow2">
<Section4Row Name="Section4Row21" Class1="3456789"/>
</Section4GroupRow>
In order for Section4GroupRow
to successfully represent this XML, it must declare the following properties:
public int Property1 { get; set; }
public enExamples Property2 { get; set; }
public enum enExamples
{
Example1,
Example2
}
In order for Section4Row
to successfully represent this XML, it must declare the following properties:
public Class1FromConfig Class1 { get; set; }
For the Section4GroupRow
, I choose to use an enumeration for a Property
Type. As you can see, an enumeration can be used like other common types.
For the Section4Row
, I chose to use a class for a Property
Type. Class1FromConfig
is a simple class with properties Left2
and Left5
that are the 2 and 5 most left characters of the provided string
value. In order for this to work, Class1FromConfig
must be accompanied by a TypeConverter
Class1FromConfigConverter
.
Here is the code:
public class Class1FromConfigConverter : TypeConverter
{
public override bool CanConvertFrom
(ITypeDescriptorContext context, Type sourceType)
{
return sourceType == typeof(String);
}
public override object ConvertFrom(ITypeDescriptorContext context,
CultureInfo culture, object value)
{
Class1FromConfig c1 = new Class1FromConfig();
c1.Left2 = value.ToString().Substring(0,2);
c1.Left5 = value.ToString().Substring(0,5);
return c1;
}
}
[TypeConverter(typeof(Class1FromConfigConverter))]
public class Class1FromConfig
{
public string Left2 { get; set; }
public string Left5 { get; set; }
}
Using this section handler is as easy as:
foreach (Section4GroupRow s4GroupRow in Section4Handler.Instance.Values.Values)
{
Console.WriteLine(String.Format("Section4GroupRow Name={0}
Property1={1} Property2={2}", s4GroupRow.Name, s4GroupRow.Property1,
s4GroupRow.Property2));
foreach (Section4Row s4Row in s4GroupRow.Values)
{
Console.WriteLine(String.Format(" Section4Row Name={0}
Class1.Left2={1} Class1.Left5={2}", s4Row.Name,
s4Row.Class1.Left2, s4Row.Class1.Left5));
}
}
and the result is:
Section4GroupRow Name=Section4GroupRow1 Property1=0 Property2=Example1
Section4Row Name=Section4Row11 Class1.Left2=12 Class1.Left5=12345
Section4Row Name=Section4Row12 Class1.Left2=23 Class1.Left5=23456
Section4GroupRow Name=Section4GroupRow2 Property1=0 Property2=Example1
Section4Row Name=Section4Row21 Class1.Left2=34 Class1.Left5=34567
Overall
The overall output is:
Section1
Property1=Value1
Property2=Value2
Property3=-1
Section2
Type1=System.String
Section3
Section3Row Name=Section3Row1 Property1=Value1 Property2=Value2
Section3Row Name=Section3Row2 Property1=Value1 Property2=
Section4
Section4GroupRow Name=Section4GroupRow1 Property1=0 Property2=Example1
Section4Row Name=Section4Row11 Class1.Left2=12 Class1.Left5=12345
Section4Row Name=Section4Row12 Class1.Left2=23 Class1.Left5=23456
Section4GroupRow Name=Section4GroupRow2 Property1=0 Property2=Example1
Section4Row Name=Section4Row21 Class1.Left2=34 Class1.Left5=34567
Points of Interest
I use the Singleton pattern not in the examples only but in our applications also. With the above naming and configuration framework, we have achieved a unified structured way of accessing configuration files.
Row and Group Row schema based configuration, generally seem to provide all the custom functionality we need, without having the need to write any code that reads XML into instances. I realize that the dictionary as a means of list might be a wrong choice, since the Name
property provides the indexing mechanism or just because you might not like to have one in your XML lists but I find this to be minor defect when you can almost get by just the means of copy and paste a ready result.
As I said in the introduction, driving an engine based on your class declarations is my preference. With this engine, you achieve that by translating a schema that is compatible with the above into typed instances, just by declaring properties in them.
History
- This is version 1.0