/ MSBuild

MSBuild Part 4: Check Your Tasks

Welcome back to this introduction to MSBuild, this time we are going to learn more about custom tasks. We will see how we can implement our own custom task both in code and declarative. We will also learn how to use custom task in our build script. So let us get started!

This post is part of the following series:

Implement our first Custom Task

Start by open Visual Studio and select File->New->Project or us the shortcut Ctrl+Shift+N to view the new project dialog. Create a Class Library with the following options:

  • Name: Construction.MSBuild.Tasks
  • Location: C:\MSBuildDemo
  • Solution name: Construction

Then click on OK to create the project.

MSBuild Custom Tasks 1
In the Solution Explorer right click on the Class1.cs and select Rename. Give the file the name Build.cs. Still in the solution explorer, right click on References and select Add Reference....

In the reference manager make sure that Assemblies are selected and then search for Microsoft.Build.Framework. Select the newest version and click OK to add the reference to your project.
MSBuild Custom Tasks 2

Thats it, we are now ready to start typing some code! Open the Build.cs file. To create a custom build task you have to at least implement the ITask interface. Your task must have a method named Execute. This is the method that is called when the task runs. The method takes no parameter and return a boolean, true if the task succeeded or false if it failed.

using Microsoft.Build.Framework;
using System;

namespace Construction.MSBuild.Tasks
{
    public class Build : ITask
    {
        public bool Execute() {
            return true;
        }
    }
}

Because the method takes no parameters and returns a boolean we have to control input and outputs in another way. We use properties to define both inputs and our output. The only difference between the inputs and the output is we have to decorate the output property with the [Output] attribute. We can also add the [Required] attribute to inputs that are required for our task to run.

using Microsoft.Build.Framework;
using System;

namespace Construction.MSBuild.Tasks
{
    public class Build : ITask
    {
        [Required]
        public string Type { get; set; }

        [Output]
        public string Result { get; set; }

public bool Execute()
        {
            if (Type.Equals("foundation", 
                StringComparison.InvariantCultureIgnoreCase))
            {
                Result = "Foundation for construction done";
            }
            else if (Type.Equals("walls", 
                StringComparison.InvariantCultureIgnoreCase))
            {
                Result = "Walls for construction done";
            }
            else if (Type.Equals("roof", 
                StringComparison.InvariantCultureIgnoreCase))
            {
                Result = "Roof for construction done";
            }
            else
            {
                Result = string.Format(
                    "Can not build anything of type: {0}",
                    Type);
            }

            return true;
        }

Finally we need to expose the two properties, BuildEngine of type IBuildEngine and HostObject of type ITaskHost. We can use the BuildEngine property to report messages, warnings and errors. ITaskHost is an interface that is used to represent host objects that can form a basis for richer communication between tasks and an environment that hosts MSBuild (such as Visual Studio). Let us finish our code by exposing these properties and add some logging to our task by using the BuildEngine method LogMessageEvent. When you are done make sure the code builds.

using Microsoft.Build.Framework;
using System;

namespace Construction.MSBuild.Tasks
{
    public class Build : ITask
    {
        [Required]
        public string Type { get; set; }

        [Output]
        public string Result { get; set; }

        public IBuildEngine BuildEngine { get; set; }

        public ITaskHost HostObject { get; set; }

        public bool Execute()
        {
            var startMessage = new BuildMessageEventArgs(
                string.Format("Building {0}...", Type),
                "Build",
                "Build",
                MessageImportance.High);

            BuildEngine.LogMessageEvent(startMessage);

            if (Type.Equals("foundation", 
                StringComparison.InvariantCultureIgnoreCase))
            {
                Result = "Foundation for construction done";
            }
            else if (Type.Equals("walls", 
                StringComparison.InvariantCultureIgnoreCase))
            {
                Result = "Walls for construction done";
            }
            else if (Type.Equals("roof", 
                StringComparison.InvariantCultureIgnoreCase))
            {
                Result = "Roof for construction done";
            }
            else
            {
                Result = string.Format(
                    "Can not build anything of type: {0}",
                    Type);
            }

            var endMessage = new BuildMessageEventArgs(
                "Build Task Finished",
                "Build",
                "Build",
                MessageImportance.High);

            BuildEngine.LogMessageEvent(endMessage);

            return true;
        }
    }
}

Using the custom task

Let us try to use our custom task in our build script. To use custom task in MSBuild scripts we need add a reference that points to the assembly of the custom task. Usually, the references are stored in a MSBuild project file with the extension .tasks. The reference our custom task we use the UsingTask node as follows:

<?xml version="1.0" encoding="utf-8"?>  
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
    <UsingTask AssemblyFile=".\Construction\Construction.MSBuild.Tasks\bin\Debug\Construction.MSBuild.Tasks.dll" TaskName="Build" />   
</Project>

Save the file as construction.tasks and then import it to the construction.proj file:

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

