Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / All-Topics

Auto Detect the Runtime Environment and Use the Right App Settings and Connection Strings

0.00/5 (No votes)
19 Mar 2010CPOL4 min read 1  
How to auto detect the runtime environment and use the right app settings and connection strings

There are many ways to manage the problem of connection string and app settings substitution in the web.config / app.config files when publishing to different environments (e.g. QA and Production servers). In the past, I have made use of the Web Deployment project's ability to replace the appsettings and connectionstrings sections, I have experimented with batch files, Build events, conditional compilation and used the extremely powerful FinalBuilder. However, my preferred solution is to have a single shared .config file with all the possible settings in it (so you only have to open one file to change any of the settings), then have the executing application automatically detect the environment and use the correct settings every time.

The technique discussed below builds on that of an earlier article which described how to centralize your shared application settings and connection strings in a common class library. It also assumes that you know the machine names of your development, QA and production servers. Obviously servers get replaced from time to time and websites sometimes get moved from one server to another, but it has been my experience that there is usually some sort of common naming convention used on servers and web farms, and knowing that convention should be good enough. Even this is not the case, the Development, QA and Production server names are stored in an app setting so you can easily change them at any time if necessary. For this example, the assumption is that the development servers are all named something like "Squirrel01", "Squirrel02", the QA boxes are "Fox01", "Fox02", and the production (farm) boxes are "Rabbit01x", "Rabbit01y", "Rabbit02x", "Rabbit02y", etc. With this in mind, it is necessary only to look for the words "Rabbit", "Fox" or "Squirrel" in the machine name we are running on to identify the current environment and know which section of our config file to use. If none of these names is found, we shall assume the app is running on the localhost of a developer's computer, and use those settings. I should point out that it is possible for a server to be configured in such a way as to prevent Environment.MachineName from returning a value, in which case, this technique simply will not work, so before you start trying to integrate this code into your solution, I recommend you create a quick test.aspx page or console app that simply does a Response.Write(Environment.MachineName)/Console.WriteLine(Environment.MachineName) and run it on your servers.

First, let's setup our .config file:

