Multiple deployment configs in one config file with ACL

By | 2014.09.30

Most projects that is deployed into production require some kind of config change upon deployment. Often it is the connection string that varies, but it can be any number of custom settings. Forgetting to change the config file when deploying, or forgetting to change config back on your dev machine after deploying can have disastrous consequences.

Back in 2005 Scott Guthrie suggested keeping separate config files and adding pre build event handler to copy correct file into place. Scott Hanselman suggested a similar approach. Visual Studio 2010 has a XSLT-based debug/release transformation upon deploy, but it has its own set of problems.

Though these solutions are fine and will do the job they do have a couple of drawbacks.

  • They still require you to remember to switch build profile.
    Failing to do so may end in prod system working on dev database, or development happening directly on prod system. On some projects this is not such a big thing, while on others it would be flat out catastrophic. If you are in the latter group you know what I’m talking about.
  • You need to maintain multiple copies of your web.config/app.config.
    Every time you add a reference or add a NuGet package that alters your config you need to manually update the other config files.

So I suggest an alternative approach. By adding a custom config section which contains the necessary configuration we can keep multiple configurations in one config file. By adding ACL (Access Control Lst) to this we can make sure it never runs on the wrong machine We’ll process the directive early on and inject the config into the usual places (connectionStrings and appSettings) so the rest of the app will work as expected without any change required.

The configuration

A sample config file might look something like this:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <!-- configSections must be first element. -->
  <configSections>
    <!-- ConfigSections for different configurations. -->
    <sectionGroup name="targetSystems">
      <section name="dev" type="Tedd.AclConfig.TargetSystemConfigSection, Tedd.AclConfig.WindowsFormsApplication"/>
      <section name="prod" type="Tedd.AclConfig.TargetSystemConfigSection, Tedd.AclConfig.WindowsFormsApplication"/>
    </sectionGroup>
  </configSections>

  <startup>
    <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
  </startup>

  <connectionStrings>
    <clear />
  </connectionStrings>

  <targetSystems>
    <dev>
      <connectionStrings>
        <add name="DefaultConnection" connectionString="Data Source=(LocalDb)\v11.0;AttachDbFilename=|DataDirectory|\LocalDB.mdf;Initial Catalog=LocalDB;Integrated Security=True" providerName="System.Data.SqlClient" />
      </connectionStrings>

      <appSettings>
        <add key="System" value="Development" />
      </appSettings>

      <!-- Regex allow/deny pattern for hostnames this configuration is allowed to run on. -->
      <allowedComputers>
        <add hostname="dev\-machine\-.*" access="Allow" />
        <add hostname=".*" access="Deny" />
      </allowedComputers>
    </dev>

    <prod>
      <connectionStrings>
        <add name="DefaultConnection" connectionString="Server=myServerName\myInstanceName;Database=myDataBase;User Id=myUsername; Password=myPassword;" providerName="System.Data.SqlClient" />
      </connectionStrings>

      <appSettings>
        <add key="System" value="Production" />
      </appSettings>

      <!-- Regex allow/deny pattern for hostnames this configuration is allowed to run on. -->
      <allowedComputers>
        <add hostname="PRODSERVER.*" access="Allow" />
        <add hostname=".*" access="Deny" />
      </allowedComputers>
    </prod>
  </targetSystems>

  <appSettings>
    <!-- Selects <targetSystems> profile. -->
    <add key="targetSystem" value="dev" />
  </appSettings>

</configuration>

 

What we have done here is to add the “sectionGroup” inside the “configSections” tags. If your config file doesn’t contain the “configSections” tags they must be added too.

Note that the first parameter in “section type” is the class where our config is located, the second parameter is the assembly (class, assembly). You need to modify the second parameter to match the assembly name of your project (or simply follow the instructions in “Summary”).

Some explanation

