While MSBuild is all about build customization,we never really blogged about what is involved in implementing custom tasks.  Partly, I suppose that's because implementing a custom task is as easy as falling off a log most of the time: 1) Subclass Microsoft.Build.Utilities.Task abstract class 2) Implement the Execute method

But there's a lot more to tasks than just that. In this series of posts, I hope to cover all there is to know about MSBuild tasks - both at a conceptual level as well as on variations such as ToolTask, AppDomainIsolatedTask, etc.  In this post I'd like to set the context for discussing some of those topics.

A task, in principle is nothing more than an implementation of the GoF Command Object Pattern i.e. an object that knows how to perform an action encapsulated via an Execute method. While it is possible to utilize tasks programmatically via code, the primary motivation was to provide a way of performing self standing units of work within build targets.

In the crudest form, a task is a type that implements the Microsoft.Build.Framework.ITask interface from the Microsoft.Build.Framework assembly.  Here's a simple implementation of a task that can set environment variables.

using Microsoft.Build.Framework;

namespace SimpleTask
{
    public class SetEnvironmentVariable : ITask
    {
        private IBuildEngine engine;              
        public IBuildEngine BuildEngine
        {
            get { return engine; }
            set { engine = value; }
        }       

        private ITaskHost host;
        public ITaskHost HostObject
        {
            get { return host; }
            set { host = value; }
        }

        private string name;
       
        [Required]
        public string Name
        {
            get { return name; }
            set { name = value; }
        }

        private string varValue;

        [Required]
        public string Value
        {
            get { return varValue; }
            set { varValue = value; }
        }

        public bool Execute()
        {                       
            System.Environment.SetEnvironmentVariable(name, varValue);
            string message = string.Format("Environment Variable {0} set to {1}", name, varValue);
            BuildMessageEventArgs args = new BuildMessageEventArgs(
                message, string.Empty, "SetEnvironmentVariable", MessageImportance.Normal);
            engine.LogMessageEvent(args);
          
            return true;
        }
    }
}

In addition to the Execute method that performs the actual work, at the minimum all tasks must expose two properties - BuildEngine of type IBuildEngine, and HostObject of type ITaskHost. For the purposes of most tasks, the implementation I have shown here will suffice.

IBuildEngine itself is used by the task to report messages, warnings and errors to the MSBuild engine.  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).  ITaskHost is extremely useful in cases where you are hosting the build engine yourself.  I will cover the details of this beast in a later post.

.NET properties on a task type allow you define parameters on the task in order to communicate with the task via the project file.  So, in the case of the SetEnvironmentVariable task, I have defined Name and Value properties as inputs into the task. My use of the Required attribute on these two properties ensures that the invocation of the tasks will not be possible without specifying those two parameters. In this context, it makes sense because the task cannot perform its function without having those to values specified to it.

Finally, returning true from the Execute method indicates success to the MSBuild engine. I have ignored the possibility of the task failing in this case to keep it simple - look for more details on this subject alone in a subsequent post.

Here's how the task is invoked from a project file:

<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
   <UsingTask AssemblyFile="SimpleTasks.dll" TaskName="SimpleTask.SetEnvironmentVariable" />
  
   <PropertyGroup>
      <OutputPath>c:\temp</OutputPath>
   </PropertyGroup>

   <Target Name="MyTarget">
      <SetEnvironmentVariable Name="OutputPath" value="$(OutputPath)" />
   </Target>
</Project>

That's all there is to defining and using custom tasks. Of course, it is way simpler to derive from the abstract class Microsoft.Build.Utilities.Task that is included in the Microsoft.Build.Utilities assembly, and that's what you should be using most of the time unless you require a richer interaction with the MSBuild engine.

In the next post, I will dig into more details on interacting with tasks via the project file. We will examine the types of objects that can be passed in and out of tasks, caveats to watch out for, etc.

[ Author: Faisal Mohamood ]