April 17, 2009 by Alistair Deneys
Managing multiple environment configurations for any project has always been a bit of a challenge. Even when you’re still in development, and different developers have different folder structures and drive assignments which would affect the location of your data folder in Sitecore, not to mention when you move to QA and production where server names will most likely also change.
There are many different ways in which to manage these configurations, but the most robust have always been incorporated into the build process, where you have a master config file from which all other environment specific versions of that file are generated.
At Next Digital, we’ve been making use of the fact that configuration in .net is mainly handled through XML files. Your web.config and App.config files are XML. So we can use XSLT to easily transform one config file into another.
The heart of this technique lies in the xsl:templates that copy over elements and attributes verbatim, so you only have to handle what needs to change. The following templates will do this for you.
<!-- Default templates to match anything else --> <xsl:template match="@*"> <xsl:copy/> </xsl:template> <xsl:template match="node()"> <xsl:copy> <xsl:apply-templates select="@*"/> <xsl:apply-templates/> </xsl:copy> </xsl:template>
Note how the node template copies the current node and inside that node calls apply templates. This means that child elements of the current element will be run through the XSLT which will traverse the entire document.
These 2 templates must appear at the bottom of your XSLT file as templates are applied in the order they appear in the file. If these templates were at the top of the file they would match all nodes and your custom templates would never get run.
If we want to change a specific config value, then all we have to do is provide a template to match the node we want to replace, and change the value. Let’s look at how we would change the data folder in a Sitecore web.config to something else. This is an example of changing an attribute value.
<xsl:template match="/configuration/sitecore/sc.variable [@name='dataFolder']/@value"> <xsl:attribute name="value">C:\inetpub\Proj\dep\ Sitecore\Data</xsl:attribute> </xsl:template>
Let’s change the text value of a simple node element such as changing the items cache size of a database.
<xsl:template match="databases/database [@id='web']/cacheSizes/items/text()">100MB</xsl:template>
And we might also want to change the contents of a complex node element that contains other nodes such as adding an event handler.
<xsl:template match="events/event [@name='item:copying']"> <xsl:copy> <xsl:apply-templates select="@*"/> <xsl:apply-templates/> <handler type="Class, Assembly" method="Method" /> </xsl:copy> </xsl:template>
Here we also have to copy the matched node which in this case is the item:copying event node, so we don’t lose any existing event handlers in the master config file.
Let’s put this all together. The following XSLT will change my dataFolder in a Sitecore web.config file and adjust the hostname to which the “website” site is bound.
<?xml version="1.0" encoding="utf-8"?> <xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0"> <xsl:output method="xml" indent="yes"/> <xsl:template match="/configuration/sitecore/sc.variable [@name='dataFolder']/@value"> <xsl:attribute name= "value">C:\inetpub\MySite\dep\ Sitecore\Data</xsl:attribute> </xsl:template> <xsl:templatematch="sites/site[@name='website']/@hostName"> <xsl:attribute name=" hostName">www.mysite.net</xsl:attribute> </xsl:template> <!-- Default templates to match anything else --> <xsl:template match="@*"> <xsl:copy/> </xsl:template> <xsl:template match="node()"> <xsl:copy> <xsl:apply-templates select="@*"/> <xsl:apply-templates/> </xsl:copy> </xsl:template> </xsl:stylesheet>
This kind of config transform is good for managing the config between different environments such as development, QA and production. When a developer adds to or changes the configuration then we can generate the environment specific versions of that config file and we don’t miss those vital configuration updates.
But we don’t want to be kicking this process off manually, let’s integrate it into the build process, so when we perform a release build the environment specific configs are generated on disk for us to move to the target environments. This will take a little bit of MSBuild tweaking.
MSBuild is the build engine used in Visual Studio and your standard project files are actually MSBuild project files. So you’ll need to edit the project file of the project that contains the master config file. You can do this from Visual Studio which means you also get IntelliSense. You’ll first need to unload the project by right clicking on the project in the solution explorer and selecting “Unload project”. Next you can right click on the unloaded project file and select “Edit <yourproject>”. This will open the text (XML) of the project file in the editor window ready for you to start editing.
In MSBuild terms, a task is an operation that needs to be performed in order such as creating a folder, compiling source files or changing the attributes on a file. Unfortunately MSBuild doesn’t come with a default XSLT task. Luckily, MSBuild is very extensible allowing you to write your own tasks. But don’t worry about writing your own XSLT task, there are several third party libraries out there that have already done this for you. In these examples I’ll be using the MSBuild Community Tasks library which you can find at http://msbuildtasks.tigris.org/.
After you’ve installed the community tasks you need to import the tasks into your project file by placing the following at the top of your MSBuild project file just under the “Project” element.
<Import Project="$(MSBuildExtensionsPath)\ MSBuildCommunityTasks\MSBuild.Community.Tasks.targets" />
Now we can create our own target to generate our config files. A target is a group of tasks which can be invoked from another target or invoked directly by whomever is running the MSBuild file. The following target will use the XSLT task to transform the web.config file into environment specific config files for QA and production.
<Target Name="Configs"> <Xslt RootTag="" Inputs="web.config" Output="web.QA.config" Xsl="web.config.QA.xslt" /> <Xslt RootTag="" Inputs="web.config" Output="web.prod.config" Xsl="web.config.prod.xslt" /> </Target
We can call this target from the “AfterBuild” target, which gets called when a build is successful. We can also place a condition on the call so it will only run for a release build.
<Target Name="AfterBuild"> <CallTarget Targets="Environment" Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' "/> </Target>
Now reload your project (right click on the project file) and build. Only when you select a release build will your custom config transforms run and the files will be generated on disk in the same folder as your web.config file.
Using the above technique we could generate specific config files for separate developers too. This is good for when the developer is in a different network and uses a different DB server, or their data folder is in a different location. But we don’t want to have to move those config files round by hand. We would much rather integrated this seamlessly into the build process. So that would mean copying the right config file over for this specific developer.
So how can we from inside MSBuild determine which developer is running the build? MSBuild passes all current environment variables in as properties. To access a property, you simply surround the property name with parenthesis and precede it with a dollar sign.
This property will be substituted before the task is run. So we have access to the COMPUTERNAME environment variable from within MSBuild. This is the perfect way to determine which developer or more specifically, which development environment I need the custom configuration for. We’ll take it a step further and only generate the environment specific development config file for this machine if one is required. The easiest way to do that is to include the computer name in the filename of your config transform file.
We can then use a conditional attribute on the XSLT task to only create the config file if the transform file for this machine exists.
<Xslt RootTag="" Inputs="web.master.config" Output="web.$(COMPUTERNAME).config" Xsl="web.config.$(COMPUTERNAME).xslt" Condition="Exists('web.config.$(COMPUTERNAME).xslt')" />
And we’ll couple that with a copy task to copy the output file over the config file, of course using a conditional attribute on the copy task to check if the config file for this machine has been created.
<Copy SourceFiles="web.$(COMPUTERNAME).config" DestinationFiles="web.config" Condition="Exists('web.$(COMPUTERNAME).config')" />
You’ll also want to precede those 2 tasks with a general copy of the master config file to the running config file for those cases where a machine specific transform doesn’t exist. Your resulting “Configs” MSBuild target might look something like the following. This can be called from the “AfterBuild” target.
<Target Name="Configs"> <Copy SourceFiles="web.master.config" DestinationFiles="web.config" /> <Xslt RootTag="" Inputs="web.master.config" Output="web.$(COMPUTERNAME).config" Xsl="web.config.$(COMPUTERNAME).xslt" Condition="Exists('web.config.$(COMPUTERNAME).xslt')" /> <Copy SourceFiles="web.$(COMPUTERNAME).config" DestinationFiles="web.config" Condition="Exists('web.$(COMPUTERNAME).config')" /> </Target>