ONDotNet.com    
 Published on ONDotNet.com (http://www.ondotnet.com/)
 See this if you're having trouble printing code examples


Beginnings of a Flexible .NET Architecture: A Simplified Config Service

by Satya Komatineni
08/19/2002

Every application needs to read configuration information at run time. This need is well recognized under .NET, which provides a unified XML-based configuration file. For ASP.NET, this file is called web.config. Your application will be able to read the contents of this file at run time. .NET provides a series of calls making this process fairly simple; nevertheless you will need to write some C# code to accomplish this. My proposed solution simplifies this process further, and provides a layer of abstraction largely independent of the XML format of your file(s).

In addition, the programmer needs know very little about the XML classes and other XML details. A "configuration service" is the beginning of a flexible architecture. It is a string that, when pulled in the proper manner, will gradually lead to the following services:

At some point in the future, I may explain in detail how this transition could take place. In this article I may allude to a combination of these services as I explain the configuration service.

Related Reading

.NET Framework Essentials
By Thuan L. Thai, Hoang Lam

Let's start by looking at a sample web.config file with some configuration entries.


<?xml version="1.0" encoding="utf-8" ?>
<configuration>
   <configSections>
     <section name="SimpleConfiguration"
              type="MySectionHandler,MyAssembly">
     </section>
   </configSections>

<SimpleConfiguration>
  <Module1>
    <section1>
      <key1>value1</key1>
      <key2>value2</key2>
    </section1>
  </Module1>
  <Module2>
    <section1>
      <key1>value1</key1>
      <key2>value2</key2>
    <section1>
  </Module2>
</SimpleConfiguration>

What Does .NET Do With This File?

While putting together the above configuration file, your intention is to read the entries in the SimpleConfiguration section. To accomplish this, you do the following in .NET:


ArrayList modules =
(ArrayList)System.Configuration.ConfigurationSettings.
    GetConfig("SimpleConfiguration");

There are a few things happening in this line. When the above call is issued, .NET will invoke a class identified by SimpleConfiguration (in the configSections) and call its well-known Create method by passing an XML node to that create method. Whatever object this Create method returns will be handed to the caller of GetConfig(...). Just to be a bit more complete, here is sample code for one such handler:


public class MySectionHandler : IConfigurationSectionHandler
{
    public object Create(object parent, object configContext, 
XmlNode section)
   {
      ... Read your xmlnode section
      ... return an ArrayList
   } // end of method
} // end of class

There must be an understanding between the client and the section handler that they are dealing with the same kind of object. In this case, this object is ArrayList.

How Can This Approach Be Simplified?

There are a few nice things about the above approach. The caller will get a C# object for each XML section. And you can have any number of XML sections. On the other hand, for such convenience you will have to:

  1. Write a class for each section.
  2. Parse your XML section to convert it into suitable objects.

Often, clients want a much simpler interface to their configuration. Here is an example:


string value = AppServices.getValue(
"/SimpleConfiguration/Module1/section1/key1");

Once we have such an API available, there is no longer any need for creating new section handlers -- at least for the simple configuration stuff. Although I call this "simple stuff," it is quite amazing how far you can take this simplicity. But first, let us tighten up this API.

Often, you will see that some configuration keys are mandatory. Keeping that in mind, AppServices.getValue(...) will throw an exception if a key is not found. Also, the client might want to specify a default value in case a certain key is not found. To take this into account, here is the overloaded version of that API.


string value = AppServices.getValue(
"/SimpleConfiguration/Module1/section1/key1",
"defaultvalue");

This version of the API will not throw an exception if the key is not found, but instead will return the default value that is passed in. For example, you can also do the following:


string value =
AppServices.getValue(
"/SimpleConfiguration/Module1/section1/key1", null);
if (value == null)
{
    // you have just avoided an exception if you care to do so
}

It is also possible to make the keys case-insensitive by reducing the error rate; this approach is quite similar to Xpath. Recognizing that, here is another version of this API:

string xpathExpression = ".";
string value = AppServices.getXPathValueString(
xpathExpression);

Occasionally you may have multiple nodes at a given key, in which case you may want to receive the node as an XML node yourself. You may want to do the following:


XMLNode node = AppServices.getXPath(xpathExpression);

Let me come back to the basic AppServices.getValue(key). Sometimes, although the key is specified in an XML file, its contents might be white space. This may be considered the same as not mentioning the key at all. And as such, one might expect the API to throw an exception. To accommodate this variation, one can call another function:


string value = AppServices.getValueHonourWhiteSpace(key);

In summary, here are the functions we have so far:


Public class AppServices
{
    // Configuration Service
    public static getValue(string key);
      // throws an exception if the key is not found or has an 
     // empty string
    public static getValueHonourWhiteSpace(string key)
      // throws an exception if the key is not found

    public static getValue(string key, string default);
      // returns the default if the key is not found or has an 
     // empty string
    public static getValueHonourWhiteSpace(string key,string default)
      // returns the default if the key is not found

    //Xpath support
    public static getXPathValue(string key);
       // throws an exception if the key is not found or has an 
      // empty string
    public static getXPathValueHonourWhiteSpace(string key)
      // throws an exception if the key is not found

    public static getXPathValue(string key, string default);
      // returns the default if the key is not found or has an 
      // empty string
    public static getXPathValueHonourWhiteSpace(string key, string default)
     // returns the default if the key is not found

    // Other future services
}

What we are striving for here is simplicity. One may choose to implement a portion of the above APIs.

The Configuration Service In Practice

Let us be more clear by applying the configuration service to a practical example. Let's say we want to send an email for a Web application. Let's also consider that in your chest of copious libraries you have earlier penned an API that looks like this:


public static void sendMail(string from, string to, string subject, string body, string format);

Equipped with such an API, my client code could look like this:


    public static trivialMailExample()
    {
      string from="support@johndoeinc.com";
      string to = "johndoe2@customerinc.com";
      string subject = "Your order is ready";
      string body="You can pick it up at facility1";
      string format = "plain-text";

      sendMail(from,to,subject,body,format);
    }

As you can see, all of the parameters of the email are hard-coded into your Web site. What if we want to read some of these from a config file?

...
<SimpleConfiguration>
    <EmailInfo>
      <from>support@johhdoeinc.com
      <subject>Your order is ready. Order number {0}</subject>
      <body> <![[CDATA
<html>
<body>
    <p>Here are your order details</p>
    {0}
</body>
</html>
      ]]>
      </body>
      <format>html</format>
    </EmailInfo>
</SimpleConfiguration>

Ah ha! Once this configuration is in place, I can change my earlier method to the following:


    public static trivialMailExampleEnabledForConfig(string toCustomer, string orderId)
    {
      string from = AppServices.getValue("/SimpleConfiguration/from"); 
        // an error not to have it
      string subject = string.Format(AppServices.getValue("/SimpleConfiguration/from"), orderId);
      string body = string.Format(AppServices.getValue("/SimpleConfiguration/from"), orderId);
      string format = AppServices.getValue("/SimpleConfiguration/from","plain-text"); 
      // defaults to plain-text

      sendMail(from,toCustomer,subject,body,format);
    }

You can very easily change your config file to alter the look and feel of your emails. Notice how string.Format(...) is used to substitute dynamic values into a template read from the config file. Also notice how you can use CDATA sections to embed HTML inside of your XML sections.

Public Static Services Turned Into Interfaces

Before I tell you how to implement these methods, let me take a little detour and explain how I want to convert the static methods into an interface. Here is the reason: AppServices is a collection of interfaces, each of which represents a service. For example:

public AppServices
{
    public static IConfig getConfigurationService();
    pulbic static IFacgtory getFactoryService();
    public static ILog getLoggingService();
    ...any other application level services
}

Let us see what IConfig could be having:

public interface IConfig
{
    public static getValue(string key);
      // throws an exception if the key is not found or has an 
      // empty string
    public static getValueHonourWhiteSpace(string key)
      // throws an exception if the key is not found

    public static getValue(string key, string default);
      // returns the default if the key is not found or has an 
      // empty string
    public static getValueHonourWhiteSpace(string key, string default)
      // returns the default if the key is not found
}

We can have another interface for XPath support, as follows:


public interface IConfigXPath
{
    //Xpath support
    public static getXPathValue(string key);
      // throws an exception if the key is not found or has an 
     // empty string
    public static getXPathValueHonourWhiteSpace(string key)
      // throws an exception if the key is not found

    public static getXPathValue(string key, string default);
      // returns the default if the key is not found or has an 
      // empty string
    public static getXPathValueHonourWhiteSpace(string key, string default)
      // returns the default if the key is not found
}

Now I can proceed to have an implementation of these as follows:


public class DefaultConfig : IConfig, IXPathConfig
{
    ... Implements all the methods
}

Now one can code AppServices as follows:


public class AppServices
{
    private IConfig m_config = new DefaultConfig(); 
      // Potentially one can get this from a factory

    public static getIConfig() { return m_config; }
    ... and others
}

Why convert static methods to an interface? Because for some tastes, case sensitivity is a good thing and for others, case insensitivity is desirable. Having an interface will allow for both.


public class CaseSensitiveConfig : IConfig
{
    //... implement your keys with case sensitive
}

public class CaseInsensitiveConfig : IConfig
{
    //... implement your keys with case insensitive
}

And your app services can return a suitable implementation at run time. If you want to know how, you may have to wait until I publish on the topic of factory services. Or you can read about in one of the references at the end of the article. Let us demonstrate one more nicety before moving on.


public class CaseInsensitiveMultiFileConfig : IConfig
{
    // Implement your keys with case insensitivity and 
    // read from multiple config files
}

As we start placing emphasis on config files, it won't be long before the idea proliferates and your XML config file will grow as long as the phone book. In a team environment, it is more desirable to break up configuration into multiple files. The above class could implement one such implementation, thereby increasing the team collaboration.

So far we have talked about benefits and how to use a configuration service. I could perhaps leave it as an exercise for the inquisitive to implement it. For the eager audience, nevertheless, I would like to help out by providing some basic code to get started.

Implementation

The approach is simple enough. All of our configuration will sit in one section. There will be classes like DefaultConfig, which will absorb this long XML section. Once absorbed, the DefaultConfig will walk through every node of the XML and make entries in a dictionary (or hashtable) for each key and value. This dictionary will then satisfy client requests for keys.

Section handler code


    public class SimpleConfigurationSectionHandler :IConfigurationSectionHandler
    {
      public object Create(object parent, object configContext, XmlNode section)
      {
        return section;
      } // end of method
    } // end of class

DefaultConfig Code


public class DefaultConfig : IConfig {

// keep a dictionary of values
private IDictionary m_keyValuePairs

// implement methods of IConfig using the above 
// dictionary details left to you

//  Constructor, where it reads your SimpleConfiguration 
// XML node using the section handler above
    public DefaultConfig()
    {
      // read the xml section for general config
      // Section name: SimpleConfiguration
      XmlNode xmlNode =
        (XmlNode)System
          .Configuration
          .ConfigurationSettings
          .GetConfig("SimpleConfiguration");

      if(xmlNode != null)
      {
      m_keyValuePairs = createDictionary(m_genConfigXmlNode);
      }
    }
}

// Here is the createdictionary
    private IDictionary createDictionary(XmlNode genConfigXmlNode)
    {
      IDictionary ht = new Hashtable();
      if(genConfigXmlNode != null && 
        genConfigXmlNode.ChildNodes != null &&
        genConfigXmlNode.ChildNodes.Count > 0)
    {
      // walk through each node
      // if it is a leaf node add it to the hash table
      // if it is not  continue the process

      walkTheNode(ht,"",genConfigXmlNode);
      }
      return ht;
    }

//  Here is how you walk the nodes recursively 
// to get your keys
private void walkTheNode(IDictionary ht, string parent, XmlNode node)
{
    if(node != null)
   {
     if (node.NodeType == XmlNodeType.Comment)
    {
      return;
    }
    if (node.HasChildNodes == false)
    {
      if (node.NodeType == XmlNodeType.Text)
      {
        // no children
        string leaf = node.Value;
        ht.Add(parent.ToLower(),leaf);
        // end of the recursive call
       return;
      }
     else if (node.NodeType == XmlNodeType.CDATA)
     {
        XmlCDataSection cdataSection = (
        System.Xml.XmlCDataSection)node;
        string leaf = cdataSection.Data;
        ht.Add(parent.ToLower(),leaf);
        // end of the recursive call
        return;
     }
     else
        {
        string key = parent + "/" + node.Name;
        string val = "";
        ht.Add(key.ToLower(), val);
        return;
        }
     }
     else
    {
      string newparent = parent + "/" + node.Name;
      // has children
      // walk all the children
     for(int i=0;i<node.ChildNodes.Count;i++)
     {
        // recursive call
       walkTheNode(ht,newparent,node.ChildNodes[i]);
     }
    }
  }
}

One caveat while coding this: pay special attention to how CDATA and empty text nodes are treated!

Related Reading

Programming .NET Web Services
By Alex Ferrara, Matthew MacDonald

Conclusion

I hope to have conveyed that one can simplify the process of accessing configuration variables from config files, be they one or many.

This article has:

In the next article in this series, we will take this simple-minded configuration service and extend it into a FactoryService that allows much more flexibility in your architectures.

References

Satya Komatineni is the CTO at Indent, Inc. and the author of Aspire, an open source web development RAD tool for J2EE/XML.


Return to .NET DevCenter

Copyright © 2009 O'Reilly Media, Inc.