XML
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <configSections>
    <sectionGroup name="Localhost" 
    type="Williablog.Core.Configuration.EnvironmentSectionGroup, Williablog.Core">
      <section name="appSettings" 
      type="System.Configuration.AppSettingsSection, 
      System.Configuration, Version=2.0.0.0, Culture=neutral, 
      PublicKeyToken=b03f5f7f11d50a3a" restartOnExternalChanges="false" 
      requirePermission="false" />
      <section name="connectionStrings" 
      type="System.Configuration.ConnectionStringsSection, 
      System.Configuration, Version=2.0.0.0, Culture=neutral, 
      PublicKeyToken=b03f5f7f11d50a3a" requirePermission="false" />
    </sectionGroup>
 
    <sectionGroup name="Dev" 
    type="Williablog.Core.Configuration.EnvironmentSectionGroup, Williablog.Core">
      <section name="appSettings" 
      type="System.Configuration.AppSettingsSection, 
      System.Configuration, Version=2.0.0.0, Culture=neutral, 
      PublicKeyToken=b03f5f7f11d50a3a" restartOnExternalChanges="false" 
      requirePermission="false" />
      <section name="connectionStrings" 
      type="System.Configuration.ConnectionStringsSection, 
      System.Configuration, Version=2.0.0.0, Culture=neutral, 
      PublicKeyToken=b03f5f7f11d50a3a" requirePermission="false" />
    </sectionGroup>
 
    <sectionGroup name="Qa" 
    type="Williablog.Core.Configuration.EnvironmentSectionGroup, Williablog.Core">
      <section name="appSettings" 
      type="System.Configuration.AppSettingsSection, 
      System.Configuration, Version=2.0.0.0, Culture=neutral, 
      PublicKeyToken=b03f5f7f11d50a3a" restartOnExternalChanges="false" 
      requirePermission="false" />
      <section name="connectionStrings" 
      type="System.Configuration.ConnectionStringsSection, System.Configuration, 
      Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" 
      requirePermission="false" />
    </sectionGroup>
 
    <sectionGroup name="Production" 
    type="Williablog.Core.Configuration.EnvironmentSectionGroup, Williablog.Core">
      <section name="appSettings" 
      type="System.Configuration.AppSettingsSection, 
      System.Configuration, Version=2.0.0.0, Culture=neutral, 
      PublicKeyToken=b03f5f7f11d50a3a" 
      restartOnExternalChanges="false" requirePermission="false" />
      <section name="connectionStrings" 
      type="System.Configuration.ConnectionStringsSection, 
      System.Configuration, Version=2.0.0.0, Culture=neutral, 
      PublicKeyToken=b03f5f7f11d50a3a" requirePermission="false" />
    </sectionGroup>
  </configSections>
 
  <Localhost>
    <appSettings>
      <add key="WebServiceUrl" 
      value="http://webservices.squirrel01.yourserver.com/YourService.asmx"/>
      <add key="SmtpServer" 
      value="smtp.yourlocalmailserver.com"/>
    </appSettings>
    <connectionStrings>
      <add name="AppData" 
      connectionString="data source=Ford01;initial catalog=MyDB;
      User ID=User;Password=Password;" providerName="System.Data.SqlClient"/>
      <add name="ElmahDB" 
      connectionString="Database=ELMAH;Server=Ford02;User=User;
      Pwd=Password;" providerName="System.Data.SqlClient"/>
    </connectionStrings>
  </Localhost>
 
  <Dev>
    <appSettings>
      <add key="WebServiceUrl" 
      value="http://webservices.squirrel01.yourserver.com/YourService.asmx"/>
      <add key="SmtpServer" 
      value="smtp.yourlocalmailserver.com"/>
    </appSettings>
    <connectionStrings>
      <add name="AppData" 
      connectionString="data source=Ford01;initial catalog=MyDB;
      User ID=User;Password=Password;" providerName="System.Data.SqlClient"/>
      <add name="ElmahDB" 
      connectionString="Database=ELMAH;Server=Ford02;User=User;
      Pwd=Password;" providerName="System.Data.SqlClient"/>
    </connectionStrings>
  </Dev>
 
  <Qa>
    <appSettings>
      <add key="WebServiceUrl" 
      value="http://webservices.Fox01.yourserver.com/YourService.asmx"/>
      <add key="SmtpServer" 
      value="smtp.yourlocalmailserver.com"/>
    </appSettings>
    <connectionStrings>
      <add name="AppData" 
      connectionString="data source=BMW01;initial catalog=MyDB;
      User ID=User;Password=Password;" providerName="System.Data.SqlClient"/>
      <add name="ElmahDB" 
      connectionString="Database=ELMAH;Server=BMW02;User=User;
      Pwd=Password;" providerName="System.Data.SqlClient"/>
    </connectionStrings>
  </Qa>
 
  <Production>
    <appSettings>
      <add key="WebServiceUrl" 
      value="http://webservices.yourserver.com/YourService.asmx"/>
      <add key="SmtpServer" value="smtp.yourmailserver.com"/>
    </appSettings>
    <connectionStrings>
      <add name="AppData" 
      connectionString="data source=Audi01;initial catalog=MyDB;
      User ID=User;Password=Password;" providerName="System.Data.SqlClient"/>
      <add name="ElmahDB" connectionString="Database=ELMAH;
      Server=Audi02;User=User;Pwd=Password;" providerName="System.Data.SqlClient"/>
    </connectionStrings>
  </Production>
 
  <appSettings>
    <!-- Global/common appsettings can go here -->
    <add key="Test" value="Hello World"/>
 
    <add key="DevelopmentNames" value="SQUIRREL"/>
    <add key="ProductionNames" value="RABBIT"/>
    <add key="QANames" value="FOX"/>
    <add key="EnvironmentOverride" value=""/>
    <!-- /Dev | /Localhost | /Production | (blank)-->
 
  </appSettings>
