Introduction
Recently I have been working on a task that required a lot of flexibility in storing and accessing configuration settings. I have searched the web and found plenty
of solutions and examples but none of them really satisfied my requirements. This forced me to give up the search and to consider writing my own version. Once I got to
the point that it appeared to be working, I decided to share it in case someone might find it more useful than the rest of the options out there.
Background
In order to finish the task, I had to make sure the following conditions were met in order to make it useful:
- User should be able to easily change or choose how and where configuration data is stored without affecting the configurable objects.
- Accessing and storing configuration should be very simple and transparent to the user but at the same time reliable.
- Types of any of the child objects are not known to the process that initiates loading of configuration and child objects are created depending on specific configurations.
A potential solution came to mind when I was working on a web project and used View State to store some values between page post backs.
It reminded me that strings can represent any data including binary. They are relatively easy to use and can be stored pretty much anywhere - XML file,
database, or even binary file. And just like we operate with strings in XML, we can access and store data as strings and convert it to appropriate data types when loading.
In order to be able to manage complex configurations, I had to come up with two concepts which we’ll call context and value. Context is simply
a container that holds a collection of other contexts and values, and values are just simple string values. The loading and saving is done using two methods
that perform reading and writing configuration to the appropriate context which is similar to the IXmlSerializable
interface.
Using the code
You will need to implement the IConfigurationElement
interface so that class configuration can be saved and loaded. The interface is similar
to the IXmlSerializable
interface and includes two methods: ReadConfiguration
and WriteConfiguration
.
As their names suggest, one is used to read configuration and the other to write.
public interface IConfigurationElement
{
void ReadConfiguration(IConfigurationContext context);
void WriteConfiguration(IConfigurationContext context);
}
Since we are dealing with strings only, we can use indexers to load and save a value. It will be the user’s responsibility to convert a string to the appropriate data type
when reading, and back when writing. For example:
context["DateOfBirth"] = DateOfBirth.ToShortDateString();
DateTime dt;
DateTime.Parse(context["DateOfBirth"], out dt);
Question now, what do we do if we want to save a subset of configuration values? In this case, you need to obtain a context object from the current context by ether creating
a new one or getting an existing one. For this, IConfigurationContext
provides three methods: Get
, GetAll
, and Create
.
The Get
method accepts a string parameter that specifies the name of the parameter and returns a list of contexts. GetAll
will return all contexts
associated with the current context and you can access the context name by using the property Name
. Accordingly, the Create
method will create
a new context with the provided name.
Here is the full interface definition:
public interface IConfigurationContext
{
IEnumerable<IConfigurationContext> Get(string contextName);
IEnumerable<IConfigurationContext> GetAll();
string this[string name] { get; set; }
string Name { get; }
IConfigurationContext Create(string contextName);
}
Below is an example of accessing and storing a person's name (FirstName
and LastName
) and postal address (MailingAddress
).
The example below does not include the implementation of the PostalAddress
class but it is available
in the attached source code together with other validation logic not shown here.
public class Person : IConfigurationElement
{
public string FirstName { get; set; }
public string LastName { get; set; }
public PostalAddress MailingAddress { get; set; }
public void WriteConfiguration(IConfigurationContext context)
{
context["FirstName"] = FirstName;
context["LastName"] = LastName;
var ctx = context.CreateContext("MailingAddress");
MailingAddress.WriteConfiguration(ctx);
}
public void ReadConfiguration(IConfigurationContext context)
{
FirstName = context["FirstName"];
LastName = context["LastName"];
var ctx = context.GetContext("MailingAddress");
MailingAddress.ReadConfiguration(ctx);
}
}
Now let’s look at how this can be used. For this demonstration, I created a working implementation of IConfigurationContext
that uses XmlNode
to store configurations.
So below is an example that should be simple enough to understand.
static void Main()
{
var xmlDoc = new XmlDocument();
xmlDoc.Load("FileName");
var context = new XmlConfigurationContext(xmlDoc.DocumentElement);
var person = new Person();
person.LoadConfiguration(context);
}
static void Main()
{
var xmlDoc = new XmlDocument();
var context = new XmlConfigurationContext(xmlDoc, "Person");
person.WriteConfiguration(context);
xmlDoc.Save("FileName");
}
And below is an example of the created configuration file:
<Context Name="Person">
<Value Name="FirstName">FirstName</Value>
<Value Name="LastName">LastName</Value>
<Context Name="MailingAddress">
<Value Name="Street1">Street1</Value>
<Value Name="Street2">Street2</Value>
<Value Name="City">City</Value>
<Value Name="State">State</Value>
<Value Name="Zip">Zip</Value>
</Context>
</Context>
So let’s go back and review my requirements.
- User should be able to easily change or choose how and where configuration data is stored without affecting the configurable objects.
We accomplished that by simply providing different implementations of
IConfigurationContext
. So instead of XmlConfigurationContext
, we can
easily create DatabaseConfigurationContext
or WebServiceContext
or other variations. - Accessing and storing configuration should be very simple and transparent to the user but at the same time reliable. Accessing settings is straightforward
and simple using the simple indexer.
- Types of any of the child objects are not known to the process that initiates the loading of the configuration. When loading
Person
, we do not know what settings it will save nor do we care, we simply provide the context where to save or find the needed values.