Easy Configuration Deployment with MSBuild and the XmlMassUpdate Task

January 18, 2008

19 comments

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.

Add comment
facebook linkedin twitter email

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>

19 comments

  1. jerschmidt14@hotmail.comJanuary 31, 2008 ב 16:10

    Hi,
    It does not seem that the $(OutputPath) is returning the correct path to my _publishedwebsites\ITMonitoring_deploy directory. I tried changing it to point to the source, but as TFSBuild is only doing a get on the files, they are read only at the time. In the deployment directory, the file are not read only, so it would work. Problem is that the outputpath is not pointing to the correct place, so i get a file not found. Any ideas? I am using a web site rather than a web project, and have added a web deployment project to it.

    Cheers,

    Jeremy

    Reply
  2. jerschmidt14January 31, 2008 ב 16:37

    Hi,

    I have some more information. Seems that TFSBuild is substituting in a new outputpath without changing the value of the variable. The error message I am getting is

    Solution: ITMonitoring.sln, Project: ITMonitoring_deploy.wdproj, Compilation errors and warnings
    e:\NETWebApps\DevTools\ITMonitoring\Sources\ITMonitoring_deploy\ITMonitoring_deploy.wdproj(60,6): error :
    Unable to load content file e:\NETWebApps\DevTools\ITMonitoring\Sources\ITMonitoring_deploy\web.config
    =====
    The Correct path to the web.config file is
    e:\NetWebApps\DevTools\ITMonitoring\Binaries\Mixed Platforms\Release\_PublishedWebsites\ITMonitoring_deploy

    Reply
  3. doronyJanuary 31, 2008 ב 19:45

    That’s weird, it seems to work for me on both a Web Application and on a Web Site.

    You seem to mention specifically TFSBuild. Is this working for you when you compile within Visual Studio?

    Reply
  4. JuanApril 9, 2008 ב 15:12

    It isn’t working for me either. In my case, I’m building/compiling a web service and the OutputPath points to the /bin were the final dlls are put. The web.config doesn’t exist there so it fails.

    Reply
  5. Mike PowellApril 10, 2008 ב 21:54

    This was a great walkthrough on the initially-confusing XmlMassUpdate task. Thanks for getting me up to speed!

    One thing I discovered was that using the ExcludeFromBuild item triggers an extra site copy operation that almost doubles the build time. Instead I used a Delete task after the XmlMassUpdate to delete the substitutions file from the output folder, and my build is a lot faster now.

    Reply
  6. doronyApril 12, 2008 ב 14:20

    Hey Juan,
    I’m not sure why is that the case for you, but you can point the task to wherever the web.config is. If OutputPath doesn’t work there is probably some other property you can use, or you can simply try something like $(OutputPath)\..\web.config.

    Reply
  7. Alex NilssonJune 25, 2008 ב 17:13

    Hi, having problem with multipe connectionstrings, what´s the syntax if I have many connectionsstrings, like this:

    web.config:




    xml file:


    I have tried a lot of things including the sample above but that only works with one connectionstring

    Reply
  8. Alex NilssonJune 26, 2008 ב 11:46

    Fixed it:

    Reply
  9. mga911August 22, 2008 ב 2:54

    I saw that you recently commented on Matt Berseth’s blog here: http://mattberseth.com/blog/2007/05/single_config_file_multiple_de.html. You said that xmlMassUpdate could achieve what he was trying to do. Your example here shows how to substitute static sections of web.config but what if the substitution you wanted to make was dynamic. Such as inserting the svnVersion that is retrieved during the MSBuild into web.config as an appSetting. How would you accomplish this?

    Reply
  10. Hilton GiesenowOctober 8, 2008 ב 8:45

    This helped a lot, thanks.

    With reference to mga911′s question, this is possible using the ProjectExtensions tag. It depends what he’s trying to accomplish.

    - HG

    Reply
  11. BillFebruary 5, 2009 ב 22:21

    The issue without looking deeper to see if it could be resolved was that XmlMassUpdate cannot work with non-standard web.config sections.

    Reply
  12. PunitMay 2, 2009 ב 1:10

    This was really helpful..thanks so much!!

    Reply
  13. JohnNovember 25, 2009 ב 2:19

    I’m having some trouble with this. XmlMassUpdate uses the key for items in the appSettings section just fine… for example, this works.

    However, in the connectionStrings section, it seems to ignore the key and always just overwrites the first node. In this case “Track” is the fourth node in the section, but it overwrites the first.

    Any idea what might be wrong here? Thanks!

    Reply
  14. JohnNovember 25, 2009 ב 17:05

    Got it!

    Changing “key” to “name” did it. Because the examples were all for the appSettings section, I made the assumption that after you specified the key name “xmu:key=”name”", that you would then specify the value for that key with key=… Turns out you don’t do anything special for that. I was overthinking it!

    This really works slick now!

    Thanks!

    Reply
  15. Mark PawelekSeptember 17, 2010 ב 11:33

    It seems to me that this will only work if I have use a deployment project as well. I want to run MSBuild from NAnt. The build must run independently of Visual Studio.

    Can I do that and use this XMLMassUpdate trick? [otherwise I'll be reduced to the uglier NAnt tricks.] I have 4 web.config variations: Local.Debug, Local.Release, QA, Production.

    Reply
  16. SreeJanuary 24, 2011 ב 23:52

    How can achieve this by using a shell script? I mean use msbuild in the a .cmd file and pass the appropriate parameter to select “Test”, “Debug” or “Release”?

    Reply
  17. VijayAugust 22, 2011 ב 19:10

    Substitutions.xml







    myConfig.xml









    TestBuild.xml

    SubstitutionsFile="$(SubstitutionsFilePath)"
    ContentRoot="Section1"
    SubstitutionsRoot="Section1/substitutions">

    when I runt he msbuild then only the last entry from Substitutions file is substituted, what is the problem.

    Reply
  18. VijayAugust 22, 2011 ב 21:10

    ok, found that I need to provide a unique key in substitutions file if the elements are same xmu:key=”id”


    Reply