</configuration>

As you can see, the first thing we do in the config file is declare four section groups, "LocalHost", "Dev", "Qa" and "Production". I chose to create a custom SectionGroup since this allowed me to strongly type the expected sections within it, greatly simplifying the code required to access those sections. All the EnvironmentSectionGroup class does, is inherit ConfigurationSectionGroup and declare two properties:

C#
namespace Williablog.Core.Configuration
{
    using System.Configuration;
 
    public class EnvironmentSectionGroup : ConfigurationSectionGroup
    {
 
        #region Properties
 
        [ConfigurationProperty("appSettings")]
        public AppSettingsSection AppSettings
        {
            get
            {
                return (AppSettingsSection)Sections["appSettings"];
            }
        }
 
        [ConfigurationProperty("connectionStrings")]
        public ConnectionStringsSection ConnectionStrings
        {
            get
            {
                return (ConnectionStringsSection)Sections["connectionStrings"];
            }
        }
 
        #endregion 
    }
}

Next, we create the sections for localhost, development, qa and production, each of which has its own appSettings and connectionStrings sections. These are of the same type as the connectionStrings and appSettings found in any .config file, meaning we don't need to write any additional code to fully utilise these sections - no traversing of primitive xmlNodes or anything like that to get the connectionstrings from that section. Finally, we add the expected, normal appsettings section which in this case will provide the global or common appsettings that are shared by all environments. It is here that we store the server names that will help us identify where the app is currently executing. The EnvironmentOverride setting is an added bonus -it allows you to use all of qa or production settings while running on localhost which helps you debug those "well it works on my machine" situations without having to manually change all of the settings for localhost.

Building on the BasicSettingsManager we built earlier, we simply add some code to determine the machine name we are running on and return the appSettings and connectionStrings sections appropriate to that environment:

C#
namespace Williablog.Core.Configuration
{
    using System;
    using System.Collections.Specialized;
    using System.Configuration;
    using System.IO;
    using System.Linq;
 
    public class AdvancedSettingsManager
    {
        #region fields
 
        private const string ConfigurationFileName = "Williablog.Core.config";
 
        /// <summary>
        /// default path to the config file that contains the settings we are using
        /// </summary>
        private static string configurationFile;
 
        /// <summary>
        /// Stores an instance of this class, to cut down on I/O: No need to keep re-loading that config file
        /// </summary>
        /// <remarks>Cannot use system.web.caching since agents will not have access 
        /// to this by default, so use static member instead.</remarks>
        private static AdvancedSettingsManager instance;
 
        /// <summary>
        /// Settings Environment
        /// </summary>
        private static string settingsEnvironment;
 
        private static EnvironmentSectionGroup currentSettingsGroup;
 
        #endregion
 
        #region Constructors
 
        private AdvancedSettingsManager()
        {
            ExeConfigurationFileMap fileMap = new ExeConfigurationFileMap();
 
            fileMap.ExeConfigFilename = configurationFile;
 
            Configuration config = 
            ConfigurationManager.OpenMappedExeConfiguration(fileMap, ConfigurationUserLevel.None);
 
            settingsEnvironment = "Localhost"; // default to localhost
 
            // get the name of the machine we are currently running on
            string machineName = Environment.MachineName.ToUpper();
 
            // compare to known environment machine names
            if (config.AppSettings.Settings["ProductionNames"].Value.Split
            (',').Where(x => machineName.Contains(x)).Count() > 0)
            {
                settingsEnvironment = "Production";
            }
            else if (config.AppSettings.Settings["QANames"].Value.Split
            (',').Where(x => machineName.Contains(x)).Count() > 0)
            {
                settingsEnvironment = "Qa";
            }
            else if (config.AppSettings.Settings["DevelopmentNames"].Value.Split
            (',').Where(x => machineName.Contains(x)).Count() > 0)
            {
                settingsEnvironment = "Dev";
            }
 
            // If there is a value in the EnvironmentOverride appsetting, 
            // ignore results of auto detection and set it here
            // This allows us to hit production data from localhost without monkeying with all the config settings.
            if (!string.IsNullOrEmpty(config.AppSettings.Settings["EnvironmentOverride"].Value))
            {
                settingsEnvironment = config.AppSettings.Settings["EnvironmentOverride"].Value;
            }
 
            // Get the name of the section we are using in this environment & 
            // load the appropriate section of the config file
            currentSettingsGroup = config.GetSectionGroup(SettingsEnvironment) as EnvironmentSectionGroup;
        }
 
