Building and deploying solutions with MSBuild

Automating the the build and deployment process is a real time saver, having a build file also lends itself towards continuous integration. For complex build its ensures no steps are missed and reliable builds are always created.

Below details the common tasks, and tricks I use when creating these build files. Before going into task specifics, there's a few handy options to be aware of first

PropertyGroups

PropertyGroups are an ideal way of storing build properties (which can be thought of as variables). Using a condition parameter allows for configuration specific values

<PropertyGroup Condition="'$(Configuration)'=='UAT'">
  <BuildFolder>UAT</BuildFolder>
  <DeployPath>\\UAT Server\sites\mySite</DeployPath>
</PropertyGroup>

<PropertyGroup Condition="'$(Configuration)'=='Preview'">
  <BuildFolder>Preview</BuildFolder>
  <DeployPath>\\Preview Server\sites\mySite</DeployPath>
</PropertyGroup>

ItemGroups, including and excluding items

An ItemGroup is a collection of items (usually files). The include and exclude parameters provide a powerful way of creating the collection. Wherever possible, use wildcards, so any additional files are automatically included. Any items captured in the exclude will take precedent of the same item being captured in the include

<ItemGroup>
  <files 
    Include="$(path)\**\*.dll;
      $(path)\**\*.svc;
      $(path)\**\*.aspx;
      $(path)\**\*.asax;
      $(path)\**\*.js;
      $(path)\**\*.htm;
      $(path)\**\*.png;"
      
    Exclude="$(path)\obj\**\*.*
      $(path)\**\app_offline.htm;
    " />	
</ItemGroup>

In the above example, all htm files will be included, except app_offline.htm. As new htm files appear, they are automatically picked up by the wildcard matches

Selecting a folder and all its contents (including sub folders)

While it would seem logical to think *.* on a given folder would do this, there's actually a non-intuitive pattern which requires an addtional \**. e.g. to select everything in the $(OutputPath) folder, the syntax is

<ItemGroup>
  <files Include="$(OutputPath)\**\*.*" />
</ItemGroup>

The following explanation is taken from this msdn article MSBuild Items

  1. The ? wildcard character matches a single character
  2. The * wildcard character matches zero or more characters
  3. The ** wildcard character sequence matches a partial path

Feedback with the Message Task

Providing feedback at any point in the build process is beneficial (especially for debugging). This can be achieved using the Message task

<Message Importance="high" Text="--- copying (Destination: $(OutputPath)) ---"/>

The build file implementation

The Task order

Typical task for a build file are below

<Target Name="Build">
  <CallTarget Targets="Clean" />
  <CallTarget Targets="Compile" />
  <CallTarget Targets="Copy" />
  <CallTarget Targets="Configure" />
  <CallTarget Targets="Compress" />
  <CallTarget Targets="Deploy"  />
</Target>
  1. Clean - Clean the contents of the output folder
  2. Compile - Build the solution
  3. Copy - Copy the built solution to the output folder
  4. Configure - Apply the transforms to the web.config file
  5. Compress (optional) - Zip the contents, possibly for a definitive software library
  6. Deploy (optional) - for test or staging environments you may wish to copy over to the site

Cleaning (deleting) previous build output

using a combination of Delete and MakeDir you output folder can be prepared

<ItemGroup>
  <filesToDelete Include="$(OutputPath)\**\*" />
</ItemGroup>

<Delete Files="@(filesToDelete)" />
<MakeDir Directories = "$(OutputPath)" Condition = "!Exists('$(OutputPath)')" />

Building the solution using the MSBuild task

When using the MSBuild task, you can pass in the path to the solution file (.sln) and all projects within that solution will be built. This will work with smaller solutions, but with the larger (or legacy) solutions, there are often projects you do not wish to include in the build. A more flexible approach its to explicitly list which projects you wish to build in an item group

<ItemGroup>
  <ProjectReferences 
    Include="..\**\*.*proj" 
    Exclude="..\**\*Utility.*proj;
      ..\**\*UnloadedProject.*proj;"
       />
</ItemGroup>

<MSBuild 
  Projects="@(ProjectReferences)" 
  BuildInParallel="false"
  Targets="Build" 
  Properties="Configuration=$(Configuration);Optimize=true"
  ContinueOnError="false" 
  StopOnFirstFailure="false" />

Copy build output to an output folder

Using the Copy task and the %(RecursiveDir) metadata the whole output tree can be copied

<ItemGroup>
  <files 
    Include="$(path)\**\*.dll;
      $(path)\**\*.svc;
      $(path)\**\*.aspx;
      $(path)\**\*.asax;
      $(path)\**\*.js;
      $(path)\**\*.htm;
      $(path)\**\*.png;"
      
    Exclude="$(path)\obj\**\*.*
      $(path)\**\app_offline.htm;
    " />	
</ItemGroup>

<Copy 
  SourceFiles="@(files)" 
  DestinationFolder="$(OutputPath)\%(RecursiveDir)"
  />

The msdn article MSBuild Well-known Item Metadata states If the Include attribute contains the wildcard **, this metadata specifies the part of the path that replaces the wildcard.. This translates to the %(RecursiveDir) metadata being replaced with the path being derived from the ** wildcard.

Leveraging Visual Studio's build transformations

Visual studio's web.config transformations can be incorporated into your build file by referencing the TransformXml task. This allows you to leverage all existing transforms. Using these transforms is often easier than using xml update tasks

Web.config Transformation file
web.release.config
<?xml version="1.0"?>

<configuration xmlns:xdt="http://schemas.microsoft.com/XML-Document-Transform">
  <connectionStrings>
    <add name="cnn" connectionString="Data Source=;Initial Catalog=;User ID=;Password=" xdt:Transform="Replace"/>
  </connectionStrings>
  
  <system.web>
    <compilation xdt:Transform="RemoveAttributes(debug)" />

    <customErrors  mode="On" defaultRedirect="~/Error/Generic" xdt:Transform="Replace">
      <error statusCode="404" redirect="~/Error/NotFound" />
    </customErrors>
    
  </system.web>
</configuration>

For the above transform, this can be incorporated by referencing the task at the top of the build file

build.xml
<Project 
  ToolsVersion="4.0" 
  DefaultTargets="Default" 
  xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  
<UsingTask 
    TaskName="TransformXml"
    AssemblyFile="$(MSBuildExtensionsPath)\Microsoft\VisualStudio\v10.0\Web\Microsoft.Web.Publishing.Tasks.dll"/>

Then, adding and configuring the TransformXml task

<TransformXml
  Source="$(SourcePath)\web.config"
  Transform="$(TransformPath)\Web.$(Configuration).config"
  Destination="$(DestinationPath)\web.config"
  StackTrace="false" />

Zipping the build output

Using the MSBuild Community Tasks Project Zip task, this is as simple as

<Zip 
    Files="@(zipFiles)"
    WorkingDirectory="$(OutputPath)\"
    ZipFileName="$(OutputPath)\$(DeployName) $(Version) - compiled.zip"/>

Running the build file

The build file can be called from a batch file, passing in the required arguments. The options details below can be found at MSBuild Command-Line Reference

ReleaseBuild.bat
@echo off
call "%VS100COMNTOOLS%\vsvars32.bat" 

set /p version=Enter version (n.n.n.n):
set /p deploy=Deploy (y/n):
 
 
@echo on
msbuild build.xml /p:Configuration=Release /p:Version=%version% /p:Deploy=%deploy%  /t:build /verbosity:m /nodeReuse:True 
  /fl /flp1:logfile=errors.txt;errorsonly

@pause