Programming July 14, 2010

Manage multiple web.config files using Phantom

A year ago I wrote about how we handled having different configuration files for different development environments in the project that I was working on using build events and Powershell. I’m currently working on a project where we do something similar, but in a more elegant and flexible way.

We’re using a build system named Phantom which is created by Jeremy Skinner. Using Phantom we write build scripts in Boo, a fairly new CLR language with a syntax similar to Python’s.

boo In case you’re thinking “Phantom!? Boo!? What the deuce!?” and feel that your browser’s back button is starting to look attractive, hold on for a second. It’s really quite simple and I found that compared to many other systems for building .NET applications it’s extremely easy to get started using Phantom.

And I promise you’ll find a link to a downloadable sample project in the end if you keep reading :-)

Multiple developers, multiple environments

On many projects, as soon as you have more than one environment that you need to run the application on, you need to vary the application’s configuration depending on the characteristics of that machine. Sometime you need to refer to the program files folder whose name differs between CPU architectures and language versions of Windows. In other cases the application may be storing files on the machine’s local hard drives and that location varies from machine to machine. Sometime you need to run the application using a different local user for each machine.

I could go on for ever, but I think you get my point: while most of the configuration specified in web.config usually remains the same we need the ability to change some settings depending on which environment we’re running the application (web site in this case) on.

The no-solution-solution

One common “solution” to this problem that I’ve seen a lot is that people check in web.config into their source control server. When someone checks out the project he or she makes whatever changes to the web.config file that is necessary to run the project on their local machine, but never checks in these changes. That way the team has some form of base web.config in source control. Whenever there is a general change that needs to be done to web.config the developer that is making it needs to undo his changes, perform the changes that need to be done and check in the updated web.config. All developers then update their web.configs to the new version and redo whatever changes they have to make to make it work on their local machines.

This approach of course comes with quite a few problems. It’s extremely time consuming and very error prone. Developers that are new to the project often unknowingly commits their changes to web.config and developers that are aware of the routines often commit their local changes by accident.

Another problem is that you end in a situation where you have a lot of different configurations that aren’t under source control.

Let’s call this solution “the no-solution-solution” :)

Transforming web.config – the Visual Studio approach

You can create files that defines transformations that are made to web.config. Which file, or transformation definition, is used depends on the active solution configuration (Release, Debug, custom ones). This is a pretty nice solution and I encourage you to look at it. Here’s a good article on the subject.

I have one problem with this solution though. It forces you to use XML to define rules/transformations, and I’m not a big fan of having to edit XML by hand as a developer.

Using a build script

A third option to address the problem of needing to have multiple configuration files is to create a script that gets executed every time the solution is compiled that does some clever copying of files. There are a number of ways to create such a script using different build systems. As I’ve mentioned before we’re using a system named Phantom.

solution-explorer-initial-setup

A simplified scenario

Let’s say that we have a standard ASP.NET MVC project (the project type doesn’t really matter) and a separate module named ConfigurableModule. ConfigurableModule only contains a single class, Configuration, which has a message property. When we load the home page of our web application the contents of the message property is displayed.

Our configuration looks like this:

<configuration>
    <configSections>
        <section name="myConfigurableModule" 
            type="ConfigurableModule.Configuration, 
                    ConfigurableModule"/>
    </configSections>
    <myConfigurableModule 
        message="Hello from standard web.config!" />
    <!-- Other configuration -->
</configuration> 

Now let’s say that this configuration section sometimes needs to vary from developer to developer. At the same time we need to maintain a standard version of the configuration.

Of course this is a simplified scenario. In our case we have several config sections that needs to differ. For instance the build- and test-server needs a different authorization section, some developers need different configurations for the log4net configuration and some need different configurations for the CMS that we’re using.

Our solution using Phantom

Our solution is to have a build script that looks for configuration files in a specific directory. For each of these files it looks to see if there is a file with the same name in a subfolder of the configuration folder that has the same name as the computer that it is currently building on. If it does it will copy that file to the web projects root, overwriting any existing file that is already there. If it doesn’t it will instead copy the file that it found in the configuration folder.

What’s nice about this is that a developer that has a computer that matches the standard configuration in the project and that doesn’t need any local changes to the configuration can just get the project from source control, build it and then be up and running. While at the same time if any local changes is needed all he or she has to do is create a new folder for his or hers computer and add whatever files he or she needs. And since those files wont interfere with other developers environment he or she can check them in into source control

Another thing that is nice about this is that we’re also able to do a bunch of other stuff when the project is compiled using our build script and all of that logic, both for handling configuration files and the other stuff is handled in the same place.

Step one – creating separates file for the configuration section

To accomplish this we begin by breaking out the configuration section into a separate file and moving both that and web.config to a new directory named Configuration that we create.

