Supporting Multiple Environments with a single XML Configuration file

Abstract

Provide a technique for setting up configuration files that can support multiple environments (e.g. staging and production) and allow for central deployment without having to update config files on the fly during deployment. Benefits of the approach described:

  • config files are not touched during deployment
  • developers are forced to think about configuration for EVERY environment at the start of the development process if their effort will involve new configuration points (like a new database or something)
  • key configuration treated as a cross-cutting concern and managed centrally rather than repeating items in web.config files and other places (this makes a big difference if you have different applications that should be sharing information like database connections, server names, etc)

Background

Any good development organization should have at least a couple of different environments that support a full application life cycle management: development itself, one or more testing areas, and a production environment are good starting points. This article is not meant to comment on which precise environments you have and their purpose — but rather acknowledges that you should probably have more than one and then addresses how to add support in your application code and deployment practices for those environments.

For each environment you probably have some metadata that helps define the environment. Such pieces of metadata might be:

CAUTION

This approach should not be used to store any secret values in a configuration file that finds its way into source control. At a minimum, inject the secret values during a deployment or override the non-secret values in this file with secret ones from another configuration source (e.g. environment variables). But this approach would work very well for non-secret values that differ from environment to environment.

  • Database connection strings
  • Network folders (logging, files, reports, etc)
  • Web service URLs
  • Server names

Approach

The basic approach involves three basic parts:

  • A common.config file that will contain the configuration information for ALL of your environments in different sections
  • A separate environment.config file that is placed on every machine running any of your applications and simply indicates which environment that machine is configured to use
  • A simple custom architecture assembly that will be referenced by your applications and provide the means to access the configuration

The Config files and Code

common.config

Create a configuration file called common.config (or some other name you like) and make it look something like this (I’m guessing you can figure out what the sample values are — you’ll see how to make these your own and even add more in the custom architecture code below) — specifically note the three SECTIONS for dev (development), tes (test), and prd (production) — you can create whatever sections you want:

 1<?xml version="1.0" encoding="utf-8" ?>
 2<configuration>
 3  <configSections>
 4    <section name="CustomConfigSection.dev" type="MyCompany.Architecture.Common.CustomConfiguration, MyCompany.Architecture.Common"/>
 5    <section name="CustomConfigSection.tes" type="MyCompany.Architecture.Common.CustomConfiguration, MyCompany.Architecture.Common"/> 
 6    <section name="CustomConfigSection.prd" type="MyCompany.Architecture.Common.CustomConfiguration, MyCompany.Architecture.Common"/>
 7  </configSections>
 8  <CustomConfigSection.dev    
 9     myMainDbConnection="Server=myDevDbServer;Initial Catalog=myDevDb;Connection Timeout=60;Trusted_Connection=Yes"
10     myFileDir="C:\Files"
11     myLogPath="C:\Logs"
12     myReportService="http://myreportserver/reportserver/reportservice2010.asmx"
13     myReportExecutionService="http://myreportserver/reportserver/reportexecution2005.asmx"    
14  />
15  <CustomConfigSection.tes
16     myMainDbConnection="Server=myTesDbServer;Initial Catalog=myTesDb;Connection Timeout=60;Trusted_Connection=Yes"
17     myFileDir="C:\Files"
18     myLogPath="C:\Logs"
19     myReportService="http://testreportserver/reportserver/reportservice2010.asmx"
20     myReportExecutionService="http://testreportserver/reportserver/reportexecution2005.asmx"    
21  />
22  <CustomConfigSection.prd    
23     myMainDbConnection="Server=myDevDbServer;Initial Catalog=myDevDb;Connection Timeout=60;Trusted_Connection=Yes"
24     myFileDir="\\prdfileshare\files"
25     myLogPath="\\prdfilesshare\logs"
26     myReportService="http://prdreports/reportserver/reportservice2010.asmx"
27     myReportExecutionService="http://prdreports/reportserver/reportexecution2005.asmx"    
28  />
29</configuration>

As noted, the above is merely a sample and should contain the environment-specific configuration values appropriate for your company / project.

environment.config

Create a second configuration file called environment.config (or a name of your choosing) and make it look like this:

1<?xml version="1.0" encoding="utf-8" ?>
2<configuration>
3<appSettings>
4  <add key="environment" value="dev"/>
5</appSettings>
6</configuration>

The Library Code

The library code is what will expose your configuration to the application. You need a class that represents your configuration options, and then an object that reads and exposes that configuration to the rest of your application code, wherever it may be.

