Easy Configuration Deployment with MSBuild and the XmlMassUpdate Task
I've been looking for the easiest possible way to deploy a web.config file to several different environments. My web.config looks something like this in development:
<configuration>
<appSettings>
<add key="LogEveryRequest" value="true" />
<add key="EnableCaching" value="false" />
<add key="DefaultGisServer" value="127.0.0.1" />
</appSettings>
<connectionStrings>
<add connectionString="UserName=scott,Password=tiger" name="DBConnection" />
</connectionStrings>
<system.web>
<compilation debug="true">
</compilation>
</configuration>
I need the ability to change specific values in the test and production (release) environments. For instance, the connection string is obviously different, and I would also like to turn off logging and debugging in production.
I'm using a Web Deployment Project, so I have MSBuild at my disposal. Now, the WDP comes with an option to replace specific configuration sections, but that option is not powerful enough, as it requires you to replace an entire configuration section with another. For example, in the above configuration file I might want to change only the EnableCaching property. I don't want to keep 3 versions of the same appSettings section, only to be able to change just that single property.
What I needed turned out to be in the MSBuild Community Tasks project. Specifically, it contains a very powerful task called XmlMassUpdate. This task allows me to specify a substitution file that looks like this:
<configuration xmlns:xmu="urn:msbuildcommunitytasks-xmlmassupdate">
<substitutions>
<Debug>
</Debug>
<Test>
<appSettings>
<add xmu:key="key" key="GisServer" value="134.122.34.3" />
<add xmu:key="key" key="EnableCaching" value="true" />
</appSettings>
<connectionStrings>
<add connectionString="UserName=TestUser;Password=Testpass" name="DBConnection" />
</connectionStrings>
<system.web>
<compilation debug="false"/>
</system.web>
</Test>
<Release>
<appSettings>
<add xmu:key="key" key="GisServer" value="181.3.12.123" />
<add xmu:key="key" key="EnableCaching" value="true" />
<add xmu:key="key" key="LogEveryRequest" value="false" />
</appSettings>
<connectionStrings>
<add connectionString="UserName=ProdUser;Password=Prodpass" name="DBConnection" />
</connectionStrings>
</Release>
</substitutions>
</configuration>
As you can see, for debug I'm leaving everything as it is. For Test and Release I'm changing some of the properties. I want logging on in the Test environment, so I don't even mention it in my substitutions file, but for production it has to be changed to false. Other than that, everything here is pretty straight forward. I change only what I need to.
One thing to notice, though, is that the appSettings values have an attribute called xmu:key. This attribute is needed for the replacement engine to know by which attribute to locate the value to replace. For example, look at the first substitution in the test environment:
<add xmu:key="key" key="GisServer" value="134.122.34.3" />
We are telling XmlMassUpdate to find the GisServer value in the original web.config file, and replace it with this one. But XmlMassUpdate needs to know how to match our substitution value with the original value, so by saying xmu:key=key we're telling it to compare them by the key attribute. This way, it will know that it should look for an appSettings setting with a key="GisServer" attribute.
Once the substitution file is ready, we have to edit the MSBuild project file to call XmlMassUpdate. For that you would first need to right click your Web Deployment project and hit "Open Project File". Than you have to add the following lines to it:
1 <Import Project="$(MSBuildExtensionsPath)\MSBuildCommunityTasks\MSBuild.Community.Tasks.Targets" />
2 <PropertyGroup>
3 <BuildDependsOn>
4 $(BuildDependsOn);
5 MyAfterBuild
6 </BuildDependsOn>
7 <SubstitutionsFilePath>$(SourceWebPhysicalPath)\substitutions.xml </SubstitutionsFilePath>
8 </PropertyGroup>
9 <ItemGroup>
10 <ExcludeFromBuild Include="$(SubstitutionsFilePath)"/>
11 </ItemGroup>
12 <Target Name="MyAfterBuild">
13 <XmlMassUpdate
14 ContentFile="$(OutputPath)web.config"
15 SubstitutionsFile="$(SubstitutionsFilePath)"
16 ContentRoot="/configuration"
17 SubstitutionsRoot="/configuration/substitutions/$(Configuration)"/>
18 </Target>
First we import the MSBuild Community Task target file. This will allow us to use the XmlMassUpdate task (of course you have to install the community tasks project first for this to work).
In lines 3-6 we're adding a build event called MyAfterBuild. In fact the WDP project already contains a target called AfterBuild, but I found out that you can't really use it. If you specify an AfterBuild target, you will override the WDP AfterBuild target, which actually copies the web site files to the target directory. If you override that, well, your build won't work. So I'm specifying that the build should also depend on my MyAfterBuild target.
In line 7 I'm specifying that my substitution file resides in the original source web, in a file called substitutions.xml.
In line 10 I'm making sure the substitution file will not be deployed together with the web site (might be a bad idea to deploy all these connection strings).
In lines 12-18 we're finally calling XmlMassUpdate.
- ContentFile tells it where to find the web.config to edit (in the output location, usually).
- SubstitutionsFile tells it where to find our substitutions.xml.
- ContentRoot tells it that in the original web.config, the xml path to start replacing stuff at is the configuration node.
- SubstitutionsRoot tells it that in our substitutions.xml, the task should take values to replace from the specified xml path. For example, when we compile in debug it will take values from /configuration/substitutions/debug, and when we compile in release it will be taken from /configuration/substitutions/release. In order to enable our Test environment substitutions, we will have to add another build configuration. That's as easy as going to Build->Configuration Manager and adding a new solution configuration called Test.
And there you go. Save and recompile your project, and you will find that the web.config in your output folder has the correct values, depending on the build configuration you chose.
Go ahead and download the MSBuild Community Tasks now. They have a pretty good documentation, which helped me a lot in finding this elegant solution and writing this post.