solution-explorer-defiant-folder-added Next we add a subfolder for each computer that we will build this application on to the Configuration folder. That doesn’t mean that we need to know that upfront though, we can easily add new folders later on.

Each folder should have the same name as the computer’s name. In my case my development machine at home is named Defiant so my folder structure now looks like the image to the right.

Next we copy the myConfigurableModule.config file into each folder that we have created for the different build environments and make any adjustments to them that we’d like. In my case I’m changing the message to “Hello from Defiant!”.

Step two – creating the build script

Next we create our build script which we name build.boo and place in the root folder of our solution.

The script looks like this:

import System.IO

project_dir = "PhantomExample"
config_dir = project_dir + "/Configuration"

desc "Copying configuration files"
target copyconfiguration:
  with FileList(config_dir):
    .Include("*.config")
    .ForEach def(file):
      file_to_copy = "${config_dir}/${file.Name}"
      machine_specific_file = "${config_dir}/${System.Environment.MachineName}/${file.Name}"
      if File.Exists(machine_specific_file):
        file_to_copy = machine_specific_file
      print "Copying ${file_to_copy} to ${project_dir}/${file.Name}"  
      File.Copy(file_to_copy, "${project_dir}/${file.Name}", true)

The script does what I described before. For each file in a folder named Configuration that is located in the project’s directory it check if a machine specific file with the same name exists and assigns that file’s path to a local variable. If it doesn’t exists it uses the path to the file in the Configuration folder. It then proceeds to copy that file to the project’s root directory.

Should we want to we could quite easily extend this to first look for a machine specific file, then perhaps a CPU architecture (32/64 bit) specific file, or a operating system language specific file, and then finally use the standard file if no other is found. My point is that since we’re using a programming language that has access to the entire .NET framework to define how we handle our files we can do some pretty advanced stuff should we need to.

Step three – Getting Phantom

Next we download Phantom. The “main branch” of it on GitHub is Jeremy Skinner’s and he also has a ready to use download package. In our case we’re actually currently using my fork of the project which in turn is a clone of Emil Cardell’s fork to which Emil has added some really nice extra features. For the sake of this tutorial Jeremy Skinner’s branch works fine though. Also, if you download the sample code you’ll get that included.

Anyway with Phantom downloaded we copy the entire Phantom folder into a new folder in our solution directory that we name “libs”.

Our solution directory, and the Phantom folder, should now something like the screenshot below.

libs-folder

Step four – Executing the script

At this stage we could execute our script by navigating to the solution folder and typing in the relative path to Phantom.exe followed by the target that we’d like to run in our script (copyconfiguration). It would look something like this:

c:\Projects\PhantomExample>libs\Phantom\Phantom.exe copyconfiguration
Phantom v0.2.0.0
Copyright (c) Jeremy Skinner 2009 (http://www.jeremyskinner.co.uk)
http://github.com/JeremySkinner/Phantom

Targets specified: copyconfiguration

copyconfiguration:
Copying PhantomExample/Configuration/DEFIANT/myConfigurableModule.config to Phan
tomExample/myConfigurableModule.config
Copying PhantomExample/Configuration/Web.config to PhantomExample/Web.config

However, since Phantom will look for a file named build.boo in the path that it’s invoked from this means that we always need to invoke it from the solution directory. There are probably many possible workarounds for that. One that I found works quite well is to simply add a batch file to the solution directory that runs Phantom and passes along any arguments to it but first ensures that the current execution path is the same that the batch file it self is located in. The file, which is named build.bat, looks like this:

@echo off
:: Change to the directory that this batch file is in
:: NB: it must be invoked with a full path!
for /f %%i in ("%0") do set curpath=%%~dpi
cd /d %curpath% 
libs\phantom\phantom.exe %*

Finally we as we want to execute the copyconfiguration target each time the project is built we add a couple of lines to web projects pre-buid event to have it executed.

pre-build

Now, when we compile the web project it will automatically copy the correct configuration files to it’s root directory and in my case the home page of the web application will say “Hello from Defiant!”.

Source code

You can download the source code for the sample project that we built during this tutorial here. To try it out run the site and view the message that it shows. Then create a new subfolder under the configuration folder that you name with your computers name and create your own configurableModule.config with a different message.

Conclusion

We’ve only scratched the surface of what we can do with a build system such as Phantom in this post and I hope to be able to write more on the subject in the future. In the meantime I recommend you to take a closer look at Phantom as well as on the Boo language. The Phantom wiki is an excellent place to get started.

PS. For updates about new posts, sites I find useful and the occasional rant you can follow me on Twitter. You are also most welcome to subscribe to the RSS-feed.

Joel Abrahamsson

Joel Abrahamsson

I'm a passionate web developer and systems architect living in Stockholm, Sweden. I work as CTO for a large media site and enjoy developing with all technologies, especially .NET, Node.js, and ElasticSearch. Read more

Comments

comments powered by Disqus

More about Programming