The first thing you'll notice as you start working with ASP.NET 4, is that they've finally solved the problem with deploying web.config files. This is a major time sink in our team as we spend hours every sprint updating configuration files on our environments. Not anymore, right!? Wrong! They only solved the problem for web.config. There's still no way to do the same thing (that I've found) for other XML-based configuration files. If you're like me, you probably like to extract configuration from web.config into other files (like log4net.config) to make it more manageable. These do however not work under the .NET 4 Web Deployment Tool.
Do it with XSLT
What Microsoft has created is a simplified version of XSLT. They have however also supplied the solution to our problem in the latest version of their build system MsBuild. The task is called XslTransformation and resides in Microsoft.Build.Tasks.v4.0.dll.
The problem of different environments
My problem is that I work with the same project on different computers, and I have a continuous integration mechanism that deploys my solution continuously to a test environment. Every environment has its own database connectionstring, and further on they will have different configurations for logging, caching, etc. The web deployment tool will take care of individual changes for each environment concerning web.config, but my integration test project stores its database connection string in App.config which leaves me totally screwed.
The solution is to specify indiviual changes for each environment
My App.config looks like this.
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<connectionStrings>
<add name="ApplicationDatabase"
connectionString="Data Source=SERVER\SQLEXPRESS;Initial Catalog=ApplicationDatabase;Integrated Security=True;"
providerName="System.Data.SqlClient" />
</connectionStrings>
</configuration>
And I create change files for each and every computer that I have. The name between App and config is the name of the environment, or rather the value of $(Computer) in MSBuild. If you have several environments on the same machine you might need to start using Build Configurations in Visual Studio and select your configuration change file on that.

Now, let's look at what that MAIA (my main developer machine) configuration looks like.
<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:msxsl="urn:schemas-microsoft-com:xslt" exclude-result-prefixes="msxsl">
<xsl:output method="xml" indent="yes"/>
<!-- Default template -->
<xsl:template match="node()|@*">
<xsl:copy><xsl:apply-templates select="node()|@*"/></xsl:copy>
</xsl:template>
<!-- Connection string replacement template -->
<xsl:template match="/configuration/connectionStrings/add[@name='ApplicationDatabase']">
<add name="ApplicationDatabase"
connectionString="Data Source=MAIA\SQLEXPRESS;Initial Catalog=ApplicationDatabase;Integrated Security=True;"
providerName="System.Data.SqlClient" />
</xsl:template>
</xsl:stylesheet>
The first part after the XML declaration means, "match every node and attribute and apply a template that matches" which will be recursive since that template matches just about everything. If you write another template matching something more precise like the connectionString configuration, that will override the base rule and let us change the contents of the node that we're matching. This is now applied before every build I make. This is done by opening up the csproj-file (which is a msbuild script) and manually adding the following at the end.
<Target Name="ApplyMachineSpecificConfiguration" Condition="Exists('App.$(Computername).config')">
<XslTransformation XmlInputPaths="App.config" XslInputPath="App.$(Computername).config" OutputPaths="App.config_output" />
<Copy SourceFiles="App.config_output" DestinationFiles="App.config" />
</Target>
<Target Name="BeforeBuild">
<CallTarget Targets="ApplyMachineSpecificConfiguration"/>
</Target>
The target BeforeBuild is by default run before any compilation and that is where we call our target that will apply the XSL on App.config. We store the result in a temporary file, because the XslTransformation does not like it when we try to overwrite the XML-file that we're reading from. Now I can edit all configuration from one place and do not have to worry about manually merging between environments anymore. Sweet! Inspiration to this article came mainly from Fredrik Knutson.

