MSBuild Tutorial - The comprehensive guide

TOC

This article gives a comprehensive introduction to the MSBuild tool.

MSBuild Tutorial - The comprehensive guide

MSBuild Basics

.NET 2.0 introduced a new tool called MSBuild. MSBuild is primarily used to build .NET applications, but can be used for other types of applications as well. From Windows Vista, MSBuild is distributed with Windows itself, making it a strong platform for implementing scripted application on non-developer PCs as well.

Before .NET 2.0, application were built by one or more of these strategies:

  • Using the build menu from inside Visual Studio
  • Using the devenv.exe process from the command line with some extra parameters
  • Using a third-party tool like NAnt (which MSBuild is based on)

The drawback with the first two strategies was that they were hard to script. The drawback with the third solution was that two build environments had to be held in sync and maintained. .NET 2.0 Visual Studio projects are now implemented in the MSBuild language, but MSBuild has no dependencies on Visual Studio itself. MSBuild can, in theory, be implemented in files with any extension, but a number of formats seem to be the supported standard from Microsoft: csproj, vbproj, vcproj, vjsproj, proj, and .targets. The first four are different types of project files for Visual Studio, while the last two are new extensions, which are either written by hand or with a tool knowing the MSBuild syntax. A few tools exist for this purpose. My personal opinion is that building in xml editor in Visual Studio is quite good for this purpose. A repeating question among MSBuild developers is if you should extend the files generated by visual studio with new stuff, or whether you should write your own containing your home-made MSBuild scripts. My personal opinion is to let Visual Studio generate its own projects (example: csproj files) and create your home-made scripts in a .proj file with the same name as the project. I usually start the .proj file by implementing the three targets: Clean, Build and Rebuild, which basically just calls the similar targets in the .csproj file. This way, you avoid updating an auto-generated file which you don’t have control over.

An MSBuild file is implemented in XML. The root element of an MSBuild XML file will always be a project containing target elements. It is possible to run multiple targets in an MSBuild file. When the MSBuild script is started from the command line, it takes a build file as parameter as well as the names of the targets, which should be executed.

A target is implemented as a sequential list of tasks. The tasks are executed in the order in which they are specified beneath the target element. Both targets and tasks can be dependent on each other.

This is an example of a very simple file to build a .NET project:

<?xml version="1.0" encoding="utf-8" ?>
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <Target Name="Build">
    <Message Text="Building msbuildintro" />
    <MSBuild Projects="msbuildintro.csproj" Targets="Build" />
  </Target>
</Project>

The example defines a project with the MSBuild namespace. Using the namespace helps you develop the build file in Visual Studio using Intellisense. The build file contains a single target named Build. This target contains two tasks: Message and MSBuild. Message outputs a message to the console and is used primarily for debugging purposes. MSBuild runs a configurable number of targets on another build file. Since the .csproj file is also implemented in MSBuild, we are able to call targets in the build file from the example build file. In this case, we call the target Build in the .csproj file. All projects generated by Visual Studio automatically implement the Clean, Build and Rebuild targets. These targets are actually executed when you select the similar commands in the Build menu from within Visual Studio.

Properties

To be able to parameterize a build script, MSBuild has implemented properties. A property is a simple key/value type, which can be used from multiple locations in the scripts. Let’s look at an example using properties:

<?xml version="1.0" encoding="utf-8" ?>
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <PropertyGroup>
    <MyReleaseOutput>.\release</MyReleaseOutput>
  </PropertyGroup>
  <Target Name="Build">
    <Message Text="Building msbuildintro" />
    <MSBuild Projects="msbuildintro.csproj" Targets="Build" />
  </Target>
  <Target Name="Release" DependsOnTargets="Build">
    <MakeDir Directories="$(MyReleaseOutput)" />
    <Copy SourceFiles=".\bin\debug\msbuildintro.exe" DestinationFolder="$(MyReleaseOutput)" />
  </Target>
</Project>

This example defines a property defined in a property group. Properties can be defined both outside and inside the scope of a target. When a property is defined as in this example, the value of the property is assigned before executing the actual targets. In this example, I define a property called MyReleaseOutput and add the value .\release. I have added two targets as well: Build and Release. The Build target is similar to the one specified in the previous example, while the Release target is new. The Release target introduces a new attribute called DependsOnTargets, which is the way to define dependencies between targets in MSBuild. In this case, we tell MSBuild to run the Build target before running the Release targets. If you run the Build target manually before running the Release target, the Build target is run only once.

