Click here to Skip to main content
15,881,938 members
Articles / Programming Languages / C#

Implementing Typed Configuration

Rate me:
Please Sign up or sign in to vote.
0.00/5 (No votes)
25 Aug 2010CPOL10 min read 14.8K   80   6  
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:

XML
<RowName Name="" Property1="" Property2=""/>
<RowName Name="" Property1="" Property2=""/>

Every class that will represent each line must be derived from <code>Sarafian.Framework.General.Configuration.Export.ExportRowBase. Every attribute must be implemented as a property. If string 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:

XML
<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:

XML
<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:

C#
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:

C#
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:

XML
<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:

C#
public const string Type1Key = "Type1";
public Type Type1
{
	get { return base[Type1Key]; }
}

and here is the code for using it:

C#
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:

XML
<section name="Section3"
   type="Example1.SectionHandlers.Export.Section3Export,Example1"/>

Section3 is declared as:

C#
public class Section3Handler :
  Sarafian.Framework.General.Configuration.CustomSectionHandler<Export.Section3RowsList> 

and Section3Row is used to represent each line of the configuration:

XML
<Section3Row Name="Section3Row1" Property1="Value1" Property2="Value2"/>
<Section3Row Name="Section3Row2" Property1="Value1"/>
In order for Section3Row to successfully represent this XML, it must declare the following properties:
C#
public string Property1 { get; set; }

[DefaultValue("Property2_Default")]
public string Property2 { get; set; }

Using this section handler is as easy as:

C#
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:

XML
<section name="Section4" type="Example1.SectionHandlers.Export.Section4Export,Example1"/>

Section4 is declared as:

C#
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:

XML
  <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:

C#
public int Property1 { get; set; }
public enExamples Property2 { get; set; }
C#
public enum enExamples
{
    Example1,
    Example2
}

In order for Section4Row to successfully represent this XML, it must declare the following properties:

C#
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:

C#
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;
    }
}
C#
[TypeConverter(typeof(Class1FromConfigConverter))]
public class Class1FromConfig
{
    public string Left2 { get; set; }
    public string Left5 { get; set; }

}

Using this section handler is as easy as:

C#
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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Team Leader ALGOSYSTEMS
Greece Greece
I live in Athens Greece and currently I am working with Business scale application with .NET latest technologies

I've been developing applications for personal and friends usage with C++ using majorly Borland's various IDEs since 1994.
In 2002 I began working for an R&D institute where I was introduced to C# which I worships ever since.

I love core application development and I would like to publish more articles here and on my blog, but there is not enough time to do so.

I usualy "waste" my spare time watching sitcoms, preferable SCI-FI.
I would like to play chess but I can't find any real world players to hang out with.

Comments and Discussions

 
-- There are no messages in this forum --