Learn MSBuild - Part 3 - Functions
MSBuild is a domain-specific language, tailored to customizing how a .NET project is built. It shares concepts with any general-purpose language, and in this post we’ll explore how MSBuild handles functions.
MSBuild has a concept of Tasks
(i.e. functions) that are executed within Targets
(i.e. event handlers).
Tasks
(i.e. functions)
A function is executed by locating an XML node inside a Target
XML node (more on these later).
<Target>
<Error Text="Something went wrong!" />
</Target>
Here would be the equivalent C#:
throw new Exception("Something went wrong!")
There are dozens of built-in tasks. Most tasks make common I/O operations easy:
<Delete Files="@(MyFiles)" />
foreach (var fileName in myFiles)
{
File.Delete(fileName);
}
<WriteLinesToFile
File="$(CacheFile)"
Lines="$([System.DateTime]::Now)"
Overwrite="true" />
File.WriteAllText(
cacheFile,
DateTime.Now)
There’s even a task called Exec
that allows you to run arbitrary console commands!
<Exec Command="dir" WorkingDirectory="$(MSBuildProjectDirectory)" />
Command.Run("dir", o => o
.WorkingDirectory(projectDirectory));
Note: The above C# example uses MedallionShell
You can also define your own task by implementing the ITask
interface. This is a must-know trick when writing complex MSBuild logic. Custom tasks can utilize NuGet packages and are far easier to unit test. The official docs are very good on this.
Here’s an example of how a custom task for minifying code would be invoked:
<UsingTask TaskName="Minify" AssemblyFile=".\bin\Debug\My.Minification.Project.MSBuild.dll" />
<Target Name="MinifyJavaScriptFiles" BeforeTargets="AfterBuild">
<Info Text="Minifying files...">
<Minify SourceFiles="@(NonMinifiedJavaScriptFiles)" />
</Target>
Targets
(i.e. event handlers)
MSBuild has an additional concept Targets
that allows you to “schedule” your tasks. It is easy to think of these like event handlers. Targets are run at a specific point in the lifecycle of a build.
<Target Name="MyTarget" BeforeTargets="BeforeBuild">
<Error Text="Fail the build!" />
</Target>
c.BeforeBuild += MyTarget;
...
static void MyTarget(object sender, BeforeBuildEventArgs e)
{
throw new Exception("Fail the build!")
}
The most common “events” that you may want your code to handle are:
BeforeBuild
. Before compilation (the creation of.dlls
, etc.) starts. It is best to handle this event if your code is doing validation or preprocessing required by the build.AfterBuild
. After compilation has completed. This event is best for code doing post-processing or validation on output file (e.g. minification). NOTE: Unintuitively, a target handlingAfterBuild
is still capable of “failing” the build.Clean
. When aClean
is requested by the user. It is a best practice to “clean up” after yourself.
MSBuild follows is designed to support “incremental” builds, e.g. only recompiling, re-restoring NuGet packages, etc. if the changeset since the last build requires it.
To implement “incremental builds” Targets
support file timestamp-based caching out of the box:
- If an
Inputs
attribute is specified, theTarget
will only execute if the “last modified” timestamp of any of files is later than the last execution. - If the
Target
does execute,Inputs
will be edited to only contain items that have been modified since the last execution.
<ItemGroup>
<JavaScriptFiles Include="wwwroot/*.js" Exclude="wwwroot/*.min.js"/>
</ItemGroup>
<Target Name="MinifyJavaScriptFiles" BeforeTargets="AfterBuild" Inputs="$(JavaScriptFiles)">
... minify files ...
</Target>
public void MinifyJavaScriptFiles(javaScriptFiles)
{
if (HaveAnyTimestampsBeenUpdatedSinceLastExecution(javaScriptFiles))
{
return;
}
... minify files ...
}
Summary
MSBuild has the same primitives as general-purpose languages like C#.
In this post, we covered its support for functions and event handlers. In the next post, we’ll take a look at a real-world example to reinforce what we’ve learned.
Appendix: A quick note on importing Targets
There are three ways to define / import a Target
into your .csproj:
- Define the
Target
in the .csproj itself. This is easy and makes sense for short, adhocTargets
written to address quirks in your build. - Define a
Target
in a.targets
file in thebuild/
folder of aNuGet
package. This is the most reusable and a very valuable skill to be able to deploy if the situation calls for it. Import
theTarget
from a.targets
file. This makes organization simple since you can group relatedTargets
andProperties
in the same files together.
<Import Project="FileWithProperties.props" />
<Import Project="FileWithTargets.targets" />