Apply XDT transforms to your ServiceDefinition.csdef file
TheWindows Azure SDK 1.5 was released over a year ago introducing support for multiple Service Configuration files. Ever since this release it has become really easy to manage multiple environments by simply defining a new Service Configuration and changing the settings for each environment:
A typical use case for working with multiple Service Configurations would be your database connection string. If you're developing locally you probably have a SQL Express database running on your machine. Then when you deploy to Windows Azure to do some tests you want to use a different database (maybe you have a different Cloud Service for your 'beta' version: myapp-beta.cloudapp.net). And finally when you deploy to production you want the application to use the production database. Well by using multiple Service Configuration files you can manage these different connection strings and choose which one to use upon deployment:
At the moment this feature only covers ServiceConfiguration.cscfg files, but there might be times where you'll also need multiple ServiceDefinition.csdef files.
Let's come back to the "beta" example I previously mentioned. This is probably a deployment used by the team internally to see how things run in Windows Azure and to have a few users test it out. Now what exactly is included in a ServiceDefinition.csdef file? The definition of your Roles (with their sizes), Virtual Directories, Endpoints, … I've seen a few customers use Medium sized roles in production and ExtraSmall sized roles in their test environment to reduce the cost of compute hours. They typically use multipleServiceDefinition.csdef files (one for each environment) which also means they need to make changes to all of these files if they add an endpoint for example.
Let's see how we can use XDT transforms (the web.config transformations syntax) to support multiple ServiceDefinition.csdef files.
Last week theXDT (web.config) transform engine was released on NuGet (official post) which makes it really easy to build your own tools that can apply (web.)config transformations. And this is what Eric Hexter did, he released theWebConfigTransformRunner package on NuGet. This is essentially a command line tool which can run transformations. So in order to get started you'll first need to add this package to your solution:
PM> Install-Package WebConfigTransformRunner
Once the package is installed you'll find it in the packages\WebConfigTransformRunner.126.96.36.199\Tools directory. What we'll do next is create a transformation that sets the size of our WebRole to ExtraSmall when we deploy to the CloudBeta environment. So first you'll need to create a new
ServiceDefinition.CloudBeta.csdef file in your Windows Azure project (make sure you do the same for the other configurations):
As you can see it's pretty easy to achieve by using
xdt:Locator (the syntax is explained here):
<?xml version="1.0" encoding="utf-8"?> <ServiceDefinition xmlns="http://schemas.microsoft.com/ServiceHosting/2008/10/ServiceDefinition" xmlns:xdt="http://schemas.microsoft.com/XML-Document-Transform" xdt:Locator="Condition(@name!='' and @schemaVersion!='')"> <WebRole name="MyWebRole" vmsize="ExtraSmall" xdt:Transform="SetAttributes" xdt:Locator="Match(name)"> </WebRole> </ServiceDefinition>
Now that we have our file the only thing left to do is modify the *.ccproj file (the project file of your Windows Azure project). We need to make 2 changes:
- Define the transformation files as Content (that way you'll see them in Visual Studio in the Windows Azure project)
- Define where the
WebConfigTransformRunnerexecutable is located. Note that you might need to change the version number.
- Create a target named
AfterValidateServiceModelwhich contains the logic to do run the actual transformation. This task will be executed when your Azure project builds (as defined in Microsoft.WindowsAzure.Targets)
Add the following code to the end of the *.ccproj file:
... <!-- Define extra Service Definition files. --> <ItemGroup> <Content Include="ServiceDefinition.Local.csdef" /> <Content Include="ServiceDefinition.Cloud.csdef" /> <Content Include="ServiceDefinition.CloudBeta.csdef" /> </ItemGroup> <PropertyGroup> <ServiceDefinitionTransform>ServiceDefinition.$(TargetProfile).csdef</ServiceDefinitionTransform> <TransformRunnerExecutable>$(MSBuildProjectDirectory)\..\packages\WebConfigTransformRunner.188.8.131.52\Tools\WebConfigTransformRunner.exe</TransformRunnerExecutable> </PropertyGroup> <!-- Execute Transformation. --> <Target Name="AfterValidateServiceModel"> <Message Text="AfterValidateServiceModel (TargetProfile=$(TargetProfile))" Importance="high" /> <Message Text=" - TransformRunnerExecutable: $(TransformRunnerExecutable)" Importance="high" /> <Message Text=" - ServiceDefinitionTransform: $(ServiceDefinitionTransform)" Importance="high" /> <Message Text=" - Executing Transform: "$(TransformRunnerExecutable)" @(TargetServiceDefinition) $(ServiceDefinitionTransform) @(TargetServiceDefinition)" Importance="high" /> <Exec Command=""$(TransformRunnerExecutable)" @(TargetServiceDefinition) $(ServiceDefinitionTransform) @(TargetServiceDefinition)" /> </Target> </Project>
That's it. Save the file, reload the project in Visual Studio and you're good to go. You can now have a base Service Definition file (ServiceDefinition.csdef) and have a transform file per environment to manage the differences between environments.
If you've worked with config transformations and MSBuild in the past you're probably wondering why I'm not using
TransformXml? Well first because I really like the
WebConfigTransformRunner tool and second, because of this issue with
TransformXml (explained here):
Exception: Could not write Destination file: The process cannot access the file ‘D:\Repositories\Local\AzureConfigTransformations\AzureConfigTransformations\bin\Release\ServiceDefinition.csdef' because it is being used by another process.
2> at Microsoft.Web.Publishing.Tasks.TransformXml.SaveTransformedFile(XmlTransformableDocument document, String destinationFile)
2> at Microsoft.Web.Publishing.Tasks.TransformXml.Execute()
But hey if you found a solution to this file in use error feel free to use
TransformXml, it will work all the same.