    <Import Project="common.props" />
    <Import Project="common.targets" />
    <Import Project="construction.tasks" />
    ...

Then update the Walls target to use our Build task:

...
    <Target Name="Walls" Condition="$(FoundationIsReady)">
        <Build Type="ConcreteWalls">
            <Output TaskParameter="Result" PropertyName="Result" />
        </Build>
        <Message Text="$(Result)" />
    </Target>
...

And also update the Foundationtarget in the common.targets file:

...
    <Target Name="Foundation" BeforeTargets="Walls">
        <Build Type="Foundation">
            <Output TaskParameter="Result" PropertyName="Result" />
        </Build>
        <Message Text="$(Result)" />
    </Target>
...

Let us run our custom task! You now the drill :)

>msbuild constuction.proj /target:Walls

MSBuild Custom Tasks 3

Implement the Task base class

There is another way to implement a custom task, and that is to use an abstract base class called Task. This class provides a little functionality for us so it is little simpler then to implement the ITask interface.

Let us convert the task we created earlier by inheriting the Task base class. First we need to load another reference into our project. In the solution explorer right click on references and select Add reference..., then search for Microsoft.Build.Utilities and select one of the assemblies (I am using version 14.0.0.0). After that we can replace the ITask interface with the Task base class and remove the BuildEngine and HostObject properties.

The base class also provides some convenient methods that can make our life easier. One of those is the Log.LogMessage method . So let us replace our logging messages to use that one instead.

using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
using System;

namespace Construction.MSBuild.Tasks
{
    public class Build : Task
    {
        [Required]
        public string Type { get; set; }

        [Output]
        public string Result { get; set; }

        public override bool Execute()
        {
            Log.LogMessage(MessageImportance.High, string.Format("Building {0}...", Type), null);

            if (Type.Equals("foundation", StringComparison.InvariantCultureIgnoreCase))
            {
                Result = "Foundation for construction done";
            }
            else if (Type.Equals("walls", StringComparison.InvariantCultureIgnoreCase))
            {
                Result = "Walls for construction done";
            }
            else if (Type.Equals("roof", StringComparison.InvariantCultureIgnoreCase))
            {
                Result = "Roof for construction done";
            }
            else
            {
                Result = string.Format("Can not build anything of type: {0}", Type);
            }

            Log.LogMessage(MessageImportance.High, "Build Task Finished", null);

            return true;
        }
    }
}

That is a little bit cleaner, to verify that it work you can can run your script again as we did before.

Implement inline custom tasks

There is actually one more way we can create custom tasks, and that is by declaratively describing our task. Let us see if we can declaratively implement our task we created earlier. Open the construction.tasks file and replace the UsingTask with the following code:

<UsingTask 
    TaskName="Build"
    TaskFactory="CodeTaskFactory"
AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.v$(MSBuildToolsVersion).dll">      
</UsingTask>

When creating a inline custom task we have to specify something called TaskFactory. A task factory specifies a factory that will create tasks dynamically from some language. The task factory CodeTaskFactory that is located in the assembly file we have specified, creates tasks dynamically written in C# or Visual Basic. We could however use another or create our own custom task factory that could for instance dynamically create task from Python or Java.

In our custom task we created before we exposed both input and output properties. When we create an inline task we declare them as follows inside the UsingTask:

<UsingTask 
    TaskName="Build"
    TaskFactory="CodeTaskFactory"
    AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.Core.dll">
    
    <ParameterGroup>
        <Type ParameterType="System.String" Required="true" />
        <Result ParameterType="System.String" Output="true" />
    </ParameterGroup>
    
    <Task>
        <Code Type="Fragment" Language="cs">
            Log.LogMessage(MessageImportance.High, string.Format("Building {0}...", Type), null);
            if (Type.Equals("foundation", StringComparison.InvariantCultureIgnoreCase))
            {
                Result = "Foundation for construction done";
            }
            else if (Type.Equals("walls", StringComparison.InvariantCultureIgnoreCase))
            {
                Result = "Walls for construction done";
            }
            else if (Type.Equals("roof", StringComparison.InvariantCultureIgnoreCase))
            {
                Result = "Roof for construction done";
            }
            else
            {
                Result = string.Format("Can not build anything of type: {0}", Type);
            }
            Log.LogMessage(MessageImportance.High, "Build Task Finished", null);
        </Code>
    </Task>
</UsingTask>

We specify the code type to Fragment so we only have to write the code that we usually would write inside the execute method. There is also the types, Class and Method the definition for all three types are:

  • If the value of Type is Class, then the Code element contains code for a class that derives from the ITask interface.

  • If the value of Type is Method, then the code defines an override of the Execute method of the ITask interface.

  • If the value of Type is Fragment, then the code defines the contents of the Execute method, but not the signature or the return statement.

We also specify that the code should be in C# with the attribute language.

You can verify that the script still works by running it again as before.

A wheel already exists

Great we now have three ways to create all the tasks we can think about. Stop! Before you do that, we should check so that the task we need does not already exists. MSBuild have several useful tasks included that you can use, so check them out first at the following site. If that is not enough a third party library called MSBuild Extension Pack also exists. The extension pack comes with a .tasks file that you can import just as we did we our custom task.

MSBuild Part 4: Check Your Tasks
Share this