October 14, 2010

MSBuild Environment Sandboxing

Sandboxing 101

In any non trivial, team oriented project you have multiple configurations:
  • team-member-one-sandbox, team-member-two-sandbox
  • QA
  • UAT
  • PROD-Americas, PROD-Europe, PROD-Asia
  • Etc
For each environment you want to configure database connection strings, web services, various limits and runtime settings; all possibly located across multiple configuration files. It is also convinient to reuse some sane defaults for a lot of the settings, and have the option to override some settings for a specific environment. Conceptually you want to have the following hierarchy of settings, where the most nested overrides or adds to the properties defined at the previous level:
  • Defaults
    • PROD-Defaults
      • Americas
      • Europe
      • Asia

.NET/MSBuild Solution

There are a couple of choices available:
  • T4
  • MSBuild
  • NAnt
  • Custom PostBuild event scripts in Visual Studio
  • An AdHoc combination of the above
When we were designing the solution described in this post, we wanted to keep the process as simple and as integrated with Visual Studio as possible, while at the same time making it simple to do automated builds from the command line. In the end, we were able to combine the Configuration functionality within with Visual Studio, with targeting of specific environments in a fairly natural way; if you follow this approach you will be able to select a target environment within VS via the configuration drop-down. At a very high level you will have the following: 1. One or more templated configuration files with ${setting1} for various configurable settings. For example you will have a bunch of settings in your app.config. 2. A set of configuration files containing the settings that need to be replaced in your templated files: The proposed solution will allow you to define your settings in the following configuration files:
  • defaults.cfg
  • prod-americas.cfg
  • prod-europe.cfg
  • prod-asia.cfg
3. A couple of custom MSBuild tasks in your CoolApp.csproj file. We will utilize open source build task library: 4. A couple of custom Configurations set up within Visual Studion (these go into CoolApp.sln file)


    <add key="setting_one" value="${setting_one}"/>


  <x key="setting_one">Hello World From DEFAULTS!</x>

debug.cfg (ie dev/qa/uat/etc)

  <x key="custom_settings">
    <add key="custom_setting_1" value="This is DEBUG custom setting ONE."/>
    <add key="custom_setting_2" value="This is DEBUG custom setting TWO."/>


  <x key="setting_one">Hello World From RELEASE!</x>
  <x key="custom_settings">
    <add key="custom_setting_1" value="This is RELEASE custom setting ONE."/>
    <add key="custom_setting_2" value="This is RELEASE custom setting TWO."/>

MSBuild Task Detail

<Import Project="$(MSBuildExtensionsPath)\MSBuildCommunityTasks\MSBuild.Community.Tasks.Targets" />
  <Target Name="ProcessConfig">
    <Message Text="==&gt;&gt;&gt;&gt; CurrentEnvironment = $(EnvId)" />
    <!-- BEGIN default (ie common) settings -->
    <XmlQuery XmlFileName="$(MSBuildProjectDirectory)\cfg\defaults.cfg" XPath="//settings/*">
      <Output TaskParameter="Values" ItemName="DefaultSettingValues" />
      <Tokens Include="%(DefaultSettingValues.key)">
    <!-- END defaults.config -->
    <!-- specific environment -->
    <XmlQuery XmlFileName="$(MSBuildProjectDirectory)\cfg\$(EnvId).cfg" XPath="//settings/*">
      <Output TaskParameter="Values" ItemName="SettingValues" />
      <Tokens Include="EnvId">
    <!-- Remove duplicates - this is important for "overrides" -->
      <Tokens Remove="%(SettingValues.key)" />
      <Tokens Include="%(SettingValues.key)">
    <Message Text="Parsed Tokens =&gt; %(Tokens.Identity): %(Tokens.ReplacementValue)" />
    <MakeDir Directories="$(ConfigOutDir)" />
    <TemplateFile Template="$(MSBuildProjectDirectory)\app.config" OutputFilename="$(ConfigOutDir)\$(EnvId).app.config" Tokens="@(Tokens)" />
  <Target Name="BeginProcessConfig">
      <EnvConfigFiles Include="$(MSBuildProjectDirectory)\cfg\*.cfg" Exclude="$(MSBuildProjectDirectory)\cfg\defaults.cfg" />
    <MSBuild Targets="ProcessConfig" Properties="EnvId=%(EnvConfigFiles.Filename)" Projects="$(MsBuildProjectFile)" />
    <!-- Copy over the correct file based on configuration in visual studio or command line -->
    <Message Text="EnvConfigIn = $(EnvConfigIn)" />
    <Copy SourceFiles="$(EnvConfigIn)" DestinationFiles="$(OutputPath)$(AssemblyName).exe.config" />
  <Target Name="AfterBuild" DependsOnTargets="BeginProcessConfig">
    <Message Text="This is the AfterBuild target: $(MSBuildToolsPath)" />

Demo Project

1. Install MSbuild Common Tasks

Note: the easy way to experiment is to just install via .msi installer, alternatively you can download the .zip file with all the binaries, but then you will have to configure the Import of msbuild tasks in the .csproj file - not difficult, and definitely something you want to do for self contain projects (typically you would include the binaries in some libs sub-folder of your project and reference it via relative paths from msbuild).

2. Download Source

View envcfgdemo-zip or if you behind an evil firewall: Download Demo Visual Studio 2008 Project

When opening the project you will be prompted with a dialog telling you that the project file is using some external extensions and whether you want to trust it. This is normal and you have to do it just once.

Try selecting different Configurations in Visual Studio, or run from command line with

msbuild /p:Configuration=CustomEnv

msbuild /p:Configuration=Debug

Note: all of the configurations are built and placed into bin\$(Configuration)\cfg folder, you can examine the files and sometimes it's useful for further automatic deployments (ie you can copy over the right config file via a script at the time you deploy vs build time).

No comments :