MSBuild basics for Sitecore devs

MSBuild can be your friend. At first, it is a difficult friendship though, because it speaks a different language. However, if you talk to it patiently, it can do a lot of useful things for you. For example, it can build your Helix projects in the same way as gulp scripts do, but much, much faster. But this is a story that I will tell you next time. Today let’s start with some basics.

Target

Consider the following basic example:

<Project>
  <Target Name="Hello">
    <Message Text="MSBuild says hello!" />
  </Target>
</Project>

Copy and save it as a Hello.csproj file, then open Developer Command Prompt for Visual Studio 2017 from Start menu or go to the MSBuild directory C:\Program Files (x86)\Microsoft Visual Studio\2017\BuildTools\MSBuild and open PowerShell there. Then execute the following command:

MSBuild.exe Hello.csproj /t:"Hello"

You should see the MSBuild says hello! message. The /t: parameter is the name of the target you want to run.

You can think about a Target as a method or function. Target is a list of Tasks that are executed one by one in order. In the example above we created one target named Hello. It contains one Task named Message. The Message is a predefined task. There is plenty of other predefined tasks that you can use. You can also implement custom tasks using C#.

Property

Think of a Property as a string variable. Each property has to be inside PropertyGroup. You can have one or more PropertyGroup and one or more Property inside single group. The PropertyGroup is just a separator. Let’s add one to our Hello.csproj file:

<Project>
  <PropertyGroup>
    <HelloMessage>MSBuild says hello from property</HelloMessage>
  </PropertyGroup>
  <Target Name="Hello">
    <Message Text="$(HelloMessage)" />
  </Target>
</Project>

Now our Hello target displays message that is stored in HelloMessage property. We access that property using $(<PropertyName>). There is also the additional advantage of using properties. You can set it’s value in command line like this:

MSBuild.exe Hello.csproj /t:"Hello" /p:HelloMessage="New Message"

The value from command line will be used instead of the one from Hello.csproj file.

Item

Think of an Item as a list of objects. Each object can have Metadata and has to be inside ItemGroup. Similar like for PropertyGroup the ItemGroup is just a container. Thanks to this, you can have property and item with the same name. Let’s update our Hello.csproj:

<Project>
  <PropertyGroup>
    <HelloMessage>MSBuild says hello from property</HelloMessage>
    <FilesToList>C:\\*.*</FilesToList>
  </PropertyGroup>
  <ItemGroup>
    <FilesToList Include="$(FilesToList)" />
  </ItemGroup>
  <Target Name="Hello">
    <Message Text="$(HelloMessage)" />
    <Message Text="@(FilesToList)" />
    <Message Text="%(FilesToList.FullPath)" />
  </Target>
</Project>

We added FilesToList property and we added FilesToList item. The name is the same however those are different things. We can access property with $ and item with @ or % if we want to get metadata. We also updated our Hello target. Now it displays two additional messages. The first one is the FilesToList item accessed with @ and it displays a list of filenames joined by ;. The second message is the same FilesToList item accessed with %. This time we get a list of FullPath of each file separated by new line.

You can find a list of Well-known Item Metadata here. You can also add your own Metadata.

DependsOnTargets

Let’s add the second target to our example:

<Project>
  <PropertyGroup>
    <HelloMessage>MSBuild says hello from property</HelloMessage>
    <FilesToList>C:\\*.*</FilesToList>
  </PropertyGroup>
  <Target Name="Hello" DependsOnTargets="PrepareFilesToList">
    <Message Text="$(HelloMessage)" />
    <Message Text="@(FilesToList)" />
    <Message Text="%(FilesToList.FullPath)" />
  </Target>
  <Target Name="PrepareFilesToList">
    <ItemGroup>
      <FilesToList Include="$(FilesToList)" />
    </ItemGroup>
  </Target>
</Project>

The DependsOnTargets attribute in our Hello target, tells MSBuild that Hello target depends on PrepareFilesToList target. MSBuild runs the PrepareFilesToList before Hello target. Inside the PrepareFilesToList we create FilesToList item and then it can be used by Hello target.

We can depend on more than one target if we want:

<Project>
  <PropertyGroup>
    <HelloMessage>MSBuild says hello from property</HelloMessage>
    <FilesToList>C:\\*.*</FilesToList>
  </PropertyGroup>
  <Target Name="Hello" DependsOnTargets="SayHello;PrepareFilesToList">
    <Message Text="$(HelloMessage)" />
    <Message Text="@(FilesToList)" />
    <Message Text="%(FilesToList.FullPath)" />
  </Target>
  <Target Name="PrepareFilesToList">
    <ItemGroup>
      <FilesToList Include="$(FilesToList)" />
    </ItemGroup>
  </Target>
  <Target Name="SayHello">
    <Message Text="$(HelloMessage)" />
  </Target>
</Project>

To separate list of targets use ;.

BeforeTargets and AfterTargets

MSBuild 4.0 introduced two new attributes: BeforeTargets and AfterTargets. You can use them instead of DependsOnTargets:

