Context

Every developer knows that managing your app.config or web.config isn’t an enjoyable task. When you’re moving your application from one environment to another, you will need to change some variables. Maybe you’re working with automatic builds, and so on.

Since Visual Studio 2010 you have a new feature: “Config transformations”. Because I’m still working with Visual Studio 2008, I can’t use it. That’s why I wrote my own config transformer.

Which procedure?

There is more than one way to achieve our result, an environment specific app.config or web.config. You could:

None of the above really gave me what (I thought) I needed. Nevertheless, they all had some good ideas. So I’ve taken some principles from the blog posts and created my own solution.

My requirements

Personally, I like it when all the environment variables are in the same file. So you can consult them at the same time and you only need to maintain a single file. So I went back to the .INI file from the “good old days” (read: from my teen years when PC games had .ini files, which I edited to tweak the settings).

I don’t need to change my current config file, because the names of my variables are already unique. I find it also important that I only have one main config file, because the config file will hold other settings which are used in all environments. FE: the < configSections > will remain the same, also the , etc. So if you would chose to use a separate config file for every environment, you will need to maintain all files for every change.

My application code will also remain the same, I just request a variable and it will give me a value.

Changes in my solution

As I mentioned before, I don’t want a lot of change to my existing application. So what does change? We need to take three simple steps in order to get to the “dynamic” config file.

1. Create custom solution configurations

We are going to create a solution configuration for every environment. I created three: development, staging and production. Important: notice that my Development configuration copies the settings from Debug and my Production from Release.

Create Solution Configuration - Step 1


Create Solution Configuration - Step 2


Create Solution Configuration - Step 3

2. Create the .INI file

Insert the exact name of your environments between [brackets] and list the variable names with their value like the images below.

Create Ini - Step 1


Create Ini - Step 2

3. Define the a post-build event

Now that everything is in place we need to call the magic that puts everything together. So go to the post build events of your project and call the EnvironmentParser.exe with the right arguments.

Post Build Event

 

EnvironmentParser?

Halas, the variables won’t be pulled automatically out of the INI file and update the config. So I created a little console app in .NET 3.5 that will handle this. Currently, it supports to edit the AppSettings and ConnectionStrings configuration sections.

Keep in mind, when the solution configuration is changed, the config file will get checked out because it will be modified. Also, notice the “_hasChanges” field. When no changes are detected, the file will not be saved, so it won’t be checked out!

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Xml.Linq;

namespace EnvironmentParser
{
    class Program
    {
        private static bool _hasChanges = false;

        static void Main(string[] args)
        {
            try
            {
                string environment = args[0];
                Dictionary<string, string> variables = GetVariables(environment, Path.Combine(args[1], args[3]));

                if (variables.Count > 0)
                {
                    XDocument config = XDocument.Load(Path.Combine(args[1], args[2]));
                    SetEnvironmentVariables(config, variables);

                    if (_hasChanges)
                        config.Save(Path.Combine(args[1], args[2]));
                }
            }
            catch (Exception)
            {
                Environment.Exit(1);
            }
        }

        private static Dictionary<string, string> GetVariables(string environment, string iniFilePath)
        {
            Dictionary<string, string> result = new Dictionary<string, string>();

            List<string> iniContent = File.ReadAllLines(iniFilePath).ToList();

            int startIndex = iniContent.IndexOf(string.Format("[{0}]", environment));
            if (startIndex >= 0)
            {
                int currentIndex = startIndex + 1;
                int operatorIndex = 0;
                string currentItem = null;

                while (currentIndex < iniContent.Count && !string.IsNullOrEmpty(iniContent[currentIndex]))
                {
                    currentItem = iniContent[currentIndex];
                    operatorIndex = currentItem.IndexOf('=');

                    result.Add(currentItem.Substring(0, operatorIndex).Trim(), currentItem.Substring(operatorIndex + 1).Trim());

                    currentIndex++;
                }
            }

            return result;
        }

        private static void SetEnvironmentVariables(XDocument config, Dictionary<string, string> variables)
        {
            foreach (KeyValuePair<string, string> configVariable in variables)
            {
                var q = (
                         from x in config.Descendants("configuration").Descendants("appSettings").Descendants("add")
                         where x.Attribute("key").Value.Equals(configVariable.Key)
                         select x
                        ).FirstOrDefault();

                if (q != null)
                {
                    if (!q.Attribute("value").Value.Equals(configVariable.Value))
                    {
                        _hasChanges = true;
                        q.Attribute("value").Value = configVariable.Value;
                    }
                }
                else
                {
                    q = (
                         from x in config.Descendants("configuration").Descendants("connectionStrings").Descendants("add")
                         where x.Attribute("name").Value.Equals(configVariable.Key)
                         select x
                        ).First();

                    if (!q.Attribute("connectionString").Value.Equals(configVariable.Value))
                    {
                        _hasChanges = true;
                        q.Attribute("connectionString").Value = configVariable.Value;
                    }
                }
            }
        }
    }
}