The release target introduces two new tasks as well. MakeDir, which (well you guessed it) creates a new directory and Copy, which copies on or more files from A to B. MakeDir contains a single attribute, defining the path of the new directory.
In this example that path is the value of the MyReleaseOutput property. Notice the use of the $(propertyname) syntax here. $() is used to reference properties in MSBuild. The copy task in our example points out a single file and the destination folder where this file should be copied to when executing the Release target. The $(MyReleaseOutput) used is again replaced by the value of this property.

Items

The example above copied a single file from one directory to another. This can certainly be useful in many situations, but sometimes this is simply not flexible enough. This is where items come in. An item gives the possibility of creating dynamic lists of, for instance, file names. Let’s start by looking at an example:

<?xml version="1.0" encoding="utf-8" ?>
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <PropertyGroup>
    <MyReleaseOutput>.\release</MyReleaseOutput>
  </PropertyGroup>
  <ItemGroup>
    <MyReleaseFiles Include=".\bin\debug\*.*" />
  </ItemGroup>
  <Target Name="Build">
    <Message Text="Building msbuildintro" />
    <MSBuild Projects="msbuildintro.csproj" Targets="Build" />
  </Target>
  <Target Name="Release" DependsOnTargets="Build">
    <MakeDir Directories="$(MyReleaseOutput)" />
    <Copy SourceFiles="@(MyReleaseFiles)" DestinationFolder="$(MyReleaseOutput)" />
  </Target>
</Project>

We start by defining an ItemGroup outer scope of a target. This means that the content is calculated before actually running any targets. In this example, I define an ItemGroup containing a single item called MyReleaseFiles. The item contains a single attribute called Include, which acts as a filter for the generated file list. In this case, all files beneath the bin\debug directory. I use this item in the copy task beneath the Release target to indicate which files should be copied when doing a release.

The Include filter decides which file to include. We also have the possibility to specify an Exclude filter. The following example shows how:

<?xml version="1.0" encoding="utf-8" ?>
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <PropertyGroup>
    <MyReleaseOutput>.\release</MyReleaseOutput>
  </PropertyGroup>
  <ItemGroup>
    <MyReleaseFiles Include=".\bin\debug\*.*" Exclude=".\bin\debug\*vshost.exe" />
  </ItemGroup>
  <Target Name="Build">
    <Message Text="Building msbuildintro" />
    <MSBuild Projects="msbuildintro.csproj" Targets="Build" />
  </Target>
  <Target Name="Release" DependsOnTargets="Build">
    <MakeDir Directories="$(MyReleaseOutput)" />
    <Copy SourceFiles="@(MyReleaseFiles)" DestinationFolder="$(MyReleaseOutput)" />
  </Target>
</Project>

I added an Exlude attribute to my MyReleaseFiles item. The exclude in this example excludes all files beneath the bin\debug directory which end in vshost.exe. An item containing both Include and Exclude will be generated by taking all the files specified in the Include and subtracting the files specified in the Exlude.

The last two examples of using items are actually not that useful because the content for the MyReleaseFiles item is generated before running the actual targets. On a new checkout or after a clean, no files exist in the bin\debug directory meaning that the generated file list will therefore be completely empty. Bummer! Let us fix the example. We define a new item inside the scope of a target instead:

<?xml version="1.0" encoding="utf-8" ?>
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <PropertyGroup>
    <MyReleaseOutput>.\release</MyReleaseOutput>
  </PropertyGroup>
  <Target Name="Build">
    <Message Text="Building msbuildintro" />
    <MSBuild Projects="msbuildintro.csproj" Targets="Build" />
  </Target>
  <Target Name="Release" DependsOnTargets="Build">
    <MakeDir Directories="$(MyReleaseOutput)" />
    <ItemGroup>
      <MyReleaseFiles Include=".\bin\debug\*.*" Exclude=".\bin\debug\*vshost.exe" />
    </ItemGroup>
    <Copy SourceFiles="@(MyReleaseFiles)" DestinationFolder="$(MyReleaseOutput)" />
  </Target>
</Project>

I simply moved the ItemGroup named MyReleaseOutput item inside the Release target.

Conditions

Besides the Property and ItemGroup task in a previous example, our build scripts have been very static until now. This can be sufficient for a lot of projects, but sometimes you need slightly more flexible build scripts. A feature for fulfilling this in MSBuild is called Conditions. A condition in MSBuild is not that different from a condition in other software languages. It is a possibility, for instance, to make more flexible structures to avoid running targets and/or tasks when certain conditions are met. You can define conditions on all targets and almost all tasks. Let’s check out another example:

