Learn MSBuild - Part 2 - Variables

MSBuild is a domain-specific language, tailored to customizing how a project is built and compiled. It shares concepts with any general-purpose language, and in this post we’ll explore how MSBuild handles variables.

MSBuild has two variable types: Properties (i.e. strings) and Items (i.e. object arrays).

Properties (i.e. strings)

A string variable is declared by locating an XML node inside PropertyGroup XML node.

<PropertyGroup>
    <CoolProperty>wow! this is so cool</CoolProperty>
</PropertyGroup>

In many ways, MSBuild behaves similarly to a general-purpose language. Here would be the equivalent C#:

var coolProperty = "wow! this is so cool!";

Properties can be reassigned at any time.

<PropertyGroup>
    <Direction>Go left</Direction>
    <Direction>Sorry! Right!</Direction>
</PropertyGroup>
var direction = "Go left";
direction = "Sorry! Right!";

Tip: Whitespace matters!

Any and all whitespace inside the XML node is respected.

<IsFoo>
    true
</Foo>
var isFoo = @"
    true
";

You can access the value of a property (using $) to define other strings (i.e. string templating).

<Name>Lizzy</Name>
<Greeting>Hello $(Name)!</Greeting> <!-- Hello Lizzy! -->
var name = "Lizzy";
var greeting = $"Hello {name}!"; // Hello Lizzy!

Properties can also be defined conditionally using the Condition attribute.

<UsesNpm Condition="Exists('package.json')">true</UsesNpm>
var usesNpm = File.Exists(Path.Combine(Environment.CurrentDirectory, "package.json"))
    ? true
    : string.Empty;

Note: Learn more about the available Condition operators in the official documentation

Items (i.e. object arrays)

Am Item is defined by locating an XML node inside ItemGroup XML node.

<ItemGroup>
    <!-- single item -->
    <FavoriteThings Include="Raindrops on roses" />
    <!-- multiple items -->
    <FavoriteThings Include="Whiskers on kittens; bright copper kettles" />
<ItemGroup>
var favoriteThings = new[] { "Raindrops on roses" };
favoriteThings = favoriteThings
    .Append("Whiskers on kittens")
    .Append("bright copper kettles");

You can access the value of a Item (using @).

<ItemGroup>
    <CoolFiles Include="A.txt; B.txt">
<ItemGroup>

<Message Text="@(CoolFiles)" /> <!-- A.txt;B.txt -->
var coolFiles = new[] { "A.txt", "B.txt" };
Console.WriteLine(string.Join(";", coolFiles)); // "A.txt;B.txt"

MSBuild is designed to make working with files very easy. You can use wildcards to filter all the files included in the project.

<NonMinifiedFiles Include="*.js" Remove="*.min.js">
var nonMinifiedFiles = Directory.EnumerateFiles(Environment.CurrentDirectory, "*.js", SearchOption.AllDirectories)
    .Where(f => !f.EndsWith(".min.js"));

Items can be arrays of more complex objects. The child XML nodes of an Item are called “metadata” and can be accessed using the % operator.

<ItemGroup>
    <Stuff Include="Hide.cs" >
        <Display>false</Display>
    </Stuff>   
    <Stuff Include="Display.cs">
        <Display>true</Display>
    </Stuff>
</ItemGroup>
<Message Text="@(Stuff)" Condition=" '%(Display)' == 'true' "/> <!-- Display.cs -->
var stuff = new[] { new { Name = "Hide.cs", Display = false }, new { Name = "Display.cs", Display = true };
Console.WriteLine(string.Join(";", stuff.Where(s => s.Display))); // "Display.cs"

Summary

MSBuild has the same primitives as general-purpose languages like C#.

In this post, we covered its support for variables. In the next post, we’ll cover its support for functions!

Appendix: A quick note on variable names

A source of MSBuild consternation is the language’s lack of variable scoping. There are no “private” variables, so you can overwrite variables defined by any MSBuild target, including the “standard library” that facilitates the compilation of your project’s C#!

To avoid collisions, I recommend “namespacing” custom variables:

<!-- Very likely to collide! -->
<IsWebProject>...</IsWebProject>

<!-- Much better! -->
<MyOrganizationName_IsWebProject>...<MyOrganizationName_IsWebProject>

<!-- Even better! -->
<MyNuGetPackageName_IsWebProject>...<MyNuGetPackageName_IsWebProject>

This “feature” of language does have perks. It makes exposing a public API for disabling a build feature dead simple:

<!-- From a NuGet package the minifies your code, provide a default that can be overridden -->
<MinifyJavaScriptFiles Condition="'$(GenerateMinifiedSourceMaps)' ==''">true</MinifyJavaScriptFiles>

<!-- In a consuming project, disable minification in local dev for better performance -->
<MinifyJavaScriptFiles Condition="$(IsLocalDev)">false</MinifyJavaScriptFiles>

Tip: Avoid naming collisions with the standard library!

Here is the documentation on built-in Property and Item names:

Updated: