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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
<?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=“dev” type=“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.
- Add reference to “System.Configuration” in your project.
- Add a class named “TargetSystemConfigSection” under the namespace “Tedd.AclConfig” and have it inherit “ConfigurationSection”.
- 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. - For each section define a section collection.
- For each collection define a config element.
- A class “TargetConfigLoader” is used to read config, check ACL and copy appSettings/connectionStrings.
- 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 |
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 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:
- Add a new class project to your solution, name it “Tedd.AclConfig”.
- Add reference to “System.Configuration” in “Tedd.AclConfig”.
- In “Tedd.AclConfig”, create a new class file named “TargetConfig”.
- Empty the new class file and copy the CS-code from above into it.
- Copy the parts of the required configuration into your web.config or app.config.
Hint: You want “targetSystems” as well as “sectionGroup” under “configSections”. - Add a reference to “Tedd.AclConfig” from your main project.
- 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.