<?xml version="1.0" encoding="utf-8" ?>
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <PropertyGroup>
    <MyReleaseOutput>.\release</MyReleaseOutput>
    <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
  </PropertyGroup>
  <Target Name="Build">
    <Message Text="Building msbuildintro $(Configuration)" />
    <MSBuild Projects="msbuildintro.csproj" Targets="Build" />
  </Target>
  <Target Name="Release" DependsOnTargets="Build" Condition="$(Configuration) == 'Release'">
    <MakeDir Directories="$(MyReleaseOutput)" />
    <ItemGroup>
      <MyReleaseFiles Include=".\bin\$(Configuration)\*.*" Exclude=".\bin\$(Configuration)\*vshost.exe" />
    </ItemGroup>
    <Copy SourceFiles="@(MyReleaseFiles)" DestinationFolder="$(MyReleaseOutput)" />
  </Target>
</Project>

I added a lot of new stuff here. But of course you are already quite an MSBuild expert, so I think you will manage. First of all, I have added a new property named Configuration. This property contains a condition attribute, which determines the value of the property at runtime. If the Configuration property has not been set up front; the value of the attribute is set to Debug. Properties can be set upfront by using a special syntax on the command line:

Msbuild.exe build.proj /p:Configuration=Debug /t:release

More new stuff. I changed all references to the debug, with the value of the new Configuration property. This means that I am now able to run the script in both Debug and Release mode.

The last new addition is the condition attribute on the release target. This condition examines if the value of the Configuration property is Release and only allows execution of the Release target if this condition is true.

Frequently Used Tasks

CallTarget – Executes another target inside the current build file. Used where a target needs to call another target at a specific place inside the target itself. If the target were just dependant on the other target being executed, we would use the DependsOnTargets attribute instead.

Copy – We already saw this task in action. Copies one or more files from A to B.

Delete – Deletes on or more files.

Exec – Executes an external process. This is typically used to executed external tools, which did not implement MSBuild targets themself.

MakeDir – Creates one or more directories.

Message – Outputs a message to the console.

MSBuild – Executes one or more targets in an external MSBuild file.

RemoveDir – Removes one or more directories.

And there are a lot more.

Custom MSBuild Tasks

It is possible to write your own MSBuild tasks. This can be extremely useful if you want to integrate some of your own tools with MSBuild or if you need to do stuff which is not yet supported by MSBuild. The following example shows how to write a simple addition task:

using System;
using Microsoft.Build.Utilities;
using Microsoft.Build.Framework;
namespace MyTasks
{
  public class AddTask : Task
  {
    private int number1;
 
    [Required]
    public int Number1
    {
      get { return number1; }
      set { number1 = value; }
    }
 
    private int number2;
 
    [Required]
    public int Number2
    {
      get { return number2; }
      set { number2 = value; }
    }
 
    private int sum;
 
    [Output]
    public int Sum
    {
      get { return sum; }
      set { sum = value; }
    }
 
    public override bool Execute()
    {
      try
      {
        sum = number1 + number2;
      }
      catch (ArithmeticException e)
      {
        Console.WriteLine("Error occured during addition: {0}", e.Message);
        return false;
      }
      return true;
    }
  }
}

I start by defining the AddTask class which extends the Task class from the MSBuild API. By extending this task, I don’t need to worry about implementing conditions etc.

