Wednesday, January 30, 2013

A simpler approach to .NET Config File Transformations

One of the nicer features of ASP.NET 4.0 is the concept of configuration file transformations. In case you are not familiar with it, here's a quick two-paragraph overview:

ASP.NET websites almost always need a different configuration for each deployment environment. The development/test configuration will differ in important ways from the staging configuration, and again from the production/live configuration. However, these differences are often only a smart part of the configuration file: a few URLs, server names and assorted settings. Before 4.0, the typical way to manage this was to maintain a separate, but 90% identical, web.config for each environment, and somehow make the right file the 'real' web.config at deployment. Keeping the files in sync was not a trivial task, especially with large projects. We had one application with a config that was well over a thousand lines, and the various environment 'editions' have gotten so out of sync with each other that we spent an embarrassing amount of time squashing bugs that would only manifest in one environment (which of course was usually production).

Web.config transformations brings some relief to this problem. With this feature, you have the web.config, and then it has several child files, one for each build configuration. The primary web.config contains all the information, and then each child file contains instructions for modifications. For example, the primary web.config might contain a section defining the test database connection string, and then the child file web.release.config contains an instruction to replace that section with the production database connection string. When you publish the website, it will transform the primary web.config based on the child config file that corresponds to the build configuration being published, resulting in a file that is valid for the environment. (These 'instructions' are an XML transformation language developed by Microsoft.)

As useful as this feature is, there are some difficulties.

The main difficulty is that transformations are not created at build time. This can make them a little hard to validate, because you have to go out and use a secondary tool or script to execute the transforms and check them over. You can do this from the command-line like so:

msbuild /nologo /target:TransformWebConfig /p:Configuration=Release IISHost.csproj

This will create the the transformed config (for the Release build configuration) in obj\Release\TransformWebConfig\transformed\Web.config.

In the spirit of making this a usual part of the build process, we attempted to include this as a post-build event. However, because this MSBUILD task actually compiles the project, this creates a circular dependency where the project is built, and then the post-build event kicks off, which builds the project a second time, which kicks off the post-build event, which keeps looping back on itself infinitely until your computer crashes.

Not generating the transforms at build-time also causes issues for deployment. Transforms are generated when a website is published, but the Publish mechanism is not the best or preferred way to deploy. In enterprise environments especially, the deployment team typically does not have development tools such as Visual Studio or MSBUILD installed, and generally just wants to have a folder of files they can copy to the correct directory. (Publish also does not lend itself to managing backups, versioning, or rollbacks.) So build engineers and deployment teams have to manually execute the transform, and then go through and copy and rename files just as they did before ASP.NET 4.0.

The third issue is that it's also only available for web.config files, not for app.config files. Now, there are plug-ins that add this feature for non-web projects, such as SlowCheetah. SlowCheetah is a good solution, but I was always hopeful I could find a simpler, more integrated approach. A number of articles, especially this one, told me it was possible, but their solutions were more complex than I really wanted. They did however guide me to this conclusion:

It is possible to execute the transform as a native part of the build by adding some elements to the project file. These elements cannot be added through the Visual Studio GUI (as far as I know), so you do have to manually edit the project file, but these additions are very small, simple bits of XML, so it's not too daunting.

In a website project, at the bottom of the file (just before the </Project> element), add the following:

<Target Name="AfterBuild">
    <TransformXml Source="web.config"
                  Transform="web.$(Configuration).config"
                  Destination="obj\$(Configuration)\web.config" />
</Target>

The target name is very important; that's the part that tells the compiler to run the TransformXml task as an after-compile task. (Because it is not making an external, recursive call, the afore-mentioned infinite loop condition is not created.) Once this section is added, every build will produce, in the obj folder corresponding to the build configuration just compiled, the transformed web.config! The beauty of this solution is that the transformed config is always available when you want to validate it (or when build engineers or deployment teams need to copy it) but the default, development web.config remains in place and untouched.

TransformXml is included by default for website projects. You can do the same thing for app.config files, but you have to explicitly import the task in non-web project files. You can do this by including the following line before the <Target> element:

<UsingTask 
   TaskName="TransformXml" 
   AssemblyFile="$(MSBuildExtensionsPath)\Microsoft\VisualStudio\v$(VisualStudioVersion)\Web\Microsoft.Web.Publishing.Tasks.dll" 
/>

(The DLL referenced here is included as part of the Visual Studio install, so you shouldn't have to add any packages or features. It does not need to be included in the Project References either.)

Being able to automate the configs in this manner has been very helpful for us. In my next post, I'll go into detail about how we were able to leverage this knowledge to accomplish even more advanced tasks.

EDIT: If you have other post-build tasks that rely on the transformed web.config, you should use "BeforeBuild" as the target name. There appears to be a slight delay between the build finishing and the final file being written out. To avoid any race conditions, use "BeforeBuild" instead of "AfterBuild".)

EDIT 2 (7/15/2016): I have modified the <UsingTask> path to use $(VisualStudioVersion) so the project file is more maintainable.