Using MSBuild in umbraco projects
In this blog post I'll explain how MSBuild can be used in the .csproj file of the Web application to set up a deployment process, we can use both when doing development on our local machine, and when deploying the finished website to test and production environments.
This post relies on the concepts around config transforms I explained in my previous blog post. Using these concepts we can automate the deployment process in a way, that makes all developers on your team able to make the deployment artifacts for test and production environments just by selecting a solution configuration and doing a build. The build process will also handle updating the local development site we're running on the developer machine.
MSBuild and the .csproj file.
As I see it MSBuild has two advantages over NAnt:
- It comes with our Visual Studio 2010 install, and as such is available out of the box. Hence it requires no further setup on the developer machine to function.
- The MSBuild automation language is baked into the .csproj file of the web application, which means we can use it to set up the build and deployment process in there and then use the build process in Visual Studio to run it.
MSBuild relies on targets, item groups, property groups, conditions and tasks. It is a XML based language that is interpreted by the command line utility MSBuild.exe.
Targets is the top container in the XML-file, and it contains tasks, that defines what is supposed to be done when running the target. These tasks in turn relies on item groups and property groups for selecting files and setting properties, while doing their job. Conditions are boolean logic that can be applied in the target and the tasks, to determine what targets and tasks are processed during the build process.
Building an Umbraco Web Application project
To build and make deployment artifacts for the web application project we need to run the following tasks:
- We need to create the output needed for the website.
For this purpose we're going to leverage the Publish Web Application process baked into Visual Studio. The publish function uses the .csproj file and MSBuild behind the scenes and we can take advantage of this fact.
- We need to copy the output files to a suitable location.
We'll do this by creating an item group pointing at the location of the files created by the publish task, and then use a copy task for copying them to our preferred location.
- We need to handle transformed config files separately.
This can be achieved by using a task designed to transform config files and copy them to a specified location.
Later in this blog post series I'll write about using Umbraco Courier in the development process, and how MSBuild can be used to automate moving the revision files around.
Editing the project file
Every web application project comes with a project file, actually it's pretty much what distinguishes a Web Application project from a Web Site project. These project files are XML files that can be edited in a text editor.
To edit the project file in Visual Studio which gives you some Intellisense help based on the Schema referenced, you need to unload the project in Visual studio by right-clicking the project node in the solution explorer.
Then you can open the project file for editing by right-clicking the project node again and select Edit YourProject.csproj
When you're done you can save your changes and the right click to reload the project.
It is also possible to keep the project loaded an edit the project file in another editor. After saving the project file in the other editor Visual Studio will automatically prompt you to reload the project when you return. (Remember to check out the project first if you're working with TFS as your CVS).
Create the Publish output.
The advantage of using publish for creating the output is that it's a well known process that uses the information in the project file to create the necessary output to publish the website on an IIS server. It'll look in the project file and determine which dll files are needed, from referenced projects and libraries and make sure these dll's are in the output. It'll build the web application according to the build setup you create with the Project property dialog, and the properties set on the files in the project.
To use the output we rely on a set of target included in the project file per default. These targets are defined in an external targets file and are included via an Include tag in the project file. Check that there is a tag like this in your project file:
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v10.0\WebApplications\Microsoft.WebApplication.targets" />
If not, what we're going to do next will not work, since the targets in the file referenced there are performing the publishing of our Web Application.
To take advantage of the publish targets we need to add a target definition to our project file. This target needs to be triggered when we do a build and our solution is in a certain configuration, and the target needs to call the publish function embedded in Microsoft.WebApplication.targets.
This can be achieved by adding the following target to the project file.
<Target Name="PublishWebsite" AfterTargets="AfterBuild" DependsOnTargets="PipelinePreDeployCopyAllFilesToOneFolder" Condition="$(Configuration) = 'Release'"> </Target>
The name attribute of the Target is just a name, but make sure the name is unique within the file! If not you'll experience weird result when trying to build your project.
AfterTargets="AfterBuild" attribute specifies that this target
should be processed after each build of the project.
that this target depends on the PipelinePreDeployCopyAllFilesToOneFolder
target which is found in Microsoft.WebApplication.targets. This effectively
means that this target will automatically be processed before our target,
if it hasn't already been processed during this build. This target performs
the Publish process, and places the resulting file in a temporary folder.
The Condition attribute specifies that this target should only be processed if the currently selected solution configuration is "Release". Using this condition attribute we can create different targets for different solution configuration, having Visual Studio automatically build and deploy the build artifacts whenever we build the project.
Copying the publish output
To copy the published output created by the PipelinePreDeplyCopyAllFilesToOnefolder target we need to grab all the files produced by the process. We'll do this by adding an ItemGroup to the target pointing at these files.
<ItemGroup> <PublishFiles Include="$(_packageTempDir)\**\*.*" /> <ItemGroup>
$(_packageTempDir) variable is set up in the
Microsoft.Webapplication.targets file, and points to the folder where
the output files are placed. This is usually placed in
obj\$(Configuration)\Package\PackageTmp relative to the project directory.
To assist us when we create a Copy task for copying the files to their destination, we'll set up a property pointing at the target folder.
<PropertyGroup> <ReleaseDir>\deploy\$(MSBuildProjectName)\$(Configuration)</ReleaseDir> </PropertyGroup>
$(MSBuildProjectName) property contains the name of the project
you're working in, and
$(Configuration) contains the currently selected
Solution configuration. Here's a list of MSBuildProperties available.
Now we can copy the published files to where we need them to be, with the copy task. This sounds straightforward but the syntax for copying files defined with wildcard references in ItemGroups is sadly anything but straightforward. Here's the task:
<Copy SourceFiles="@(PublishFiles)" DestinationFiles="@(PublishFiles->'$(ReleaseDir)\%(RecursiveDir)%(Filename)%(Extension)')" />
Notice we reference the ItemGroup name with an @-sign. This references the entire collection of files in the itemgroup. The challenge then is to get the copy syntax in the DestinationFiles-property right with all its special characters, parentheses and other stuff.
Copying the transformed config files
The publish process run during the build produces a partially transformed web.config file, located in the root of the PackageTmpDir. For some reason I haven't yet figured entirely out, this file can have some replacement tokens in places like connectionstrings and the like.
For this reason, I usually set up an extra copy task the copies the real
transformed config file which is located in the
below your project directory. This is done using this task:
<Copy SourceFiles="$(MSBuildProjectDirectory)\obj\$(Configuration)\TransformWebConfig\transformed\Web.config" DestinationFiles="$(DeployDir)\Web.config" OverwriteReadOnlyFiles="true" SkipUnchangedFiles="true" />
After this you might want to include transformed versions of some of Umbraco config files in the /config folder.
This can be achieved using the TransformXml task. This task runs the xml transformation process used on web.config files on any xml-file you set it run on. The task can be set up like this:
<TransformXml Source="$(MSBuildProjectDirectory)\config\umbracoSettings.config" Transform="$(MSBuildProjectDirectory)\config\umbracoSettings.$(Configuration).config" Destination="$(DeployDir)\config\umbracoSettings.config" />
This task will transform /config/umbracoSettings.config using the the config file with the name of the currently selected solution configuration in the middle of its name, and save the result in our output folder in the appropriate location. (Thanks to Stief for pointing this out in the comments to my previous blog post)
Tip: Install the SlowCheetah - XML Transforms extension in Visual Studio. This extensions adds a right click menu item on any .config file name "Add Transform", enabling easy creation of transformation files. And a right click menu item on the transform files enabling previewing the transformation in your configured file-diff viewer.
What did we achieve?
This rather lengthy blog post explained how to set up your Web application project to produce deployment artifacts in a folder of your choosing on every build. This was done using MSBuild in the .csproj file of your web application project.
Be sure to check out the finished result of the changes explained in this post. The file shows a complete build target for deploying the content of your web application. If you need to have different setups for different solution configurations, just insert multiple targets (with different names) in the project file. Then use the condition on the target to determine when the target is run.