I defined two properties which act as input variables to the addition task: Number1 and Number2. Both properties are marked with the Required attribute which tells the script to fail if they are not specified. An output property is added as well. The value of this property will be available within the build script. Catching an (System.ArithmeticException)[https://elmah.io/exceptions/System.ArithmeticException/] to illustrate how the Execute method needs to return false in the case of an error.

Let’s see how to use this new and fancy task:

<?xml version="1.0" encoding="utf-8" ?>
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <UsingTask TaskName="MyTasks.AddTask" AssemblyFile=".\AddTask.dll"/>
  <Target Name="Addition">
    <AddTask Number1="10" Number2="12">
      <Output TaskParameter="Sum" PropertyName="CalculatedSum" />
    </AddTask>
    <Message Text="10 + 12 = $(CalculatedSum)" />
  </Target>
</Project>

Because my AddTask in an external task not built into the MSBuild framework, I use the UsingTask element to reference it. The name of the task as well as the assembly name is specified.

The Addition target uses the AddTask with the two Number properties as attributes. The Output element copies the result of the task to a property called CalculatedSum. This property is accessible in the Message task for output on the console.

You just implemented your first custom MSBuild task. Pretty easy, right?

Generic Targets

Once in a while a question pops up in different forums: How do I parameterize a common part of my script, making it possible to use it from different targets? This is a great question. We don’t want to have the same lines of code spread around our build script, making it longer than necessary and complex to maintain. MSBuild provides the CallTarget task, which executes another target inside the current build file. Unfortunately there is no way to parameterize the CallTarget task, making it unusable for anything other than simple situations where needed tasks are 100% identical.

Let’s look at and example where we have two targets with some identical tasks:

<?xml version="1.0" encoding="utf-8" ?>
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <Target Name="Target1">
    <Message Text="Hello World from Target1" />
  </Target>
  <Target Name="Target2">
    <Message Text="Hello World from Target2" />
  </Target>
</Project>

In the example we define two targets: Target1 and Target2. You will probably notice that the targets look very similar. The only difference is the text inside the Message task. This is a simple example of the problem and of course changing the text in both message tasks would be fairly simple. But you probably understand the problem here: It would be nice to refactor the targets (let’s call it “Extract Target”), so that the Hello World text will only be specified once. First of all we need to move the common tasks to a new target:

<Target Name="PrintMessage">
  <Message Text="Hello World from X" />
</Target>

Note that the replaced part of the Text with the X. Now we need to call this target, making sure that X will be replaced with the correct string. For this purpose we use another task called MSBuild. The MSBuild task was originally produced to be able to call targets inside other MSBuild scripts. The great thing about the MSBuild task is that it has the capability to receive properties. In order to call our PrintMessage target and print the correct string, we parameterize the target like this:

<?xml version="1.0" encoding="utf-8" ?>
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <Target Name="Target1">
    <MSBuild Projects="$(MSBuildProjectFile)" Targets="PrintMessage" Properties="Caller=Target1"/>
  </Target>
  <Target Name="Target2">
    <MSBuild Projects="$(MSBuildProjectFile)" Targets="PrintMessage" Properties="Caller=Target2"/>
  </Target>
  <Target Name="PrintMessage">
    <Message Text="Hello World from $(Caller)" />
  </Target>
</Project>

We use the MSBuild tasks in both Target1 and Target2. The $(MSBuildProjectFile) is a built-in property in MSBuild, which returns the filename of the current build file. What we do here is that we call the PrintMessage task on this. The properties attribute specify a single property named Caller. Multiple properties can be specified inside this attribute by separating them with a semicolon. In the PrintMessage task we simply print the message with the Caller property specified inside the Text attribute.

Property functions

Property functions are a great addition to the MSBuild language, introduced in MSBuild 4.0. Before property functions, some of the most simple tasks to execute in .NET, took ages implementing in MSBuild, cause you’d need to write custom tasks for every function not build into MSBuild or available through third party tasks. Allow me to explain the basics of property functions through an example.The purpose of the example MSBuild script is to write the current date and time to the console. Admitted not the most advanced script, but it serves our purpose of explaining property functions just fine. In the old days (before 4.0), you would either have to find some third party library or alternatively implement one yourself. The code is of course trivial, but you’d still need to write and maintain it. With property functions doing things like this is almost as easy as in .NET. The script can be implemented using property functions as this:

<?xml version="1.0" encoding="utf-8" ?>
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <Target Name="PrintCurrentDateTime">
    <Message Text="The current date and time is: $([System.DateTime]::Now)." />
  </Target>
</Project>

Notice that no external tasks are imported to write this script. In order to generate the current data and time, I simply use the $() syntax, which we recognise from the normal way of referencing MSBuild properties. Instead of including a property name inside the parethesis, I use one of the build-in System types available through .NET, in this case System.DateTime. Also notice that System.DateTime is included in angular brackets ([ ]), which tells MSBuild that this is in fact not a property declared elsewhere in the build file, but one of the supported types from .NET. Knowing that DateTime contains a static property name Now, I can simply reference this through the double comma (::) syntax, which translates into a dot (.) in .NET. It may seem a bit strange for some people, why Microsoft choose the double comma syntax instead of the ordinary dot syntax in .NET, but for PowerShell developers this should be spot on.

elmah.io: Error logging and Uptime Monitoring for your web apps

This blog post is brought to you by elmah.io. elmah.io is error logging, uptime monitoring, deployment tracking, and service heartbeats for your .NET and JavaScript applications. Stop relying on your users to notify you when something is wrong or dig through hundreds of megabytes of log files spread across servers. With elmah.io, we store all of your log messages, notify you through popular channels like email, Slack, and Microsoft Teams, and help you fix errors fast.

See how we can help you monitor your website for crashes Monitor your website