Broken Thoughts

Techknowledge

.Net Dojo: Custom Configuration

A best practice in .Net development is to have as much of your code in reusable libraries as possible. This cuts down on development time and helps enforce business standards across all applications. One problem however is that sometimes reusable libraries need to have a few customizations. The easiest way is to create a Settings file and set each property through the use of the app.config or web.config.

A more complicated solution is to create your own custom configurations. In my case, I have an enormous library that consists of different parts. I have database tools, data caching, encryption, cookie management, querystring management, Infocard support, email wrappers, as well as several other useful tools. Being a perfectionist, I like to have my settings split out by the piece they belong to. Sure, there is more xml in the config file, but, if I only want to use one section of my code I don’t need to have the whole configuration. Plus, having the configurations split out means I can easily split my library up into multiple DLL’s down the road.

Let’s start with a simple example of a custom config section. My ‘Olympus’ library has a root set of settings called OlympusConfiguration. This class sets the most commonly used pieces of my library - Email authentication, encryption, and something I call Application environment. This is a pre-defined set of values (development, testing, staging, production) that allow me to switch on and off certain pieces of code depending on the environment, by only changing one value.

My OlympusConfiguration class extends from System.Configuration.ConfigurationSection. As you can see below it is just a collection of properties with a ConfigurationProperty attribute. I am using default values incase the section or individual property is not included. On important settings, such as a connection string, it is a better practice to throw an error rather than use a default value.


// Class that creates the configuration handler
    public class OlympusConfiguration : ConfigurationSection
    {

        // Empty Construct
        public OlympusConfiguration()
        {
        }

        // Server host or IP address
        [ConfigurationProperty("mailServer", DefaultValue = "127.0.0.1", IsRequired = false)]
        public string MailServer
        {
            get
            {
                return this["mailServer"].ToString();
            }
        }

        // UserName for mailserver
        [ConfigurationProperty("mailUserName", DefaultValue = "", IsRequired = false)]
        public string MailUserName
        {
            get
            {
                return this["mailUserName"].ToString();
            }
        }

        // Password for mail server
        [ConfigurationProperty("mailPassword", DefaultValue = "", IsRequired = false)]
        public string MailPassword
        {
            get
            {
                return this["mailPassword"].ToString();
            }
        }

        // First Number Property
        [ConfigurationProperty("applicationEnvironment", DefaultValue = Olympus.Web.Environment.Production, IsRequired = false)]
        public Olympus.Web.Environment ApplicationEnvironment
        {
            get
            {
                try
                {
                    return (Olympus.Web.Environment)Enum.Parse(typeof(Olympus.Web.Environment), this["applicationEnvironment"].ToString(), true);
                }
                catch
                {
                    return Olympus.Web.Environment.Production;
                }
            }
        }

        [ConfigurationProperty("encryptionKey", DefaultValue = "", IsRequired = false)]
        public string EncryptionKey
        {
            get
            {
                return this["encryptionKey"].ToString();
            }
        }

        [ConfigurationProperty("encryptionIV", DefaultValue = "", IsRequired = false)]
        public string EncryptionIV
        {
            get
            {
                return this["encryptionIV"].ToString();
            }
        }
    }

You could expose this class directly and call it when you need to access the configuration setting, but I prefer to have a centralized static class called Configuration that exposes all of my custom configurations. Here is what the Property for accessing the OlympusConfiguration looks like:


        public static OlympusConfiguration OlympusConfiguration
        {
            get
            {
                OlympusConfiguration config = (OlympusConfiguration)System.Configuration.ConfigurationManager.GetSection("olympus/settings");

                return config;
            }
        }

Notice the value I am passing to GetSection(). This is the element path in the XML to my section. Now that I have the code written up for my section, it’s time to add the XML so we can set the properties. The first section to add is the section declaration. This is added inside the <configuration /> element. Usually there are already other definitions in the configSections element, so I’ve shown how I added mine in conjunction with the ones .Net added.


