Background
Quite often, I have seen developers implementing their own version of XML configuration reader/writer which may take time to develop and maintain. Though .NET 2.0 provides a Configuration library, I have not yet seen it much in use.
Configuration is one aspect no one has a definite and consistent approach about. From my experience, I have seen most developers implementing their own version of code to store/access configuration (which could either be in database, XML files, INI files, or Registry).
In this sample, I have come up with a very basic XML configuration file, the entire sample application is based on, and I believe will be very easy to use. You can always extend the configuration file/sample application to suit your needs, or use database instead of XML etc., but I suppose this example should be good enough to get you started, and will help you maintain the code to access configuration files a lot easier.
The Simple Idea
Well, this is not a path breaking sample application, but like I mentioned, it might help developers to understand and reuse components. Also, since this application uses Reflection, custom attributes, and LINQ, I guess it will be a good sample exercise to understand the concepts at a beginner level.
So, before we begin, here is how the sample config file looks like:
<Configuration>
<Module Name="Application-A">
<Section Name="Section-A">
<Entry Name="RefreshRate">11</Entry>
</Section>
</Module>
<Module Name="Application-B">
<Section Name="Section-B">
<Entry Name="LogFile">SomeLogFile.Log</Entry>
</Section>
</Module>
</Configuration>
The configuration is stored inside a Module
and a Section
as an Entry
tag.
I'll soon describe how to load and save files, but before that, let's discuss how the config variables are retrieved and saved.
The XML tags, i.e. Module
, Section
, and Entry
, are mapped to enum values, so they are easy to read and write, analogous to property fields. This is made possible because of the custom attribute that we attach to each enum value. The custom attribute takes the respective name of the application, Section
tag, and Entry
tag to which it relates to; for example, the RefreshRate
enum value in the example below.
public enum Confiugration
{
[CustomConfig(ApplicationName = "Application-A",
SectionName = "Section-A",
EntryName = "RefreshRate",
DefaultValue = "5")]
RefreshRate,
[CustomConfig(ApplicationName = "Application-B",
SectionName = "Section-B",
EntryName = "LogFile")]
LogFile
};
This allows us to read the configuration using this code:
configHelper.GetConfigEntry(Configuration.RefreshRate);
Similarly, you can save the configuration value with the following code:
configHelper.SetConfigEntry(Confiugration.RefreshRate, "11");
Here are the five lines of custom attribute code required:
[AttributeUsage(AttributeTargets.Field, AllowMultiple = false, Inherited = false)]
public sealed class CustomConfigAttribute : Attribute
{
public String ApplicationName { get; set; }
public String SectionName { get; set; }
public String EntryName { get; set; }
public String DefaultValue { get; set; }
}
In short, you didn't have to implement any code to read or write the variable; all you have to do is create an enum for your configuration, and fill in the appropriate attribute values attached to the enum value, and let the CustomConfigHelper
class read and write the configuration entry for you.
I am sure it must be obvious to you that the CustomConfigHelper
class provides functions that reads the attributes attached to the enum values and, as required, reads/updates the config file.
To read the custom attributes attached to an enum, the CustomConfigHelper
class uses Reflection on the enumeration that you pass. For example, for the GetConfigEntry
function, we'd do something like this:
public String GetConfigEntry(Enum enumValue)
{
Type type = enumValue.GetType();
FieldInfo info = type.GetField(enumValue.ToString());
var customAttribute = Attribute.GetCustomAttribute(info,
typeof(CustomConfigAttribute)) as CustomConfigAttribute;
Once the custom attribute is retrieved we can then use LINQ to retrieve the appropriate entry from the xml file. To load the xml file we use XElement.Load function
if (File.Exists(fileName))
{
rootNode = XElement.Load(fileName);
var item = from applicationNode in rootNode.Elements("Module")
where (String)applicationNode.Attribute("Name") ==
customAttribute.ApplicationName
from sectionNode in applicationNode.Elements("Section")
where (String)sectionNode.Attribute("Name") ==
customAttribute.SectionName
from entryNode in sectionNode.Elements("Entry")
where (String)entryNode.Attribute("Name") ==
customAttribute.EntryName
select entryNode.FirstNode;
if (item.Any())
{
return item.First().ToString();
}
}
Similarly, we can also save the configuration:
var item = from applicationNode in rootNode.Elements("Module")
where (String) applicationNode.Attribute("Name") ==
customAttribute.ApplicationName
from sectionNode in applicationNode.Elements("Section")
where (String) sectionNode.Attribute("Name") ==
customAttribute.SectionName
from entryNode in sectionNode.Elements("Entry")
where (String) entryNode.Attribute("Name") ==
customAttribute.EntryName
select entryNode;
if(!item.Any())
{
return;
}
item.First().Value = entryValue;
Points of Interest
Here is the full code of the CustomConfigHelper
class that does the magic for you:
public class CustomConfigHelper
{
private static XElement rootNode;
private CustomConfigHelper(){}
private static String configFileName { get; set; }
public CustomConfigHelper(String fileName)
{
if (File.Exists(fileName))
{
rootNode = XElement.Load(fileName);
configFileName = fileName;
}
}
private static void SaveConfigFile(String fileName)
{
rootNode.Save(fileName);
}
public String GetConfigEntry(Enum enumValue)
{
Type type = enumValue.GetType();
FieldInfo info = type.GetField(enumValue.ToString());
var customAttribute = Attribute.GetCustomAttribute(info,
typeof(CustomConfigAttribute)) as CustomConfigAttribute;
return customAttribute == null ? String.Empty :
GetConfigEntry(customAttribute);
}
public void SetConfigEntry(Enum enumValue, String entryValue)
{
if(entryValue == null)
{
entryValue = String.Empty;
}
Type type = enumValue.GetType();
FieldInfo info = type.GetField(enumValue.ToString());
var customAttribute = Attribute.GetCustomAttribute(info,
typeof(CustomConfigAttribute)) as CustomConfigAttribute;
SetConfigEntry(customAttribute, entryValue);
}
private static String GetConfigEntry(CustomConfigAttribute attribute)
{
if (rootNode == null)
{
return attribute.DefaultValue ?? String.Empty;
}
var item = from applicationNode in rootNode.Elements("Module")
where (String)applicationNode.Attribute("Name") ==
attribute.ApplicationName
from sectionNode in applicationNode.Elements("Section")
where (String)sectionNode.Attribute("Name") ==
attribute.SectionName
from entryNode in sectionNode.Elements("Entry")
where (String)entryNode.Attribute("Name") ==
attribute.EntryName
select entryNode.FirstNode;
if (item.Any())
{
return item.First().ToString();
}
return attribute.DefaultValue ?? String.Empty;
}
private static void SetConfigEntry(CustomConfigAttribute attribute,
String entryValue)
{
if (rootNode == null)
{
return;
}
var item = from applicationNode in rootNode.Elements("Module")
where (String) applicationNode.Attribute("Name") ==
attribute.ApplicationName
from sectionNode in applicationNode.Elements("Section")
where (String) sectionNode.Attribute("Name") ==
attribute.SectionName
from entryNode in sectionNode.Elements("Entry")
where (String) entryNode.Attribute("Name") ==
attribute.EntryName
select entryNode;
if(!item.Any())
{
return;
}
item.First().Value = entryValue;
SaveConfigFile(configFileName);
}
}
I hope this helps. Comments and suggestions as always are appreciated. Cheers.
History
Original version.