First of all we’ll need to define the section type, shown as “<section name=devtype=Tedd.AclConfig.TargetSystemConfigSection/>” in the config file. We start at this level so that we allow you to add any number of configurations to the system simply by adding sections in your config file.

  1. Add reference to “System.Configuration” in your project.
  2. Add a class named “TargetSystemConfigSection” under the namespace “Tedd.AclConfig” and have it inherit “ConfigurationSection”.
  3. Define 3 sections: connectionStrings, appSettings and allowedComputers.
    Note! If you want any attributes directly onto the tag (ie. <dev name=”Development”>…) then simply add a ConfigurationProperty of type string, int, bool or whatever you need.
  4. For each section define a section collection.
  5. For each collection define a config element.
  6. A class “TargetConfigLoader” is used to read config, check ACL and copy appSettings/connectionStrings.
  7. We need to call TargetConfigLoader.InitConfig() first thing when application starts. This can be done by adding a line to the startup of your application. Different project types have different startup codes.
    • Console/WinForms/WPF: As first line in “Main” function in Program.cs.
    • Web project: As first line in “Application_Start()” in Global.asax. If your project doesn’t have a global.asax-file you need to add it first.

 

The code

using System.Collections.Generic;
using System.Configuration;
using System.Linq;
using System.Reflection;
using System.Security;
using System.Text.RegularExpressions;

namespace Tedd.AclConfig
{
    public class TargetSystemConfigSection : ConfigurationSection
    {
        [ConfigurationProperty("appSettings", IsRequired = true)]
        public AppSettingsCollection AppSettings
        {
            get { return (AppSettingsCollection)this["appSettings"]; }
        }

        [ConfigurationProperty("connectionStrings", IsRequired = true)]
        public ConnectionStringsCollection ConnectionStrings
        {
            get { return (ConnectionStringsCollection)this["connectionStrings"]; }
        }

        [ConfigurationProperty("allowedComputers", IsRequired = true)]
        public AclCollection AllowedComputers
        {
            get { return (AclCollection)this["allowedComputers"]; }
        }
    }

