In this second installment in the series, I’ll showcase the core concepts in MSBuild that will get you up and running quickly, with concrete examples to try out the various constructs.
If you have never tweaked your
.csprojs in any significant way (i.e. beyond adding
Condition here and there or tweaking a properly value), a quick overview of the
core concepts in MSBuild would be helpful before we move on to more advanced topics.
A build typically uses properties (i.e.
Configuration with a value of
and items (i.e. all
.cs files in a project) to produce some output (i.e. a
.nupkg, etc.) by processing then using tasks (i.e.
Copy a file, or
Csc to invoke the
C# compiler), which are grouped in targets (i.e.
Publish and so
A farily useful analogy with a programming language might be:
- properties > scalar (string) values
- items > arrays
- target > method declaration
- task > method invocation
Make sure you create an empty (say)
test.proj file in a new empty directory,
keep it open in an editor (i.e. VS Code or VS 2017+) and also keep a developer
command prompt opened in the same directory as we go along. That’s key to
learning this thing ;).
I suggest you use the following MSBuild switches on every
msbuild run from
/nologo: avoids rendering the MBuild version and copyright information, unnecessary in order to learn to learn MSBuild ;)
/v:m: short form of
/verbosity:minimalso that you only see output we render explicitly and nothing more. Useful when learning to cut down the noise
Optionally, for the more advanced scenarios and to understand more of the inner workings and the MSBuild evaluation:
/bl: automatically generates an
msbuild.binlogfile for advanced inspection of the build process. Can be opened with the MSBuild Structured Log Viewer on Windows
Pro Tip: except for the core MSBuild XML elements, it’s important to remember that pretty much everything is case-insensitive in MSBuild, even target, property, item and task names and even task parameters!
Target to execute, a project cannot do anything. There is nothing to invoke. All a target needs at a minimum is a name, like:
<Project> <Target Name="Test" /> </Project>
If you run
msbuild /nologo /v:m you will esentially get an empty output in the console.
Even if such a target seems really pointless right now, there are use cases where it’s
actually useful, which we’ll learn in a future post.
To compare the output, running just
msbuild would get you something like:
Microsoft (R) Build Engine version 15.3.409.57025 for .NET Framework Copyright (C) Microsoft Corporation. All rights reserved. Build started 9/22/2017 12:42:49 AM. Build succeeded. 0 Warning(s) 0 Error(s) Time Elapsed 00:00:00.32
Build started 9/22/2017 12:43:22 AM. Build succeeded. 0 Warning(s) 0 Error(s) Time Elapsed 00:00:00.32
From now on I’ll omit
/v:m but remember to add them always to get the
NOTE: unlike Tasks, Targets cannot receive arguments, so they are more like parameterless void methods or entry points (i.e.
If your project has more than one target, you can specify which one to run with
msbuild /t:TARGET, such as
msbuild /t:test (note that the target name is case-insensitve
too). You can also specify that more than one target should run in sequence, like
Our empty target would essentially be the equivalent of an empty-bodied method. It needs a body now to do something, which is typically made of tasks.
As mentioned, you can think of a task as a (optionally parameterized) method invocation. There are many built-in tasks for common operations (like copying, reading, writing and deleting files, executing external tools and so on), but you can also author and consume custom tasks.
One very useful task while learning is
which receives just two arguments:
Importance and renders that to the output:
<Project> <Target Name="Test"> <Message Text="Hello World!" Importance="high" /> </Target> </Project>
If you run
msbuild you will now get the expected message in the output window:
C:\source\test>msbuild test.proj /nologo /v:m Hello World! C:\source\test>
Note that since almost everything is case-insensitive in MSBuild, you could also have written that as:
<Project> <Target Name="Test"> <message text="Hello World!" importance="High" /> </Target> </Project>
But intellisense for the built-in tasks will guide you towards the
PascalCase style typically.
Now let’s say you want to allow the build to configure the message to render in that task. That’s what you’d use a Property for.
As mentioned, a property contains a scalar (string) value, and is declared within a
<PropertyGroup> enclosing element, such as:
<Project> <PropertyGroup> <Message>Hello World!</Message> </PropertyGroup> </Project>
Properties are referenced using the
$ sign and enclosed in parenthesis, like
Let’s replace the
Text parameter in the task with the property:
<Project> <PropertyGroup> <Message>Hello World!</Message> </PropertyGroup> <Target Name="Test"> <Message Text="$(Message)" Importance="high" /> </Target> </Project>
msbuild will yield the exact same output as before, since we haven’t changed the contents
of the message.
Note that there is no ambiguity nor conflict between a Task named
Message and a Property named
the same, since the XML element is used in different contexts: the former can only be used as a
direct child of
<Target> and the latter as a child of
<PropertyGroup>. Referencing the property
always happens either inside an attribute or inside the content of another property, and is always
You can also mix constant strings with property references, such as:
<Project> <PropertyGroup> <Name>kzu</Name> </PropertyGroup> <Target Name="Test"> <Message Text="Hello $(Name)!" Importance="high" /> </Target> </Project>
C:\source\test>msbuild test.proj /nologo /v:m Hello kzu!
Finally, you can also reference properties when declaring other properties:
<Project> <PropertyGroup> <Name>kzu</Name> <message>Hello $(name)!</message> </PropertyGroup> <Target Name="Test"> <Message Text="$(Message)" Importance="high" /> </Target> </Project>
(which would render the same message as the previous example)
Note that the property names are case-insensitive, and although you could use such a confusing mixed casing, I don’t recommend doing so for obvious readability reasons ;).
Properties are fully mutable at any point during execution, and they are evaluated in document order. If you reference a property before it is declared, or a non-existent property altogether, you just get an empty string. There is simply no way to distinguish between an undeclared property and one that has an empty value.
You can also specify a value for a property via the command line, like
It might be a bit confusing to see that message since we are clearly setting the value of the property
to something else in the project. The reason for this is that by default, any MSBuild property can be overriden with the command line
/p switch, even if it’s assigned unconditionally in the project.
In order to avoid this default behavior and allow the project-determined value to prevail, you can
add the following attribute to the
<Project TreatAsLocalProperty="Name"> ...
This signals to MSBuild that the project can modify the value of the property locally. If you run
msbuild /p:name=Daniel, you will see that the message is
Hello kzu! this time around.
In addition, environment variables are automatically exposed as properties, such as
Basically everything you can
echo %VARNAME% in the console, you can reference in MSBuild with
Pro Tip: somewhat confusingly, environment variables are always overridable by the MSBuild project, without needing the
In the above example, we could replace the hardcoded
kzu with the corresponding envvar instead:
<Project> <PropertyGroup> <Name>$(USERNAME)</Name> <Message>Hello $(Name)!</Message> </PropertyGroup> <Target Name="Test"> <Message Text="$(Message)" Importance="High" /> </Target> </Project>
Say we want to allow more than one name above. We’d use an item group instead:
<Project> <ItemGroup> <Name Include="kzu" /> <Name Include="daniel" /> </ItemGroup> </Project>
Items are added to the “array”
Name (the element name within the item group becomes the
name of the array, so to speak) by “including” them via the
The simplest way to reference the items is with
<Target Name="Test"> <Message Text="Hello @(Name)" Importance="High" /> </Target>
Text attribute/parameter for the
is a simple string, and therefore an
automatic conversion from the array
@() to a string scalar is performed by concatenating
the items with the default separator
;. In this case, it would look better if we changed
the separator to a
, instead, which can be done as follows:
<Target Name="Test"> <Message Text="Hello @(Name, ', ')." Importance="High" /> </Target>
Which would render on build like:
Hello kzu, daniel.
If we wanted to render a full message for each name, we’d need to
foreach on the items.
This can be done by using the
%() construct instead. Update the target as follows:
<Target Name="Test"> <Message Text="Hello %(Name.Identity)." Importance="High" /> </Target>
Which would render on build like:
Hello kzu. Hello daniel.
This is called batching and is a key concept we will explore further in future posts because it’s key to several advanced scenarios including incremental builds.
Note that we can’t just do
Identity is a built-in item metadata which is
the value of the
Include element in the
<Name> item declarations. Items can also have
custom metadata, such as:
<Project> <ItemGroup> <Name Include="kzu" FullName="Daniel Cazzulino" /> <Name Include="daniel" FullName="ditto ;)" /> </ItemGroup> </Project>
We could reference the new custom metadata just like the built-in metadata:
<Target Name="Test"> <Message Text="Hello %(Name.FullName)." Importance="High" /> </Target>
Conditionally changing the outcome of the build is also a core characteristic in most cases,
such as building differently for
Release builds. MSBuild has a consistent and
general mechanism for conditionally evaluating all the core elements discussed so far, by
simply appending a
Condition attribute on them. A condition attribute must evaluate to
a string that can be parsed as a boolean (in a case insensitive manner, as everything else,
such as ‘true’ or ‘False’);
Going back to our example of the overridable
Name property for the greeting message,
we might want to make the property overridable either via a command line
or via an environment variable. As mentioned, the first case is always covered even if we
assign a “default” hardcoded value within the project, but to account for the environment
variable override, we must check for an empty value, before assigning the “default”, since
otherwise we’d overwrite the envvar-provided value:
<PropertyGroup> <Name Condition="'$(Name)' == ''">kzu</Name> <Message>Hello $(Name)!</Message> </PropertyGroup>
That property can now be safely overriden by both environment variables and command line arguments.
The condition could be placed in the entire
PropertyGroup too, for the case where the
Message is overriden:
<PropertyGroup Condition="'$(Message)' == ''"> <Name Condition="'$(Name)' == ''">kzu</Name> <Message>Hello $(Name)!</Message> </PropertyGroup>
Conditions on targets skip the entire target’s execution:
<Project> <Target Name="Test" Condition="'$(RunTest)' == 'true'"> <Message Text="$(Message)" Importance="High" /> </Target> </Project>
Individual tasks can also be conditioned exactly the same way:
<Project> <Target Name="Test"> <Message Condition="'$(Greet)' == 'true'" Text="$(Message)" Importance="High" /> </Target> </Project>
That’s basically it for the very basic concepts that you can use to start creating build scripts, IMO. You can read more at the official MSBuild Concepts documentation.