<configSections>
  <sectionGroup name="system.web.extensions" type="System.Web.Configuration.SystemWebExtensionsSectionGroup, System.Web.Extensions, Version=1.0.61025.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35">
   <sectionGroup name="scripting" type="System.Web.Configuration.ScriptingSectionGroup, System.Web.Extensions, Version=1.0.61025.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35">
    <section name="scriptResourceHandler" type="System.Web.Configuration.ScriptingScriptResourceHandlerSection, System.Web.Extensions, Version=1.0.61025.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" requirePermission="false" allowDefinition="MachineToApplication"/>
    <sectionGroup name="webServices" type="System.Web.Configuration.ScriptingWebServicesSectionGroup, System.Web.Extensions, Version=1.0.61025.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35">
     <section name="jsonSerialization" type="System.Web.Configuration.ScriptingJsonSerializationSection, System.Web.Extensions, Version=1.0.61025.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" requirePermission="false" allowDefinition="Everywhere"/>
     <section name="profileService" type="System.Web.Configuration.ScriptingProfileServiceSection, System.Web.Extensions, Version=1.0.61025.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" requirePermission="false" allowDefinition="MachineToApplication"/>
     <section name="authenticationService" type="System.Web.Configuration.ScriptingAuthenticationServiceSection, System.Web.Extensions, Version=1.0.61025.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" requirePermission="false" allowDefinition="MachineToApplication"/>
    </sectionGroup>
   </sectionGroup>
  </sectionGroup>
  <!-- Olympus Custom Config -->
  <sectionGroup name="olympus">
   <section name="settings" type="Olympus.OlympusConfiguration" allowLocation="true" allowDefinition="Everywhere"/>
   <section name="cache" type="Olympus.CacheConfiguration" allowLocation="true" allowDefinition="Everywhere"/>
  </sectionGroup>
 </configSections>

As you can see, we have the group name olympus and the section name settings. Also, there is a declaration for the cache config section. As I add more sections, I simply add them into this list. The type is the Namespace and class name of the configuration class that extends from System.Configuration.ConfigurationSection. Also, note how the olympus/settings passed into GetSection matches exactly (casing and all) the group and section in my web.config.

Now that we have the section defined, it’s time to set our values. This is done inside the <configuration /> element, but not inside <system.web/>, unless you places your configuration inside the system.web group. You can do this by nesting the sectionGroup elements, but make sure the path matches the value passed into GetSection().


<olympus>
 <settings mailServer="mail.myserver.com" applicationEnvironment="Development" />
 <cache/>
</olympus>

Take special note of the casing for the attributes and then examine the properties in the configuration class. The casing must match exactly. This is an example of a simple configuration section. There are ways to make collections of configurations buy using a class extending from ConfigurationElementCollection. In my case, I store a collection of settings for the cookies my site uses. In the example below, CookieConfig is a class extending from ConfigurationSection, just like the class above. The magic is is the CookieCollectionConfiguration class.


public class CookieCollectionConfiguration : ConfigurationElementCollection
    {

        public CookieConfig this[int index]
        {
            get
            {
                return base.BaseGet(index) as CookieConfig;
            }
            set
            {
                if (base.BaseGet(index) != null)
                {
                    base.BaseRemoveAt(index);
                }
                this.BaseAdd(index, value);
            }
        }

        public CookieConfig this[string name]
        {
            get
            {
                return base.BaseGet(name) as CookieConfig;
            }
            set
            {
                int index = -1;
                if (base.BaseGet(name) != null)
                {
                    index = base.BaseIndexOf(base.BaseGet(name));
                    base.BaseRemove(name);
                }

                if (index == -1)
                {
                    this.BaseAdd(value);
                }
                else
                {
                    this.BaseAdd(index, value);
                }
            }
        }

        protected override ConfigurationElement CreateNewElement()
        {
            return new CookieConfig();
        }

        protected override object GetElementKey(ConfigurationElement element)
        {
            return ((CookieConfig)element).Name;
        } 

    }

This code lets me select a CookieConfig based on the index, or, in a better case, by the name. To retrieve my collection of CookieConfigs, I use another class extended from ConfiguartionSection.


public class CookieConfiguration : ConfigurationSection
    {
        // Empty Construct
        public CookieConfiguration()
        {
        }

        [ConfigurationProperty("cookies")]
        public CookieCollectionConfiguration Cookies
        {
            get
            {
                return this["cookies"] as CookieCollectionConfiguration;
            }
        }

    }

Then, in my static Configuration class, I call up the whole configuration using the following method.


        public static CookieConfiguration CookieConfiguration
        {
            get
            {
                CookieConfiguration config = (CookieConfiguration)System.Configuration.ConfigurationManager.GetSection("olympus/cookie");

                return config;
            }
        }

Here is what the Config looks like for this section


<configSections>
 <!-- Olympus Custom Config -->
 <sectionGroup name="olympus">
  <section name="settings" type="Olympus.CookieConfiguration" allowLocation="true" allowDefinition="Everywhere"/>
 </sectionGroup>
</configSections>
<olympus>
 <cookies>
  <cookie name="MyCookieOne" />
  <cookie name="MyCookieTwo" />
 </cookies>
</olympus>

Creating custom configurations can be tricky, but it can be very powerful by allowing you to change how your application operates by altering the config file. This means there is no need to recompile the app, only a small alteration of the configuration. A trick I use when building web apps to have a seperate web.config for each environment. Then, when code is pushed to any servers, all that is needed is to delete the web.config and rename the appropriate file to the configuration needed.

January 29, 2008 - Posted by Broken Bokken | .Net | , , , , , , , , , , , | No Comments

No comments yet.

Leave a comment