Codementor Events

Quick Intro to MSBuild Projects

Published Sep 15, 2015Last updated Mar 13, 2018
Quick Intro to MSBuild Projects

Note: This article pertains to Visual Studio 2015 and preceding projects. The behavior of MSBuild projects has changed in Visual Studio 2017 and is not covered here.

Have you ever opened up a .csproj file (the project file created by Visual Studio for C#) and wondered what all that gobbligook was all about?
This is a quick overview introduction to MSBuild projects so that you can read and understand Visual Studio project XML syntax right away.

Did you know that Visual Studio Projects are MSBuild Scripts?!

I always knew this at a cursory level, since after all I knew that MSBuild was building my projects and I knew that the projects had what MSBuild needed to get the job done. But I never knew that these were not just manifests and high level metadata but you can actually have full control over the way MSBuild does things--even invoke code, even C# code--using semantics that are surprisingly expressive and flexible.

It's all in the XML

To get started, create a new C# project in Visual Studio, any "normal" app such as a console application, class library, Windows Forms app, or WPF app. You might even create a Web App.

Once created, open up the .csproj file and take look. Here's a default console app.

<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="14.0" DefaultTargets="Build" 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>{64AD1366-8340-4451-973F-36CC0CC76280}</ProjectGuid>
      <OutputType>Exe</OutputType>
      <AppDesignerFolder>Properties</AppDesignerFolder>
      <RootNamespace>ConsoleApplication5</RootNamespace>
      <AssemblyName>ConsoleApplication5</AssemblyName>
      <TargetFrameworkVersion>v4.5.2</TargetFrameworkVersion>
      <FileAlignment>512</FileAlignment>
      <AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
    </PropertyGroup>
    <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
      <PlatformTarget>AnyCPU</PlatformTarget>
      <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' ">
      <PlatformTarget>AnyCPU</PlatformTarget>
      <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="Program.cs" />
      <Compile Include="Properties\AssemblyInfo.cs" />
    </ItemGroup>
    <ItemGroup>
      <None Include="App.config" />
    </ItemGroup>
    <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
  <!-- To modify your build process, add your task inside one of the targets below and uncomment it. 
         Other similar extension points exist, see Microsoft.Common.targets.
    <Target Name="BeforeBuild">
    </Target>
    <Target Name="AfterBuild">
    </Target>
  -->
</Project>

Let's break this down.

The Most Important Components

PropertyGroup Blocks and Properties

<PropertyGroup>
    <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
    <!-- ... -->
</PropertyGroup>

<PropertyGroup> blocks allow you to define properties and their values. If a property already existed its value will be updated, otherwise the property will also be created. These properties are named by tag name and their value declared by text value. So in the above example, the property name is "Configuration", and the value is "Debug". There is also a condition on this property, which is "'$(Configuration)' == ''". This basically means that this value assignment is only applied if there isn't already a value, and there will be no value (that is, the value will be empty string) if the Configuration property has never been declared prior to this, in which case this PropertyGroup is declaring this property for the first time.

You read these properties out anywhere in the MSBuild script (such as the Condition here but elsewhere too) using the syntax "$(PropertyName)". You can even read the same property while updating itself to append it .. for example ..

<PropertyGroup>
    <PropertyA>A</PropertyA>
    <PropertyB>B</PropertyB>
    <PropertyAB>$(PropertyA).$(PropertyB)</PropertyAB>
    <PropertyA>$(PropertyA)X</PropertyA>
</PropertyGroup>

In this example, PropertyAB's value becomes "A.B". A property can also be replaced. These declarations are sequentially evaluated. So while PropertyAB is "A.B", PropertyA is "AX".

Sometimes properties are imported from a ".props" file, i.e. "MyProperties.props". This is done using the syntax:

<Import Project="MyProperties.props" />

The syntax of the contents of the .props file is exactly the same as the project, given the necessary <Project> container element.

<?xml version="1.0" encoding="utf-8"?>
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
    <PropertyGroup>
        <MyCustomProperty>my value</MyCustomProperty>
    </PropertyGroup>
</Project>

Property Functions

As mentioned, properties can be read into attributes or values using the syntax $(PropertyName). You can also evaluate .NET 4+ functions in properties using the syntax $([CLASS]::METHOD(arguments)) or $([CLASS]::PROPERTY). For example,

<PropertyGroup>
    <BuildTime>$([System.DateTime]::Now</BuildTime>
</PropertyGroup>

Items

As we know, Visual Studio projects normally consist of file manifests. The assumption made by some, prior to learning this detail about MSBuild scripts, is that a project is just a manifest plus some properties that get passed on to a compiler, or something, whatever. Well, the manifest part is certainly true. The format of this manifest is, of course, the combination of the <ItemGroup> blocks.

<ItemGroup>
    <Compile Include="Program.cs" />
    <Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup>
<ItemGroup>
    <None Include="App.config" />
</ItemGroup>

As with <PropertyGroup> tags, ItemGroup tags are simply combined and their being broken up into multiple groups is largely inconsequential. Having multiple groups split as such does offer some flexibility to add conditions or metadata on specific groups, however.

The tag names used for items under <ItemGroup> are the item types. These also correlate to the Build Action property that you see in Visual Studio in the Solution Explorer for a particular file. The "None" item type calls for the "None" Build Action. The "Compile" item type calls for the file to be compiled, using the compiler associated with the project (i.e. csc, which is the C# compiler). Generally these are added to a list of parameters for one csc invocation, not typically isolated to separate invocations. "Content" marks it as content. And so the list goes on.

<Reference> items obviously aren't file manifest items, but are still part of the manifest.

All item types have an Include=".." attribute. Some item types call for additional attributes or even nested tags, but these go beyond the scope of this article. As with properties, item types can be custom set for your own use.

We will circle around back to items when we look at consuming them in tasks, but here's a hint: Items of a common type can be iterated over using the '%' character.

Targets

You can think of targets as named sets of instructions of tasks. They are like named methods or functions or subroutines.

<Target Name="MyCustomTarget" AfterTargets="Build">
    <Message Text="Hello World" Importance="high" />
</Target>

In this example, a new target named "MyCustomTarget" is declared. It will be invoked after Build has been invoked. The target will invoke the Message task, causing the Output window in Visual Studio to output "Hello World". (The Importance="high" attribute enables it to appear with the default log verbosity in Visual studio.)

This is functionally equivalent to a C# method that might look like this pseudocode:

[InvokeAfter("Build")]
public static void MyCustomTarget() {
    Log.WriteLine("Hello World", LogImportance.high);
}

I call this "pseudocode" because the above function has no real world invocation scenario, but it sure reads understandably.

Targets are sometimes imported into a project from ".targets" files.

<Import Project="MyTargets.targets" />

As with the .props file mentioned earlier, the syntax of the contents of the imported file is just the same as the project XML syntax including the <Project> container element.

<?xml version="1.0" encoding="utf-8"?>
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
    <Target Name="MyCustomTarget" AfterTargets="Build">
        <Message Text="Hello World" Importance="high" />
    </Target>
</Project>

Nested Properties and Items

A custom <Target> can declare its own <PropertyGroup> and its own <ItemGroup> (and, of course, declare things within them). The scope of properties or property overrides when declared within the <Target> is applied by that <Target> when it executes.

For example:

<PropertyGroup>
    <MyProperty>This is the default value.</MyProperty>
</PropertyGroup>
<Target Name="MyTarget">
    <Message Text="$(MyProperty)" Importance="high" />
</Target>
<Target Name="AnotherTarget">
    <PropertyGroup>
        <MyProperty>This is the overridden value.</MyProperty>
    </PropertyGroup>
    <Message Text="$(MyProperty)" Importance="high" />
</Target>
<Target Name="ExecuteCustomTargets" AfterTargets="Build">
    <CallTarget Targets="MyTarget" />
    <CallTarget Targets="AnotherTarget" />
</Target>

If the above example was pasted directly into your project, the resulting output in the Output log window would look like this:

This is the default value.
This is the overridden value.

Tasks

In the Targets section above where an example Target was declared, the example included a task. That task was <Message>. MSBuild ships out-of-the-box with a large number of built-in tasks that you can execute in your targets. You can find them documented here.

Community Tasks

You can also make use of a number of additional tasks created by the MSBuild community. Particularly of interest is the project known as "MSBuild Community Tasks" which is available here.

Getting started with MSBuild Community Tasks

To get started with MSBuild Community Tasks, these are the steps I found worked for me.

  1. In Visual Studio, open the Package Manager console window and execute: Install-Package MSBuildTasks
  2. The solution now has a new .build virtual folder. Open the .targets file.
  3. In the <PropertyGroup> block at the top, remove the first property, then modify the second property so that only the filename of the DLL is in the string (no directory, no parenthesis).
  4. Just before your custom <Target> in your project, add:
    <Import Project="$(MSBuildProjectDirectory)\..\.build\MSBuild.Community.Tasks.targets" />
    Note that this assumes that the project directory is contained in the solution directory, otherwise you can eliminate the "..".
  5. Now you can add one or many of the documented community tasks. Try <Beep /> to validate things are working.

UsingTask Declarations

The <UsingTask> declaration enables you to use tasks in your target(s) that you would otherwise not have access to. You can use the <UsingTask> XML element to declare one of at least a couple different things:

  1. Import a task or tasks from an external DLL, such as TransformXml. Or,
  2. Declare a new custom task, such as an inline task. An inline task is a task that is created inline in the project and can be written in C#.

An example of importing tasks:

<UsingTask TaskName="TransformXml" AssemblyFile="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v12.0\Web\Microsoft.Web.Publishing.Tasks.dll" />

An example of an inline task and invoking it:

<UsingTask TaskName="Hello" TaskFactory="CodeTaskFactory" AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.v4.0.dll" >
  <ParameterGroup />
  <Task>
    <Code Type="Fragment" Language="cs">
      Log.LogMessage("Hello, world!", MessageImportance.High);
    </Code>
  </Task>
</UsingTask>
<Target Name="TestBuild">
  <Hello />
</Target>

Iterating over Items

I mentioned earlier that you can iterate over items using the % character. This sounds gnarly, but it's actually quite handy. Consider this example:

<ItemGroup>
    <Attendee Include="Fred">
        <Guest>Ruth</Guest>
    </Attendee>
    <Attendee Include="John">
        <Guest>Michael</Guest>
    </Attendee>
</ItemGroup>
<Target>
    <Rsvp Attendee="%(Attendee.Identity)" Guest="%(Attendee.Guest)" />
</Target>

In this example, a custom task called Rsvp is being invoked twice. MSBuild will iterate over all of the different items in the list referenced by %(Attendee.Identity) and pass each one in as a parameter in a separate invocation of the task. The .Identity reference refers to the value of the Include="" attribute of the item.

Default Targets

When Visual Studio builds, it iterates over the semicolon-delimited list of values in the DefaultTargets attribute of the <Project> node of the project file. So if you want your custom target to always execute whenever Visual Studio builds the project (or whenever the MSBuild shell command is executed pointing at your project without specifying the target name) then you might consider just putting your target in the DefaultTargets attribute by appending a semicolon and your target's name.

What Will You Do Now?

If you got this far and understand it all, you should now be able to open up a .csproj file or .vbproj file and fully understand exactly what is going on, and you might even consider getting a little creative with automating your build scripts with MSBuild. Good luck.

Discover and read more posts from Jon Davis
get started