13 Comments
Mikael Lundin said
@Robert On my current project we don't need unique configuration for every developer, but only for the different environments <em>development</em>, <em>test</em>, <em>staging</em>, <em>production</em>. This works like charm, because we treat the configuration files as any other piece of code. On commit, the correct transformation file is chosen for the target environment and transformed.
Using different configuration transform files for each and every developer on the team, could also work, but there would be a risk that your transformation file for your indiviual development environment could get outdated when the base configuration changes dramatically, like when refactoring.
Right now, we have a "default" transformation that we apply first, that contains all the defaults. Then we transform the specific environment changes to that default. We've been using it for 1 month on 8 different environments without any problems.
Mikael Lundin said
I did run into that while attempting this on a TFS project. The problem then was not that I didn't have access to App.config, but rather that TFS made it readonly as it where not checked out. I solved it with the attrib task to remove the readonly attribute on the file, copy over the file and add the readonly attribute again to make sure that TFS does not warn me that the file has been changed outside the control of TFS. I don't have the code here but it looks something like this
<br/><br/>
<Attrib Files="App.config" ReadOnly="false" /><br/>
<Copy SourceFiles="App.config_output" DestinationFiles="App.config" /><br/>
<Attrib Files="App.config" ReadOnly="true" /><br/>
The Attrib task seems to be a part of the msbuild community tasks. You'll find them here: http://msbuildtasks.tigris.org/
Hope this will solve your problem.
Cindy said
Thanks Mikael and Fredrik for sharing this info! I just recently tried this out but am getting the error:
Unable to copy file "App.config_output" to "App.config". Access to the path 'App.config' is denied.
Thoughts?
Mikael Lundin said
There is also away to get this working with eariler versions of .NET using the Xslt task from msbuild community tasks. Great if you're not on a .NET 4 project.
http://msbuildtasks.tigris.org/
Mikael
Fredrik Knutson said
Nice!
We've integrated this in our builds on a build server and have different transformation files depending on the build configuation currently beeing built. In addition we've set up different build configurations for our different deployment environments, TEST, PROD and so on.
/Fredrik
Robert said
Nice idea,
but won't this lead to endless version conflicts on a team? You want *.config versioned, but with all devs rewriting it every build, you'll get conflicts often, and real changes that are not caused by automatic rewriting may easily be lost?
Sean Hederman said
A slight improvement (well for me anyway): rather do it AfterBuild and have it update the output .config. This avoids the issue of checking out and/or changing app.config.
Great concept! Have been doing something similar using XSLT, but your way is much better than mine.
Encrypt your web.config with MSBuild « Mint said
[...] use that to encrypt your web.config. By having those unencrypted versions in version control, and building fresh configuration files for every deploy – it becomes quite a blast to let msbuild encrypt sensitive data just before [...]
Encrypt your web.config with MSBuild « Mint said
[...] use that to encrypt your web.config. By having those unencrypted versions in version control, and building fresh configuration files for every deploy – it becomes quite a blast to let msbuild encrypt sensitive data just before [...]
Denis Gladkikh said
I solve this problem with little tool http://ctt.codeplex.com/ which I wrote and which I use with CCNet for making packages.
Jesse House said
Nice! I was checking out the built in TransformWebConfig, but got really disappointed when I found out it only worked on Web.config files, not on App.config. Great solution!
Suresh said
Good post.
My environment is similar to Mikael Lundin saying in his comment.
Is my understanding correct, in that, the transformation is tied to the build process in pre-build/post-build? Now, are you runing a separate build and create deploymnet packages for each of the environment? For me that would mean, I am not deploying what I tested to the next environment. It would be good to have a way to apply the configuration changes on a deployment.
Mikael Lundin said
Yes that is correct. I apply environment specific configuration with transformation after build, but before deploy.
I have no testing of the configuration before it is deployed to target environment. That is why I have several test environments.
* Deployment - where continuous integration will deploy to on every checking
* Test - for acceptance testing
* Staging - that is identical to production
* Production
Staging is really the environment where I test the production specific configuration. This configuration is identical to production, so if there's any problem with configuration it will be found here, before changes go live.
Makes sense?