<Project>
  <PropertyGroup>
    <HelloMessage>MSBuild says hello from property</HelloMessage>
    <FilesToList>C:\\*.*</FilesToList>
  </PropertyGroup>
  <Target Name="Hello">
    <Message Text="$(HelloMessage)" />
    <Message Text="@(FilesToList)" />
    <Message Text="%(FilesToList.FullPath)" />
  </Target>
  <Target Name="PrepareFilesToList" BeforeTargets="Hello">
    <ItemGroup>
      <FilesToList Include="$(FilesToList)" />
    </ItemGroup>
  </Target>
  <Target Name="SayHello" BeforeTargets="Hello">
    <Message Text="$(HelloMessage)" />
  </Target>
</Project>

Hello does not depend directly on other targets, however, you directed MSBuild to execute PrepareFilesToList and SayHello targets before Hello target. Sometimes it’s easier to use these two attributes to plug your code into an existing pipeline.

You can read more details about the order in which targets are run here.

Conditions

You can use conditions to execute some parts of code only if something is true:

<Project>
  <PropertyGroup>
    <HelloMessage Condition="$(HelloMessage) == ''">MSBuild says hello from property</HelloMessage>
    <FilesToList>C:\\*.*</FilesToList>
  </PropertyGroup>
  <Target Name="Hello">
    <Message Text="$(HelloMessage)" />
    <Message Text="@(FilesToList)" />
    <Message Text="%(FilesToList.FullPath)" />
  </Target>
  <Target Name="PrepareFilesToList" BeforeTargets="Hello">
    <ItemGroup>
      <FilesToList Include="$(FilesToList)" />
    </ItemGroup>
  </Target>
  <Target Name="SayHello" BeforeTargets="Hello">
    <Message Text="$(HelloMessage)" />
  </Target>
</Project>

When HelloMessage is empty then set it to our initial message. You can use conditions on properties, items and targets (and some others).

Examine Class Library project

Below you can see the content of a new Class Library project that I created in VisualStudio 2017:

<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
  <PropertyGroup>
    <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
    <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
    <ProjectGuid>edcbf0e5-7fe5-4ac2-af45-979f949d03db</ProjectGuid>
    <OutputType>Library</OutputType>
    <AppDesignerFolder>Properties</AppDesignerFolder>
    <RootNamespace>ClassLibrary1</RootNamespace>
    <AssemblyName>ClassLibrary1</AssemblyName>
    <TargetFrameworkVersion>v4.7.2</TargetFrameworkVersion>
    <FileAlignment>512</FileAlignment>
    <Deterministic>true</Deterministic>
  </PropertyGroup>
  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
    <DebugSymbols>true</DebugSymbols>
    <DebugType>full</DebugType>
    <Optimize>false</Optimize>
    <OutputPath>bin\Debug\</OutputPath>
    <DefineConstants>DEBUG;TRACE</DefineConstants>
    <ErrorReport>prompt</ErrorReport>
    <WarningLevel>4</WarningLevel>
  </PropertyGroup>
  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
    <DebugType>pdbonly</DebugType>
    <Optimize>true</Optimize>
    <OutputPath>bin\Release\</OutputPath>
    <DefineConstants>TRACE</DefineConstants>
    <ErrorReport>prompt</ErrorReport>
    <WarningLevel>4</WarningLevel>
  </PropertyGroup>
  <ItemGroup>
    <Reference Include="System"/>
    
    <Reference Include="System.Core"/>
    <Reference Include="System.Xml.Linq"/>
    <Reference Include="System.Data.DataSetExtensions"/>
    
    
    <Reference Include="Microsoft.CSharp"/>
    
    <Reference Include="System.Data"/>
    
    <Reference Include="System.Net.Http"/>
    
    <Reference Include="System.Xml"/>
  </ItemGroup>
  <ItemGroup>
    <Compile Include="Class1.cs" />
    <Compile Include="Properties\AssemblyInfo.cs" />
  </ItemGroup>
  <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
 </Project>

We have three PropertyGroup and two ItemGroup. In the first PropertyGroup there is Configuration property. It has Condition attribute that directs MSBuild to set it’s value to Debug only if it’s empty. You can set configuration in Visual Studio when you do a build and in that case, Visual Studio will pass correct value to the Configuration property. The other two PropertyGroup also has conditions. This time, however, whole PropertyGroup will be executed or not. Have you noticed how the condition is concatenated to use two parameters?

 <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">

It is similar to interpolated string in C#

We also have two ItemGroup. We could, of course, put everything into single ItemGroup. It just separates two different lists. The first ItemGroup adds all project references into Reference list. The second one adds all files into Compile list. MSBuild will then use those properties and items to build your project.

Imports

When you execute Clean, Build or Rebuild from Visual Studio you actually execute targets with the same name that are defined in Microsoft.Common.targets file. In the project above, there is Import line at the end. It loads Microsoft.CSharp.targets file that is located in the path stored in a MSBuildToolsPath property. This file contains bunch of other imports inside and Microsoft.Common.targets is between them.

Import is one of a few ways how you can extend MSBuild with your custom code. Read my next article in the series to find out more about this.

Advanced topics

This article only describes the basics of MSBuild. The best way to learn more is to read official documentation, read Microsoft.Common.targets and other target files and read this book.

I want more

To learn more, read my next article in the series where I described msbuild extension points.