Welcome to part 4 of my tour through ASP.NET 4.0. In this instalment, we'll be looking at a new feature in VS2010 – configuration transformation files. We’ll see the scenario that it addresses, how you’d deal with it in VS2008, how it compares to this part of the new Deployment process in VS2010, and what your options are.
One Configuration File, Many Web Servers
As web developers, we all know that a single web site may well end up deployed on several sets of servers during its development and maintenance lifecycle. At the very least, it will end up on two – your development machine and the live server. (I mean you don’t run your live websites on your dev machine, do you?) In most cases, there will also be a staging server where clients can access sites in development for testing and feedback as well. And with different servers come different database connection strings, different mail server settings, and in fact many other settings stored in your web.config file that you need to change per server.
The Manual Approach
You might try and make these changes manually each time you deploy a new version of the site to a server, perhaps for example having all the correct database connection strings in the web.config file and commenting out all but the correct one for the target servers, once deployed.
<connectionStrings> <clear/> <add name="DansDb" connectionString="string_to_my_staging_db" /> <!-- <add name="DansDb" connectionString="string_to_my_dev_db" /> <add name="DansDb" connectionString="string_to_my_live_db" /> --> </connectionStrings>
This is fine for very simple config files with only or two lines of config to change, but when you’ve even just five separate sections to change per deployment, it gets very tedious and prone to error very quickly. Which is where Web Deployment Projects take over.
Using Web Deployment Projects
An out-of-band release for VS2008 that appeared for download in January 2008, a Web Deployment Project (WDP) provides a configurable post-build step in VS2010 for web developers. Or, in English, adding a WDP to your web project means that you can specify a number of other actions for VS2008 to perform on your code once it has been built, as well as whether it should pre-compiled or not. Crucially to this discussion, this dialog allowed you to tell VS2008 to replace sections in your web.config file with the contents of another file.
So then, your base web.config would contain the settings for your development environment. Or, if you prefer, links to files where the settings that would change between servers are located. For example,
<appSettings configSource="AppSettings.config"/> <connectionStrings configSource="ConnectionStrings.config"/> <system.net> <mailSettings> <smtp configSource="Web.Mail.config" /> </mailSettings> </system.net>
And, as VS2008 goes through its post-build step for a Release build, it changes the references from these development environment files to their live equivalents, as specified in the screenshot above.
<appSettings configSource="AppSettings.Live.config"/> <connectionStrings configSource="ConnectionStrings.Live.config"/> <system.net> <mailSettings> <smtp configSource="Web.Mail.Live.config" /> </mailSettings> </system.net>
If you’re feeling adventurous, you could even get VS to delete the development environment files after a release build as well. You just made a few additions to the MSBuild file for the WDP itself, as follows.
<Target Name="AfterBuild"> <!-- Delete dev and UAT files when deploying live --> <Delete Files="$(SolutionDir)..\Release\AppSettings.config" Condition="'$(Configuration)|$(Platform)' == 'Release|AnyCPU'" /> <Delete Files="$(SolutionDir)..\Release\ConnectionStrings.config" Condition="'$(Configuration)|$(Platform)' == 'Release|AnyCPU'" /> <Delete Files="$(SolutionDir)..\Release\Web.mail.config" Condition="'$(Configuration)|$(Platform)' == 'Release|AnyCPU'" /> </Target>
On the whole, this is manageable, assuming you don’t mind the large number of config files you potentially end up with.
If you’re curious to read about all the actions possible with WDPs, please check out Scott Guthrie’s post here. It is targeted at the VS2005 version of WDP but the 2008 version differs only slightly.
Enter MSDeploy and Config Transformation Files
Config transform files (CTFs) are part of the new deployment process and in isolation are set up in a very similar fashion to how WDPs work as detailed above. When you add a web.config file to a web site or web application project, you’ll also see a number of CTFs added too, one for each build configuration you’ve created. If you create any further build configurations after you’ve added web.config to the project, simply right click on web.config in Solution Explorer and select Add Config Transforms. Additional CTFs will be added for each new build configuration.
The basic strategy for these files is to create a base level of settings in web.config – essentially, what’s needed to get things working in your development environment – and then use the CTF for a specific build configuration to tell VS how to change web.config for that build. Look in Web.Debug.config and you’ll see that no changes will be made. Look in Web.Release.config and you’ll see the following (minus the comments)
<?xml version="1.0"?> <configuration xmlns:xdt="http://schemas.microsoft.com/XML-Document-Transform"> <system.web> <compilation xdt:Transform="RemoveAttributes(debug)" /> </system.web> </configuration>
An educated guess will correctly tell you that this is an instruction to remove the debug attribute from the <compilation> element whatever its value, thus setting it to the machine-wide default of false.
CTFs share the same schema as standard .NET config files but declare a second namespace which gives you two extra attributes to set down on any element in the file : Locator and Transform.
Locator identifies the element or attribute to be altered in one of four ways. Its syntax is
Locator = "Command(<parameter>)"
where Command is one of three possibilities
- Condition : In which case <parameter> is an XPath expression identifying the element or attribute to change relative to the element on which Locator is declared.
- Match : In which case <parameter> is a comma-delimited list of the attributes on the current elements to be changed
- XPath : In which case <parameter> is the absolute XPath from the root of web.config to the element to be changed
The fourth way to use Locator is to omit it. If a Transform attribute is set on an element without a Locator, it is applied to that element, as demonstrated in the example straight from Web.Release.config above.
Let’s demonstrate the first three options by using each in turn to do the same task: change some mail settings. In our standard web.config file for development purposes, we use pickupDirectory to test email.
<configuration> ... <system.net> <mailSettings> <smtp deliveryMethod="SpecifiedPickupDirectory" from="email@example.com"> <specifiedPickupDirectory pickupDirectoryLocation="D:\Temp"/> </smtp> </mailSettings> </system.net> </configuration>
but when deploying live, we want to use an actual mail server and have the mailSettings look like this
<configuration> ... <system.net> <mailSettings> <smtp deliveryMethod="network" from="firstname.lastname@example.org"> <network host="liveMailServer" /> </smtp> </mailSettings> </system.net> </configuration>
We can achieve this transformation by using Condition to affect all smtp elements with deliverymethod set to SpecifiedPickupDirectory, like so:
<?xml version="1.0"?> <configuration xmlns:xdt="http://schemas.microsoft.com/XML-Document-Transform"> <system.net> <mailSettings> <smtp deliveryMethod="Network" from="email@example.com" xdt:Locator="Condition(@deliveryMethod='SpecifiedPickupDirectory')" xdt:Transform="Replace"> <network host="liveMailServer"/> </smtp> </mailSettings> </system.net> </configuration>
Using Match rather than Condition, we identify all smtp elements that have a from attribute with value firstname.lastname@example.org to transform. They key here is that the attribute being matched will not have its value changed.
<?xml version="1.0"?> <configuration xmlns:xdt="http://schemas.microsoft.com/XML-Document-Transform"> <system.net> <mailSettings> <smtp deliveryMethod="Network" from="email@example.com" xdt:Locator="Match(from)" xdt:Transform="Replace"> <network host="liveMailServer"/> </smtp> </mailSettings> </system.net> </configuration>
Finally, you can use XPath to specify absolutely the element or group of elements to transform
<?xml version="1.0"?> <configuration xmlns:xdt="http://schemas.microsoft.com/XML-Document-Transform"> <system.net> <mailSettings> <smtp deliveryMethod="Network" from="firstname.lastname@example.org" xdt:Locator="XPath(configuration/system.net/mailSettings/smtp [@deliveryMethod='SpecifiedPickupDirectory'])" xdt:Transform="Replace"> <network host="liveMailServer"/> </smtp> </mailSettings> </system.net> </configuration>
This simple example perhaps belies the potential on offer here. Because we are using XPath expressions, we can potentially affect groups of elements in web.config rather than just the one, or elements if only certain conditions are met. But aside from a direct replacement as demonstrated above, what other changes can we make?
The Transform attribute defines exactly what alteration to make to an element. Its syntax is
where command is one of xyz possibilities. (Code samples here are snippets rather than full CTF listings.)
- Insert : Adds the element it is specified on as the last child to the selected element. For instance, this adds a new element to <appSettings>
<appSettings> <add name="LiveSetting" value="Something" xdt:Transform="Insert" /> </appSettings>
- InsertBefore(XPath) and InserAfter(XPath): Inserts the element it is specified on immediately before/after the element specified by the XPath expression. For example, this adds a <clear /> immediately before a connection string declaration
<connectionStrings> <clear xdt:Transform=" InsertBefore(/configuration/connectionStrings/add[@name='MyDB'])" /> </connectionStrings>
- Replace : Replaces the selected element with that specified, as demonstrated earlier
- Remove : Removes the selected element, or the first in the selected group.
- RemoveAll : Removes all the elements in the selected group. For example, this removes all the <add> elements under <appSettings>
<appSettings> <add xdt:Transform="RemoveAll" /> </appSettings>
- RemoveAttributes(comma-separated list of attributes to remove) : Removes all the specified attributes from the selected elements, as demonstrated in the original Web.Release.config file.
- SetAttributes(comma-separated list of attributes to add) : Adds the named attributes on the selected element with the values given. For example, this sets the default from address on all mail servers.
<system.net> <mailSettings> <smtp from="email@example.com" xdt:Transform="SetAttributes(from)"> </smtp> </mailSettings> </system.net>
- XSLT(filepath) : Applies an XSLT file to the selected elements.
Phew! I hope you can see how powerful CTFs can be, especially if you use the power of XPath to identify elements to transform. If you’re new to XPath, the w3schools introduction to XPath is a good place to start. You can find more information and examples on CTF syntax here. Until the next time, happy coding!