    //
    // To implement these sections we need to create a collection object and an element object for each section. Since the collection object is mostly the same we’ll implement a generic base class for them. 
    //
    public abstract class GenericConfigurationElementCollection<T> : ConfigurationElementCollection
        where T : ConfigurationElement, new()
    {
        public T this[int index]
        {
            get { return base.BaseGet(index) as T; }
            set
            {
                if (base.BaseGet(index) != null)
                    base.BaseRemoveAt(index);
                this.BaseAdd(index, value);
            }
        }

        public T this[string key]
        {
            get { return base.BaseGet(key) as T; }
        }

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

    //
    // Now we can implement each section. 
    //
    public class AppSettingsCollection : GenericConfigurationElementCollection<AppSettingsElement>
    {
        protected override object GetElementKey(ConfigurationElement element)
        {
            return ((AppSettingsElement)element).Key;
        }
    }

    public class ConnectionStringsCollection : GenericConfigurationElementCollection<ConnectionStringElement>
    {
        protected override object GetElementKey(ConfigurationElement element)
        {
            return ((ConnectionStringElement)element).Name;
        }
    }

    public class AclCollection : GenericConfigurationElementCollection<AclElement>
    {
        protected override object GetElementKey(ConfigurationElement element)
        {
            return ((AclElement)element).Hostname;
        }
    }

    //
    // And then the elements. Each have unique properties so we’ll implement them uniquely. 
    //
    public class AppSettingsElement : ConfigurationElement
    {
        [ConfigurationProperty("key", IsRequired = true)]
        public string Key
        {
            get { return this["key"] as string; }
        }

        [ConfigurationProperty("value", IsRequired = true)]
        public string Value
        {
            get { return (string)this["value"]; }
        }
    }

    public class ConnectionStringElement : ConfigurationElement
    {
        [ConfigurationProperty("name", IsRequired = true)]
        public string Name
        {
            get { return this["name"] as string; }
        }

        [ConfigurationProperty("connectionString", IsRequired = true)]
        public string ConnectionString
        {
            get { return (string)this["connectionString"]; }
        }

        [ConfigurationProperty("providerName", IsRequired = true)]
        public string ProviderName
        {
            get { return (string)this["providerName"]; }
        }
    }

    public class AclElement : ConfigurationElement
    {
        public enum AclAcceptRejectEnum
        {
            Allow,
            Deny
        }

        [ConfigurationProperty("hostname", IsRequired = true)]
        public string Hostname
        {
            get { return this["hostname"] as string; }
        }

        [ConfigurationProperty("access", IsRequired = true)]
        public AclAcceptRejectEnum Access
        {
            get { return (AclAcceptRejectEnum)this["access"]; }
        }

        public override string ToString()
        {
            return Hostname + ":" + Access.ToString();
        }
    }

    //
    // Thats all the code we need to support our new config sections. The config sections can be used directly, but we want to take it one step further and satisfy third party expectations by copying the data into the standard connectionStrings and appSettings. We also want to check ACL. 
    //
    public static class TargetConfigLoader
    {
        private static bool AclCheck(List<AclElement> aclList, string host)
        {
            foreach (var acl in aclList)
            {
                if (Regex.IsMatch(host, "^" + acl.Hostname + "$", RegexOptions.IgnoreCase | RegexOptions.Compiled))
                    return (acl.Access == AclElement.AclAcceptRejectEnum.Allow);
            }
            return false;
        }

        public static void InitConfig()
        {
            var targetSystemString = ConfigurationManager.AppSettings.Get("targetSystem");
            var targetSystem = (TargetSystemConfigSection)ConfigurationManager.GetSection("targetSystems/" + targetSystemString);

            // Check ACL 
            var aclList = targetSystem.AllowedComputers.Cast<AclElement>().ToList();
            var host = System.Environment.MachineName;
            var allowed = AclCheck(aclList, host);

            if (!allowed)
                throw new SecurityException("Application is running on host \"" + host +
                                            "\" which doesn't match allowed host list (regex): " +
                                            string.Join(",", aclList) + ".\r\n"
                                            + "This is a security precaution to avoid accidentially running production setting in test.\r\n"
                                            + "Please modify application config file to point to correct target system, or include this host in the desired target system.");

            // Add to appSettings 
            foreach (var appSetting in targetSystem.AppSettings.Cast<AppSettingsElement>())
            {
                ConfigurationManager.AppSettings[appSetting.Key] = appSetting.Value;
            }

            // Add to connectionStrings 
            // http://stackoverflow.com/questions/360024/how-do-i-set-a-connection-string-config-programatically-in-net
            var settings = ConfigurationManager.ConnectionStrings;
            var element = typeof(ConfigurationElement).GetField("_bReadOnly", BindingFlags.Instance | BindingFlags.NonPublic);
            var collection = typeof(ConfigurationElementCollection).GetField("bReadOnly", BindingFlags.Instance | BindingFlags.NonPublic);

            element.SetValue(settings, false);
            collection.SetValue(settings, false);
            foreach (var connectionString in targetSystem.ConnectionStrings.Cast<ConnectionStringElement>())
            {
                settings.Add(new ConnectionStringSettings()
                {
                    Name = connectionString.Name,
                    ConnectionString = connectionString.ConnectionString,
                    ProviderName = connectionString.ProviderName
                });
            }
        }
    }
}

 

Summary

To sum it all up here is a quick step-by-step guide to get up and running:

  1. Add a new class project to your solution, name it “Tedd.AclConfig”.
  2. Add reference to “System.Configuration” in “Tedd.AclConfig”.
  3. In “Tedd.AclConfig”, create a new class file named “TargetConfig”.
  4. Empty the new class file and copy the CS-code from above into it.
  5. Copy the parts of the required configuration into your web.config or app.config.
    Hint: You want “targetSystems” as well as “sectionGroup” under “configSections”.
  6. Add a reference to “Tedd.AclConfig” from your main project.
  7. Add “TargetConfigLoader.InitConfig();” as first executed line in your startup app.
    Console/WinForms/WPF: As first line in “Main” function in Program.cs.
    Web project: As first line in “Application_Start()” in Global.asax. If your project doesn’t have a global.asax-file you need to add it first.

Leave a Reply