        #endregion
 
        #region Properties
 
        /// <summary>
        /// Returns the name of the current environment
        /// </summary>
        public string SettingsEnvironment
        {
            get
            {
                return settingsEnvironment;
            }
        }
 
        /// <summary>
        /// Returns the ConnectionStrings section
        /// </summary>
        public ConnectionStringSettingsCollection ConnectionStrings
        {
            get
            {
                return currentSettingsGroup.ConnectionStrings.ConnectionStrings;
            }
        }
 
        /// <summary>
        /// Returns the AppSettings Section
        /// </summary>
        public NameValueCollection AppSettings
        {
            get
            {
                NameValueCollection settings = new NameValueCollection();
                foreach (KeyValueConfigurationElement element in currentSettingsGroup.AppSettings.Settings)
                {
                    settings.Add(element.Key, element.Value);
                }
 
                return settings;
            }
        }
 
        #endregion
 
        #region static factory methods
 
        /// <summary>
        /// Public factory method
        /// </summary>
        /// <returns></returns>
        public static AdvancedSettingsManager SettingsFactory()
        {
            // If there is a bin folder, such as in web projects look for the config file there first
            if (Directory.Exists(AppDomain.CurrentDomain.BaseDirectory + @"\bin"))
            {
                configurationFile = string.Format(@"{0}\bin\{1}", 
                AppDomain.CurrentDomain.BaseDirectory, ConfigurationFileName);
            }
            else
            {
                // agents, for example, won't have a bin folder in production
                configurationFile = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, ConfigurationFileName);
            }
 
            // If we still cannot find it, quit now!
            if (!File.Exists(configurationFile))
            {
                throw new FileNotFoundException(configurationFile);
            }
 
            return CreateSettingsFactoryInternal();
        }
 
        /// <summary>
        /// Overload that allows you to pass in the full path 
        /// and filename of the config file you want to use.
        /// </summary>
        /// <param name="fullPathToConfigFile"></param>
        /// <returns></returns>
        public static AdvancedSettingsManager SettingsFactory(string fullPathToConfigFile)
        {
            configurationFile = fullPathToConfigFile;
            return CreateSettingsFactoryInternal();
        }
 
        /// <summary>internal Factory Method
        /// </summary>
        /// <returns>ConfigurationSettings object
        /// </returns>
        internal static AdvancedSettingsManager CreateSettingsFactoryInternal()
        {
            // If we haven't created an instance yet, do so now
            if (instance == null)
            {
                instance = new AdvancedSettingsManager();
            }
 
            return instance;
        }
 
        #endregion
    }
}

As before, you can then access the appSettings of the Core.Config from any of your projects like so:

C#
Console.WriteLine
(Williablog.Core.Configuration.AdvancedSettingsManager.SettingsFactory().AppSettings["Test"]);

To make this work, you will need to add a reference to System.Configuration. If the config file and Settings manager code is to be part of a class library, you will need to set the "Copy to Output Directory" property of your .config file to "Copy always"and add a reference to System.Configuration to each of your projects.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)