I work in an environment where I need to have a development version of my application, connected to a development database, with all file paths pointed to places most likely located on my own computer. When I am ready for user testing and load testing, I deploy to a clone of my production environment, with its own database server, web server, batch server, and file server. Finally, when I am ready to deploy to production, that environment also has its own set of servers.

Like everyone else, I use a configuration file to store all of the connection strings, file paths, and other “stuff”. In the old days, part of my deploy procedure was to make all of my config file changes as the code was moved from place to place. Then one day, a fellow employee told of a magical bit of code from the land of wizards and wonder that he had used to make all of this happen magically.

OK, actually, he started us on the path to a custom configuration tool.

With this tool, we were able to add our own sections to the config file, determine what environment we were on, and load a cache of the custom configuration specific to the environment on which the application is running.

Here is a simple version of a configuration file we use:

<configuration>
    <configSections>
        <sectionGroup name="CustomConfig">
            <section name="HostServer" type="System.Configuration.NameValueFileSectionHandler, System, Version=1.0.3300.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
            <sectionGroup name="Environments">
                <section name="Development" type="System.Configuration.NameValueFileSectionHandler, System, Version=1.0.3300.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
                <section name="Development-Test" type="System.Configuration.NameValueFileSectionHandler, System, Version=1.0.3300.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
                <section name="Development-Prod" type="System.Configuration.NameValueFileSectionHandler, System, Version=1.0.3300.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
                <section name="Test" type="System.Configuration.NameValueFileSectionHandler, System, Version=1.0.3300.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
                <section name="Production" type="System.Configuration.NameValueFileSectionHandler, System, Version=1.0.3300.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
            </sectionGroup>
        </sectionGroup>
    </configSections>
    <CustomConfig>
        <!-- HostServer section aligns server names with environment sections -->
        <HostServer>
            <add key="MICHAEL-LAPTOP" value="Development-Test" />
            <add key="INTERN-PC" value="Development" />
            <add key="WEBSITE-DEV" value="Test" />
            <add key="WEBSITE-PROD" value="Production" />
        </HostServer>
        <Environments>
            <Development>
              <add key="DBConnStr" value="Data Source=DBDev;Initial Catalog=DBDev;User ID=DevUser;Password=pw;Pooling=True;Connect Timeout=1200;Application Name=Config Application Dev" />
              <add key="WebsiteURL" value="http://localhost:54561/" />
            </Development>
            <Development-Test>
              <add key="DBConnStr" value="Data Source=DBTest;Initial Catalog=DBTest;User ID=TestUser;Password=pw;Pooling=True;Connect Timeout=1200;Application Name=Config Application Dev-Test" />
              <add key="WebsiteURL" value="http://localhost:54561/" />
            </Development-Test>
            <Development-Prod>
              <add key="DBConnStr" value="Data Source=DBProd;Initial Catalog=DBProd;User ID=ProdUser;Password=pw;Pooling=True;Connect Timeout=1200;Application Name=Config Application Dev-Prod" />
              <add key="WebsiteURL" value="http://localhost:54561/" />
            </Development-Prod>
            <Test>
              <add key="DBConnStr" value="Data Source=DBTest;Initial Catalog=DBTest;User ID=TestUser;Password=pw;Pooling=True;Connect Timeout=1200;Application Name=Config Application Test" />
              <add key="WebsiteURL" value="http://ConfigAppTest/" />
            </Test>
            <Production>
              <add key="DBConnStr" value="Data Source=DBProd;Initial Catalog=DBProd;User ID=ProdUser;Password=pw;Pooling=True;Connect Timeout=1200;Application Name=Config Application Prod" />
              <add key="WebsiteURL" value="http://ConfigApp.com/" />
            </Production>
        </Environments>
    </CustomConfig>
    <appSettings>
        <add key="SQLCommandTimeout" value="1200" />
        <add key="SMTPServer" value="smtp.domain.local" />
        <add key="ClientValidationEnabled" value="true" />
        <add key="UnobtrusiveJavaScriptEnabled" value="true" />
    </appSettings>
<configuration>

Now, a few explanations. At the top, in the configSections section, we are warning the configuration manager of the custom sections we are adding.

One is the HostServer section, where we are telling the Config.cs class below which machine will be using which environment. Notice that on my computer, I am using the Development-Test environment, so that I can test on my localhost against the test database.

Next is the actual environments. We have Test, Development, and Production, along with environment definitions for testing locally against the test and production databases.

Finally, notice we still have some settings in the appSettings section. These will apply across all environments. One simple call to Config.GetValue([keyname]) will get the value of any configuration setting, whether in the custom section or the app settings section.

Now for our wrapper class file, Config.cs:

Code:

using System;
using System.Collections.Specialized;
using System.Configuration;

