Transform Windows Azure Service Model Files During Packaging
The latest versions of the Windows Azure Tools for Visual Studio have the ability to maintain multiple versions of the Service Configuration (.cscfg) file. The developer is then prompted to select the version to accompany a deployment, whether it’s going to Windows Azure or the development fabric (i.e. the emulator). However, sometimes a developer needs to do something a little more dynamic, where elements of the Service Configuration file must be generated on-the-fly before deployment.
The Tools generate a Windows Azure package via a process driven by MSBuild. The process already performs certain transformations of the Service Configuration file from the original hosted by the project. These transformations include:
- Adding configuration setting values when IntelliTrace or Profiling are enabled during publishing
- Adding configuration setting values when Remote Desktop is enabled during publishing
- Adding endpoints when WebDeploy is enabled
This transformation process is easily extensible (for certain definitions of “easy”). To do this, some of the MSBuild concepts you will need are:
TargetProfile: This MSBuild property corresponds to the configuration selected for packaging or deployment, e.g. “Local” or “Cloud”.
SourceServiceConfiguration: This MSBuild item group* corresponds to the Service Configuration file hosted by the project that was selected for deployment (based on the value of the TargetProfile property), e.g. ServiceConfiguration.Cloud.cscfg.
TargetServiceConfiguration: This MSBuild item group* corresponds to the actual Service Configuration file that will be deployed. This file begins as a copy of SourceServiceConfiguration but may then be transformed based on the selected publish settings.
TransformServiceModel: This target represents the part of the build and package process that controls transformation of the Service Configuration file. It can be extended by overriding the BeforeTransformServiceModel or AfterTransformServiceModel targets, or by adding new targets to the CoreTransformServiceModelDependsOn MSBuild property.
*Despite being an item group, it only ever contains a single file.
To add a new transformation step, start by updating the project file (.ccproj). Add a new property group that redefines CoreTransformServiceModelDependsOn, adding a new target to the end of the original value. Next, define the new target; it will now get called during the transformation phase of the package process. In the target, open the file represented by the @(TargetServiceConfiguration)item group, modify it as desired, and then save it.
.
.
.
<Import Project="$(CloudExtensionsDir)Microsoft.WindowsAzure.targets" />
<PropertyGroup>
<CoreTransformServiceModelDependsOn>
$(CoreTransformServiceModelDependsOn);
NewServiceConfigurationTransform;
</CoreTransformServiceModelDependsOn>
</PropertyGroup>
<Target Name="NewServiceConfigurationTransform">
<!-- Perform transformation of @(TargetServiceConfiguration) here. –>
</Target>
.
.
.
To see a practical example of this technique used to workaround an issue encountered by some customers in the v1.7 Tools, read this MSDN forum post.
Don’t Forget the Service Definition File!
The Service Definition (.csdef) file is intended to be the same across all deployments of a Windows Azure application. This is why it’s located in the package rather than alongside the package like the Service Configuration file. However, there are still scenarios where a developer may wish to change the Service Definition dynamically, based on the environment for which the application is packaged. Even though the Tools do not support multiple Service Definition files hosted by a project, they undergo a very similar transformation process that can be extended in the same way, using similar MSBuild concepts:
- SourceServiceDefinition: This MSBuild item group* corresponds to the Service Definition file hosted by the project, e.g. ServiceDefinition.csdef.
- TargetServiceDefinition: This MSBuild item group* corresponds to the actual Service Definition file that will be packaged. This file begins as a copy of SourceServiceDefinition but may then be transformed based on the selected publish settings.
*Despite being an item group, it only ever contains a single file.
You can apply transformations to the Service Definition file using the @(TargetServiceDefinition)item group in the same target as you do the Service Configuration file.
Project File Customization or Common Target?
If the transformation scenario is specific to a single Windows Azure project, then it makes sense to alter the build and package process via that project file directly. However, if the scenario is common it may make sense to split out the customization into a separate targets file. This target file can then be imported in various ways depending on how commonly it’s needed:
- (Option #1) Import the targets directly from the project: In this option, the targets file is stored in a well-known location (preferably in a path that can be specified relative to any project which has occasion to use it). The project file is then modified by hand to import the targets file after the import of the Windows Azure targets file. This option is suitable when a limited, related set of Windows Azure projects (e.g. within the same solution) need the same customization.
- (Option #2) Globally import the targets: In this option, the targets file is placed in a well known location on the machine which will cause it to be automatically imported by every Windows Azure application on that machine. This option is suitable when any Windows Azure project might need the same customization. To have a target file imported automatically by all Windows Azure projects, add the file to this location:
<Program Files (x86)>\MSBuild\Microsoft\VisualStudio\<VS Version>\Windows Azure Tools\<Tools Version>\ImportAfter\
For example, on a machine with Visual Studio 2012 installed and version 1.7 of the tools the location would be:
C:\Program Files (x86)\MSBuild\Microsoft\VisualStudio\v11.0\Windows Azure Tools\1.7\ImportAfter\
Note that, since this target is imported by all Windows Azure projects on the machine, it should be robust enough to handle cases where the customization is not necessary.
But I Want Web.config-style Transforms!
A common question is “why don’t the Tools support multiple Service Configuration files using web.config-style transforms?” (This transformation mechanism is being more commonly referred to as SlowCheeta.) Well, the short answer is: because the Tools has a designer over the Service Model files and web.config does not. Using multiple, complete, configuration files to represent different configuration environments is far less complex to automate via a design surface like the Role Editor (a.k.a. Service Model UI). In addition, it’s easier for the beginning Windows Azure developer to understand complete files and not be forced to learn the complex XML matching and transformation syntax.
For those that still want web.config-style transformations, you can use the same techniques described above to get close to the familiar web application project experience. Start by updating the project file (.ccproj). Add a new property group that redefines CoreTransformServiceModelDependsOn, adding a new target to the end of the original value. Next, add a UsingTask element that refers to the TransformXml task in the web application publishing tasks assembly. Define the new target; it will now get called during the transformation phase of the package process. In the target, call the TransformXml task using the @(TargetServiceConfiguration) item group as both the source and destination, and specify an arbitrary transform file. In the example below, I’m choosing a transform file based on the current TargetProfile. For example, if the TargetProfile is “Cloud” meaning the ServiceConfiguration.Cloud.cscfg file will be deployed, the task will transform the source file using the file ServiceConfiguration.Cloud.Transform.cscfg, if it exists. (If it does not exist, no transform is performed.)
.
.
.
< Import Project="$(CloudExtensionsDir)Microsoft.WindowsAzure.targets" />
< PropertyGroup>
< CoreTransformServiceModelDependsOn>
$(CoreTransformServiceModelDependsOn);
PerformXmlTransform;
</CoreTransformServiceModelDependsOn>
< /PropertyGroup>
<UsingTask TaskName="TransformXml" AssemblyFile="$(MSBuildExtensionsPath)\Microsoft\VisualStudio\v11.0\Web\Microsoft.Web.Publishing.Tasks.dll" />
<Target Name="PerformXmlTransform" Condition=" Exists('ServiceConfiguration.$(TargetProfile).Transform.cscfg') ">
<TransformXml Source="@(TargetServiceConfiguration)"
Transform="ServiceConfiguration.$(TargetProfile).Transform.cscfg"
Destination="@(TargetServiceConfiguration)" />
<Message importance="high" Text="Transformed @(TargetServiceConfiguration) using ServiceConfiguration.$(TargetProfile).Transform.cscfg." />
< /Target>
.
.
.
To sum up, if you are willing to spend some time to learn a few basic MSBuild concepts, the Windows Azure Tools for Visual Studio offers a lot of packaging flexibility. If you have custom packaging scenarios that the Tools do not support directly, consider using the Tools extensibility to add your own!
Comments
- Anonymous
November 27, 2012
excellent writeup on transforming the config file!