Here’s the class representing the configuration options presented in the common.config above – note that the class name and assembly name for this should match what you have in the TYPE value of the common.config section nodes. You should note the connection between the configuration property attribute values and the attribute values from the common.config file above — this file defines what you expect that section to look like — so make it whatever you want. Refer to the .Net ConfigurationProperty documentation for more information if needed.

 1public class CustomConfiguration : ConfigurationSection
 2{
 3 [ConfigurationProperty("myMainDbConnection", IsRequired = true)]
 4 public string MyMainDbConnection
 5 {
 6  get { return (string)this["myMainDbConnection"]; }
 7  set { this["myMainDbConnection"] = value; }
 8 }
 9 [ConfigurationProperty("myFileDir", IsRequired = true)]
10 public string MyFileDir
11 {
12  get { return (string)this["myFileDir"]; }
13  set { this["myFileDir"] = value; }
14 }
15 [ConfigurationProperty("myLogPath", IsRequired = true)]
16 public string MyLogPath
17 {
18  get { return (string)this["myLogPath"]; }
19  set { this["myLogPath"] = value; }
20 }
21 [ConfigurationProperty("myReportService", IsRequired = true)]
22 public string MyReportService
23 {
24  get { return (string)this["myReportService"]; }
25  set { this["myReportService"] = value; }
26 }
27 [ConfigurationProperty("myReportExecutionService", IsRequired = true)]
28 public string MyReportExecutionService
29 {
30  get { return (string)this["myReportExecutionService"]; }
31  set { this["myReportExecutionService"] = value; }
32 }
33}

Ok, so now that we have the class representing the options, we need to read it and provide it to the application. Turns out that’s pretty simple. Here’s some more code (notes follow the code):

 1public class Config
 2{
 3    public CustomConfiguration CustomConfig { get; private set; }
 4 
 5    private static Config _instance;
 6    static readonly object Singleton = new object();
 7 
 8    private static string _cfgPath = @"..\cfg";
 9 
10    public static Config Instance
11    {
12        get
13        {
14            lock (Singleton)
15            {
16                if (_instance == null)
17                {
18                    _instance = new Config();
19                }
20                return _instance;
21            }
22        }
23    }
24 
25    private Config()
26    {
27        const string envConfigFile = @"environment.config";
28        const string configFile = @"common.config";
29 
30        //if (!Directory.Exists(_cfgPath))
31        // _cfgPath = UtilityMethods.GetCommonPath();
32 
33        var envConfigFileMap = new ExeConfigurationFileMap {ExeConfigFilename = Path.Combine(_cfgPath, envConfigFile)};
34        var envConfig = ConfigurationManager.OpenMappedExeConfiguration(envConfigFileMap, ConfigurationUserLevel.None);
35 
36        var configFileMap = new ExeConfigurationFileMap { ExeConfigFilename = Path.Combine(_cfgPath, configFile) };
37 
38        var config = ConfigurationManager.OpenMappedExeConfiguration(configFileMap, ConfigurationUserLevel.None);
39 
40        EnvCd = envConfig.AppSettings.Settings["environment"].Value.ToLower();
41        CustomConfig = (CustomConfiguration)config.GetSection("CustomConfigSection." + EnvCd);
42    }
43}

This code is where it all comes together. A utility class called Config is built as a singleton class to avoid reading the config file over and over. The first time it is referenced it will read the file with the private constructor. If you ignore the location of the file (_cfgPath) for a second, the code simply reads your environment.config file to see which environment applies here. Then it reads the corresponding section from common.config and stores that configuration in the CustomConfig property which any piece of application code can read from.

Regarding the file location: you can easily pick some standard area (we picked a custom folder under Program Files (x86) for ours that will ALWAYS be the location of your config files. Then just leave the 2 commented out lines in the constructor commented out and make sure you initialize the path correctly in the top of the class. We chose to read from a local path to the source executables when available to aid developers, and to fall back to a standard path if it was not available. It’s not a huge difference, so just choose an approach for locating the files that works for you.

The Application Code

The application code is really simple, then, for ANY application (web site, web service, web api, console app, wcf service, wpf app; even other class libraries) to access. You just include a reference to your library assembly containing the Config class (and your CustomConfiguration class as well, I would imagine) and then write some code that looks like this:

1var connStr = Config.Instance.CustomConfig.MyMainDbConnection;

Pretty easy — and you get strong types with IntelliSense to boot!!!

Deployment

In order to set this up on all of your different machines, you need to get the common.config and environment.config put on each machine. Then you have a one-time-only touch of the environment.config on each machine to set which environment it belongs to. You can deploy the common.config file with every deploy action or however you deem appropriate. The environment.config file is generally never touched unless your machines change the environments they support. For us, staging machines will always be staging machines, and the same for production, so we never touch them.

That’s it! Happy configuration. 🙂