namespace Utilities
{
    public static class Config
    {
        private static NameValueCollection _configCollection;
        private static DateTime _refreshTime;
        private static string _strServer;
        private static string _currentDirectory;
        public static string Environment;

        /// <summary>
        /// Builds a list of custom config keys that are specific to a certain environment as determined by the host server.
        /// </summary>
        public static void LoadConfig()
        {
            Config.Environment = Config.GetEnvironment();
            _configCollection = ConfigurationManager.GetSection("CustomConfig/Environments/" + Config.Environment) as NameValueCollection;
            _refreshTime = DateTime.Now;
        }

        /// <summary>
        ///  used for unit testing
        /// </summary>
        /// <param name="Environment"></param>
        /// <param name="RefreshTime"></param>
        public static void SetConfig(string Environment, DateTime RefreshTime)
        {
            Config.Environment = Environment;
            _configCollection = ConfigurationManager.GetSection("CustomConfig/Environments/" + Config.Environment) as NameValueCollection;
            _refreshTime = RefreshTime;
        }

        /// <summary>
        ///  used for unit testing
        /// </summary>
        /// <param name="Server"></param>
        /// <param name="CurDirectory"></param>
        /// <param name="RefreshTime"></param>
        public static void SetConfig(string Server, string CurDirectory, DateTime RefreshTime)
        {
            _strServer = Server;
            _currentDirectory = CurDirectory;
            Config.GetEnvironment();
            _configCollection = ConfigurationManager.GetSection("CustomConfig/Environments/" + Config.Environment) as NameValueCollection;
            _refreshTime = RefreshTime;
        }

        /// <summary>
        /// used to cleanup unit test messes
        /// </summary>
        public static void ResetConfig()
        {
            _configCollection = null;
            _refreshTime = DateTime.MinValue;
            _strServer = null;
            _currentDirectory = null;
            Environment = null;
        }

        /// <summary>
        /// Gets the environment.
        /// </summary>
        /// <returns>environment</returns>
        public static string GetEnvironment()
        {
            _strServer = System.Environment.MachineName.ToUpper();
            _currentDirectory = AppDomain.CurrentDomain.BaseDirectory;

            NameValueCollection configSections = ConfigurationManager.GetSection("CustomConfig/HostServer") as NameValueCollection;
            string strCollection = configSections[_strServer];

            if (strCollection == null)
                throw new ApplicationException("HostServer not defined. See CustomConfig/HostServer in configuration file.");

            return strCollection;
        }

        public static System.Collections.Specialized.NameObjectCollectionBase.KeysCollection GetEnvironments()
        {
            System.ConfigurationConfiguration config = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None);
            return config.SectionGroups["CustomConfig"].SectionGroups["Environments"].Sections.Keys;
        }

        /// <summary>
        /// Get a value from the custom config collection.
        /// </summary>
        /// <param name="pKey">Key parameter is used to find a specific value in the custom config collection.</param>
        /// <returns>
        /// A string value.
        /// </returns>
        public static string GetValue(string pKey)
        {
            try
            {
                if (_refreshTime.AddMinutes(5) < DateTime.Now)
                    _configCollection = null;

                if (_configCollection == null || _configCollection.Count == 0)
                    Config.LoadConfig();

                if (_configCollection[pKey] != null)
                    return _configCollection[pKey].ToString();
                else if (ConfigurationManager.AppSettings[pKey] != null)
                    return ConfigurationManager.AppSettings[pKey];
                else
                    return string.Empty;
            }
            catch
            {
                return ConfigurationManager.AppSettings[pKey];
            }
        }

        /// <summary>
        /// Gets the config info.
        /// </summary>
        /// <returns>config info</returns>
        public static string GetConfigInfo()
        {
            if (_refreshTime.AddMinutes(5) < DateTime.Now)
                _configCollection = null;

            if (_configCollection == null)
                Config.LoadConfig();

            string strConfigInfo = "Server : " + _strServer + "\n";
            strConfigInfo += "Directory : " + _currentDirectory + "\n";
            strConfigInfo += "Refresh Time : " + _refreshTime.ToString() + "\n";
            strConfigInfo += "\nConfig Settings:\n";

            foreach (string strKey in _configCollection.Keys)
            {
                strConfigInfo += "\t" + strKey + " : " + _configCollection[strKey] + "\n";
            }

            return strConfigInfo;
        }
    }
}


A couple of notes on this file:

  • Cache lasts for 5 minutes. I suppose that could be a config setting. That would be SO META!!
  • You can explicitly SET the environment. This is helpful in test programs.
  • No need to call ANYTHING except Config.GetValue([keyname]). It loads the environment, loads the config into the cache, and returns the value. Magic!

That is all for now. Props to Kyle for starting us down this road. It is one of the most indispensable members of our Utilities library.

Advertisements