Inside MSBuild
Compile Apps Your Way With Custom Tasks For The Microsoft Build Engine
Sayed Ibrahim Hashimi
This article discusses:
- Fundamentals of MSBuild
- Targets, items, transformations, and tasks
- Extending the build process
- Implementing custom tasks
|
This article uses the following technologies:
Visual Studio 2005
|

Contents
I
n previous versions of Visual Studio, the build process was mostly a black box, and there were few things that you could do to customize your build process. With the release of Visual Studio® 2005 and the Microsoft® .NET Framework 2.0, your managed projects can be built using the Microsoft Build Engine (MSBuild). MSBuild is extensible and allows you to customize each step taken during the course of a build. It uses an XML file (actually the project file, but more on that later) to describe each step, allowing you to easily change and augment how your projects are built.
In this article I will introduce you to MSBuild and show you how to use it to customize your builds. I will demonstrate how to use MSBuild from the command line and show you how you can replicate the same process that is used when your projects are built from the Visual Studio IDE. It's also important to note that MSBuild ships as a part of the .NET Framework, so you don't need Visual Studio installed on a machine in order to have it build your projects. The files supporting MSBuild that are shipped with Visual Studio 2005 only support C#, Visual Basic®, and J# projects; however, MSBuild as a platform can support any language.
MSBuild Fundamentals
The first thing you should know about MSBuild is that your project file is your XML build file. This is important for a few reasons. First, it ensures that the build produced by Visual Studio is repeatable outside of Visual Studio. Second, you get a default build file when you create a Visual Studio project. Let's have a look at the project file for a C# application. For this example I created a new C# Windows®-based application project. When the project is open, right-click on it in the Solution Explorer and select Unload Project. Then right-click on it again and select Edit. Figure 1 shows an excerpt of this project file. From this snippet you can see that various project properties are defined. For example, you can see the Platform and the AssemblyName of the project.

Figure 1 Application Project in C#
|
<Project DefaultTargets="Build"
xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">
Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProductVersion>8.0.50727</ProductVersion>
<SchemaVersion>2.0</SchemaVersion>
<ProjectGuid>{2763BAAA-B9EC-4D3F-8992-11A99E669682}</ProjectGuid>
<OutputType>WinExe</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>WindowsApplication1</RootNamespace>
<AssemblyName>WindowsApplication1</AssemblyName>
</PropertyGroup>
...
</Project>
|
All of the declarations within the PropertyGroup element are called properties, which are simply name-value pairs. Most Visual Studio configuration settings are stored within properties, which must be contained in a PropertyGroup element. In the project file shown in
Figure 1 you'll find this declaration contained within a PropertyGroup element:
|
<AssemblyName>WindowsApplication1</AssemblyName>
|
In this example, AssemblyName is the name of the property and WindowsApplication1 is the value of this property. In order to access these values you use the $(PROPERTY_NAME) syntax. For example, for this AssemblyName property you would use $(AssemblyName) to access its value. Later you will see how using properties can make your life a lot easier and how the entire build process is actually tied together with properties.
Elements that will be executed by MSBuild must be contained in a target. When Visual Studio builds your project, it is actually executing an MSBuild target.
A target is simply a container for a set of related tasks that will be executed one after another. Many targets are shipped with MSBuild that pertain to building your projects, such as Compile, Build, Rebuild, and Publish. If you have a look at the WindowsApplication1 project file, you will not find a single target defined in that file. This is because all of the targets are imported from another set of files. I will discuss how this works later.
I'll show you how to execute some of the preexisting targets, then I'll offer a few examples of how to create some new targets. In order to execute MSBuild targets, open the Visual Studio 2005 Command Prompt. To find this tool, click Start | All Programs | Microsoft Visual Studio 2005 | Visual Studio Tools. After opening the command prompt window, navigate to the folder that contains the WindowsApplication1.csproj file. Now, to clean this project, use the following command:
|
msbuild.exe WindowsApplication1.csproj /t:Clean
|
In this example you're passing in two parameters: the project file to use and the target to execute. If all went well with cleaning the project, your results should look similar to Figure 2.
Figure 2 Cleaning a Project
You can also build a project by executing the Build target. The command for this is:
|
msbuild.exe WindowsApplication1.csproj /t:Build
|
Along with the project name and target, there are several other command-line parameters you can send to Msbuild.exe. For example, you can specify the detail of log output with the /verbosity (/v) parameter or you can specify properties using the /property (/p) parameter. For a complete list of available parameters and their usage, see
msdn2.microsoft.com/ms164311.aspx.
By having a look at the core msbuild schema document (located at %FrameworkDir%/MSBuild/Microsoft.Build.Core.xsd), you can see that the target element can have up to five attributes assigned to it—Name, Condition, DependsOnTargets, Inputs, and Outputs. The Name attribute is the reference to the target, as was shown previously. Each MSBuild element can have a Condition associated with it and, if this condition evaluates to false, then the entire element will be ignored. You may have noticed this from the Configuration property declaration shown earlier. Here is the relevant section:
|
<Configuration Condition=" '$(Configuration)' == '' ">
Debug</Configuration>
|
In this declaration, if the Configuration property is empty, it will be assigned a value of Debug. It can be useful to have the same target defined, but different implementations are based on various conditions.
The DependsOnTargets attribute is a semicolon-delimited list of targets that must execute before the declared target. Each of the targets will be examined for execution in the order that they are declared in the DependsOnTargets list. Keep in mind that a target is executed at most one time per MSBuild instance. A target always has an associated build state; the initial state is NotStarted. The other possible build states are InProgress, CompletedSuccessfully, CompletedUnsuccessfully, and Skipped. If a target has a state other than NotStarted, it will not be executed again. Remember this as you design your targets. Typically you'll want completed targets to be skipped, but there are some situations where this is not desirable.
The remaining target attributes are Inputs (files that the target will act upon) and Outputs (files that the target will generate). These lists play a critical role in incremental building, meaning building only the pieces of the application that are out of date.
During the course of a build within Visual Studio, you most likely have seen a message stating that a portion of the build was being skipped. This is incremental building. That portion of the build is being skipped because nothing has changed in that section since the last build. For example, if you have a solution containing many different projects, when you perform a build you'd like to only build projects that have changed, and projects that depend on other projects that have changed. It would be a waste of time to include unaffected projects for each build.
This incremental build concept is supported by the Inputs and Outputs. The idea is that you provide an Inputs list of files that the target will examine. Along with this, you provide an Outputs list of files that your target will create. If Inputs are older than Outputs, then the target will be skipped. As you create targets that perform a lot of work and are time consuming it will be critical to provide correct inputs and outputs so MSBuild can support incrementally building those targets as well.
Creating Targets
Now that you have the basics down, you know almost everything needed to create a simple MSBuild target. The only outstanding issue is tasks. I'll cover this in more depth in another section, but here's a brief description so you can start to build some targets. In MSBuild a task is simply a unit of work. There are many pre-defined tasks that are available for your use, such as copying files, signing files, invoking the C# compiler, and so on.
A task is executed within a target. When a target is being executed, each task contained inside of it will be invoked in the order in which it is declared. For example, take a look at this simple Hello target:
|
<Target Name="Hello">
<Message Text="Hello MSBuild" Importance="high"/>
</Target>
|
This target uses the Message task to send "Hello MSBuild" to the logger, in this case to be printed to the screen. If you place this target into the WindowsApplication1 project file, you can verify that it works correctly. You can achieve this by editing the WindowsApplication1 project from within Visual Studio, and you'll have IntelliSense® to help you.
To inject this target into the project file, I placed it immediately above the closing tag for the Project element. As long as it is an immediate child of the Project element, the location doesn't matter in this case. Verify that this does indeed work by invoking the Hello target on the WindowsApplication1 project file. To do this, execute the following command:
|
msbuild WindowsApplication1.csproj /t:Hello
|
You should see a result like this:
|
Target Hello:
Hello MSBuild
|
Items and Transformations
Since builds are heavily reliant on files, it only makes sense to have a construct that makes it easier to work with files. This is the purpose of items. Although items are not limited to being used with files, that certainly is their designed target. An item is a reference to a file (existing or nonexisting) or a group of files. Items can gather sets of related files together and act on them. The item references are declared in the WindowsApplication1 project file like this:
|
<ItemGroup>
<Reference Include="System" />
<Reference Include="System.Data" />
<Reference Include="System.Deployment" />
<Reference Include="System.Drawing" />
<Reference Include="System.Windows.Forms" />
<Reference Include="System.Xml" />
</ItemGroup>
|
As you can see, items are contained in an ItemGroup XML element. An item can be decomposed into three parts: an Include list, an Exclude list, and metadata (see Figure 3).

Figure 3 Item Attributes
|
Attribute |
Description |
| Include |
A semicolon-delimited list of files to be included in the item. You can also use wildcards to identify the files. |
| Exclude |
A semicolon-delimited list of files to be excluded from insertion into the item. You can use wildcards on this attribute as well. |
| Metadata |
Data that is associated with the item. There are two kinds of metadata. Well-known metadata is available out of the box and includes such things as FileName and FullPath. Custom metadata is attached to an item in the build file. |
The Reference item has the following values associated with it: System, System.Data, System.Deployment, System.Drawing, System.Windows.Forms, and System.Xml. I could have achieved that exact same effect with this more concise declaration:
|
<Reference Include="System;System.Data;System.Deployment;
System.Drawing;System.Windows.Forms;System.Xml"/>
|
The code in
Figure 4 indicates that you can use wildcards to initialize items. There are three wildcard elements: ?, *, and **. The ? wildcard replaces a single character with any possible character:
|
<ItemGroup>
<MyItem Include="Fi?e.txt"/>
</ItemGroup>
|

Figure 4 Compile Declaration
|
<ItemGroup>
<Compile Include="Form1.cs">
<SubType>Form</SubType>
</Compile>
<Compile Include="Form1.Designer.cs">
<DependentUpon>Form1.cs</DependentUpon>
</Compile>
<Compile Include="Program.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<EmbeddedResource Include="Properties\Resources.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
<SubType>Designer</SubType>
</EmbeddedResource>
<Compile Include="Properties\Resources.Designer.cs">
<AutoGen>True</AutoGen>
<DependentUpon>Resources.resx</DependentUpon>
</Compile>
<Compile Include="Properties\Settings.Designer.cs">
<AutoGen>True</AutoGen>
<DependentUpon>Settings.settings</DependentUpon>
<DesignTimeSharedInput>True</DesignTimeSharedInput>
</Compile>
</ItemGroup>
|
In this declaration MyItem could include files like Fire.txt, File.txt, Fine.txt, and so on, but it would not include files like Fie.txt or FilingCabinet.txt. When MSBuild encounters this item declaration it will examine the current directory (because no path information was provided for the declaration) for files that match the expression. Any file matching this expression is included in the MyItem list.
The * wildcard in a declaration can be replaced with any combination of zero or more characters:
|
<ItemGroup>
<MyItem Include="Fi*e.txt"/>
</ItemGroup>
|
Now MyItem can include files with names that include Fie.txt, File.txt, Fiddle.txt, Fi5e.txt, and so on.
The ** wildcard allows you to tell MSBuild to recursively search all subdirectories, as well as the current directory, for items:
|
<ItemGroup>
<MyItem Include="InputFiles\**\Fi*e.txt"/>
</ItemGroup>
|
When MSBuild encounters this declaration, it will look in the InputFiles folder and each folder under it recursively for files that match the Fi*e.txt pattern. All such files will be included in the MyItem list.
Transformations can seem to be confusing at times, but they are a very important part of MSBuild. An item transformation is a one-to-one mapping between two item lists. For example, if you would like to copy files from one location to another, you will need to use a transformation, and you will use metadata as a part of that transformation.
There are two kinds of metadata: well-known and custom. Well-known metadata is available for all file items. Some examples of well-known metadata are Filename, FullPath, and Extension. These return the file name of the file, the folder path to the file, and the file name extension of the file, respectively. (For a full list of well-known metadata see
msdn2.microsoft.com/ms164313.aspx.) Custom metadata is associated with the item directly in the project file.
To illustrate using metadata and transformations, let's copy the files to be compiled to another location. First you need to know how to refer to these files. If you have a look at the WindowsApplication1 project file again, you'll see an item declaration named Compile. This is the set of files that will be sent to the C# compiler. The complete declaration for this item is shown in Figure 4.
Notice that this declaration is quite different from the declaration of References shown previously. This is because the Compile item associates custom metadata to these items. The custom metadata includes the XML elements contained within the items, such as the AutoGen elements, which tell Visual Studio that this is an auto-generated file.
Let's move these files to a folder named SourceCopy in the project directory. The project directory can be determined by using the reserved MSBuildProjectDirectory property. For a list of other reserved properties see
msdn2.microsoft.com/ms164309.aspx.
The syntax for a transformation is:
|
@(ItemName->'TransformationDetails')
|
ItemName is the name of the item you are transforming, and the TransformationDetails can contain raw text, evaluated properties, and metadata references. The CopyFiles target will copy the files to the location specified:
|
<Target Name="CopyFiles">
<!-- If the CopyLocation doesn't exist then create it -->
<MakeDir Directories="$(CopyLocation)"
Condition="!Exists('$(CopyLocation)')"/>
<!-- Now actually copy the files -->
<Copy SourceFiles="@(Compile->'%(FullPath)')"
DestinationFiles="@(Compile->
'$(MSBuildProjectDirectory)
\$(CopyLocation)\%(RelativeDir)\%(FileName)%(Extension)')"
/>
</Target>
|
The CopyFiles target first invokes the MakeDir task to create the directory, if it doesn't exist. This is followed by the Copy task that will copy the files to the specified location. (The Copy task is provided by MSBuild. You can get information about built-in MSBuild tasks at
msdn2.microsoft.com/7z253716.aspx.) Web Resources
This example passes in two parameters to the Copy task: the SourceFiles and the DestinationFiles. For the SourceFiles the task is extracting the FullPath of the Compile items. The DestinationFiles transformation specification is a little more daunting, so I'll break it down into its components. There are two properties that will be evaluated, $(MSBuildProjectDirectory) and $(CopyLocation). After this there are three pieces of metadata that help piece together the entire path. The metadata used is %(RelativeDir) for path information, %(FileName), and the %(Extension). If you put these together you end up with the path. It is important to note that this process will invoke the Copy task only one time.
Now you need to verify that the task works as intended by executing the CopyFiles target on the WindowsApplication1 project file. From the Visual Studio 2005 Command Prompt, go to the directory containing that project file and execute this command:
|
msbuild WindowsApplication1 /t:CopyFiles
|
The result from the execution of this target is shown in Figure 5.
Figure 5 CopyFiles Target Output
Let's also take a look at an example of how the Visual Studio project file provides MSBuild functionality right out of the box. Consider this scenario: you have many different projects located on your hard drive and you'd like to clean them all. At first glance you might consider simply deleting all the \bin and \obj directories, but you may inadvertently delete some important files. The ideal solution would be to have each project determine what is needed and what can be thrown out. MSBuild can help you do exactly that. Take a look at the very simple MSBuild project file, CleanAllProjects.proj:
|
<Project xmlns=http://schemas.microsoft.com/developer/msbuild/2003
DefaultTargets="CleanAllProjects">
<!-- Find all projects in or below the current directory -->
<ItemGroup>
<Projects Include="**\*.*proj" />
</ItemGroup>
<Target Name="CleanAllProjects">
<MSBuild Projects="@(Projects)" Targets="Clean"
StopOnFirstFailure="false" ContinueOnError="true">
</MSBuild>
</Target>
</Project>
|
This project has a single item, Projects, which contains all of the project files that are in or under the current directory. Along with this item there is the CleanAllProjects target. When this task is invoked it will call the Clean target on all of the project files contained in the Projects item. This is achieved by using the default MSBuild task, which allows you to create a new instance of MSBuild to process a project file. With this solution you can feel confident that no necessary files will be removed because you are letting the project make the determination. Before Visual Studio 2005 and MSBuild, this type of functionality was not available outside of a custom solution.
Tasks
In MSBuild a task is simply a unit of work that is to be performed. Each task is independent of every other task and is not natively aware of any other task. Examples of tasks are those that copy files, compile C# files, create directories, and so on. A task can have a set of parameters associated with it and can have parameters that it returns. These parameters are the means that a task communicates with the MSBuild project file that is invoking it.
MSBuild ships with several useful tasks. For a complete list, see the Microsoft.Common.tasks file located in the %WinDir%\Microsoft.NET\Framework\v2.0.50727 directory. Some frequently used tasks are summarized in Figure 6. For more specific information about how to use these and other tasks shipped with MSBuild see the MSDN® MSBuild Task Reference.

Figure 6 MSBuild Tasks
|
Task |
Description |
| CreateItem |
Creates items. Since items are evaluated at the beginning of the build, if you need files generated by your build included in an item then you must use this task. If the item that is being created already exists then it will be appended. |
| CreateProperty |
Creates a property. Properties are evaluated at the beginning of the build (except those created by CreateProperty). If the property that is being created exists, it will be overwritten. |
| Copy |
Copies files from one location to another. |
| Delete |
Deletes files. |
| Error |
Notifies MSBuild that an error has occurred. Usually this is used with a Condition attribute that means something went wrong. |
| Exec |
Invokes an executable file. |
| Message |
Passes a message to the MSBuild logger. |
| MSBuild |
Invokes MSBuild on another project file. You can also use the Exec task to start Msbuild.exe, but the MSBuild task has advantages such as getting the output of executed targets. |
Previously you saw an example of how to use the Message task to output a message to the console logger:
|
<Target Name="Hello">
<Message Text="Hello MSBuild" Importance="high"/>
</Target>
|
This task invocation passes two pieces of information to the Message task: Text and Importance. As you use tasks you'll quickly notice that each task has its own inputs and outputs. For example the Message task has an input named Text, but the Copy file does not. You can determine the correct inputs and outputs from the task documentation or the source code. The Message task, for example, does not create any outputs.
To demonstrate using a task output, I will use the CreateItem task. For the CreateItem task you need to specify the list of files to be included in the item created. The name of this input is Include. This is also the output that you will use to retrieve the newly generated item. In the WindowsApplication1 sample, I have added the following towards the end of the project file:
|
<Target Name="PrintOutputFiles" DependsOnTargets="Build">
<!-- First create an Item that contains these files.
$(OutputPath) is the property that contains the
locations where built files are placed. -->
<CreateItem Include="$(OutputPath)\**\*">
<Output TaskParameter="Include" ItemName="OutputFiles" />
</CreateItem>
<!-- Now send these values to the logger -->
<Message Text="@(OutputFiles->'%(Filename)%(Extension)')"
Importance="high"/>
</Target>
|
As you can see, the CreateItems task is invoked to create the Output Files item, which is then used in the Message element to print the values to the console. The OutputFiles item is available immediately after the CreateItem task completes, and it is available to all other targets as well, not just the one currently executing.
Extending the Build Processes
The entire build process used by MSBuild, and therefore by Visual Studio, is defined by your project file. When creating projects by using Visual Studio, a set of files is imported into your project file. Files are imported using the Import element. For C# projects, the Microsoft.CSharp.targets file is imported. This file contains specific information about how to build a C# project, such as defining the step to call the C# compiler (CSC). It then imports the Microsoft.Common.targets file. This file is responsible for defining the generic steps that are used during the course of all managed builds. Much of the build process is defined in this file.
From the Microsoft.Common.targets file, let's look at how the build target is defined:
|
<PropertyGroup>
<BuildDependsOn>
BeforeBuild;
CoreBuild;
AfterBuild
</BuildDependsOn>
</PropertyGroup>
<Target
Name="Build"
Condition=" '$(_InvalidConfigurationWarning)' != 'true' "
DependsOnTargets="$(BuildDependsOn)"
Outputs="$(TargetPath)"
/>
|
This is the target that will be invoked by default when you build your project using Visual Studio. You can see that this target itself does nothing. It relies completely on its dependent targets to do all of the work. This is specified using the DependsOnTargets list. As I mentioned previously, this is the list of targets that must be executed before the target gets invoked. In this case, this list is defined in the property BuildDependsOn. This makes it easier for you to customize the necessary steps to execute the target.
To show you just how this works, let's inject a step into the middle of a build process. Do this by modifying the CoreBuildDependsOn list (see Figure 7). This is the list of targets that the CoreBuild target uses. CoreBuildDependsOn is redefined in the WindowsApplication1 project file. Along with this redefinition, you place the target needed for your particular project.

Figure 7 CoreBuildDependsOn
|
<PropertyGroup>
<CoreBuildDependsOn>
BuildOnlySettings;
PrepareForBuild;
PreBuildEvent;
UnmanagedUnregistration;
ResolveReferences;
PrepareResources;
ResolveKeySource;
Compile;
MyCustomStep;
GenerateSerializationAssemblies;
CreateSatelliteAssemblies;
GenerateManifests;
GetTargetPath;
PrepareForRun;
UnmanagedRegistration;
IncrementalClean;
PostBuildEvent
</CoreBuildDependsOn>
</PropertyGroup>
<Target Name="MyCustomStep">
<Message Text="Inside of MyCustomStep target" Importance="high"/>
</Target>
|
The MyCustomStep target is placed into the dependency list after the following statement in the WindowsApplication1 project:
|
<Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
|
The BuildDependsOn property must be inserted after the Import statement because the Microsoft.Common.targets also defines this property. When properties are defined multiple times, the last definition is the one that is used. So if you placed this definition above the Import statement, it would simply be overridden.
The MyCustomStep passes a message to the MSBuild logger. You can verify that you've successfully injected the step by building the project. At the Visual Studio 2005 Command Prompt execute the following command:
|
msbuild WindowsApplication1.csproj /t:Build
|
The results from this are shown in Figure 8.
Figure 8 Build Output for MyCustomStep
There are other ways to inject your custom steps into the build and many targets are created for this sole purpose. For instance, the BeforeBuild and AfterBuild targets are empty by default and are designed to be overridden. If you need a quick and easy way to perform steps, you can override one of these targets and it will be called at the appropriate time. But there is a drawback to using this technique. If there is more than one place where the same target is overridden, only one of the overrides will be preserved. If you need to add steps before or after a target that has a DependsOn property, then the best option is to append to that list. For example, instead of overriding the BeforeBuild target, you can define a target and prepend it to the BuildDependsOn property:
|
<PropertyGroup>
<BuildDependsOn>
CustomBeforeBuild;
$(BuildDependsOn);
</BuildDependsOn>
</PropertyGroup>
<Target Name="CustomBeforeBuild">
<!-- Your steps here -->
</Target>
|
This redefinition uses the existing value for the BuildDependsOn property, accessed through $(BuildDependsOn), and simply adds to it. If this was performed multiple times then all of the modifications would exist and no target execution would be lost.
Extending MSBuild
MSBuild has two main ways to be extended: custom tasks and custom loggers. I will show how to write a custom task later in this article. An MSBuild logger, on the other hand, is the object responsible for taking the build event notifications and processing them. MSBuild ships with two loggers by default, the console logger and a file logger. You can use the Msbuild.exe command-line parameters to declare which logger to use. I don't have enough space here to delve into custom loggers but you can find information about them at
msdn2.microsoft.com/ms171470.aspx.
If you look at the Microsoft.Common.tasks file, you will see many tasks declared. You'll find a Copy task and a Delete task, but no Move task. Typically during a build you will perform copy operations and even delete files; however, you will not be required to move many files. Yet there are certain times when you will want this functionality, and it would be nice if there was a task to take care of this.
When creating a task, keep in mind that as with a good subroutine, you want to make it reusable. Because of this, a task should complete small steps. You don't want a task that is responsible for doing too many things. Also, you need to consider how you want this task to communicate with other components. This communication is facilitated through the task's inputs and outputs. Input and output types that are supported include strings and all other built-in types, plus Microsoft.Build.Framework.ITaskItems and arrays of these objects. ITaskItem is the underlying interface for the items used by MSBuild. So in your project file when you create an item you're actually creating one or more ITaskItems. If your task contains ITaskItems then you can access their metadata in a similar way as you would in the project file.
Every MSBuild task must implement the Microsoft.Build.Framework.ITask interface. ITask has two properties (BuildEngine and HostObject) and one method (Execute). The Execute method will be called to carry out the activity of the task at the appropriate time. This is a pretty simple interface and is easily implemented.
When you write your own task, it is best to actually extend the abstract Microsoft.Build.Utilities.Task class. This class will take care of implementing the two properties, and it will help with a few other things. By doing this you can focus on writing the logic behind your task, and take the MSBuild aspect for granted.
The Move task will just extend the Task class.The Move task contains four properties and the Execute method. The class diagram for the Move class is shown in
Figure 9. From the four properties, three are designed as possible inputs: SourceFiles, DestinationFiles, and DestinationFolder. The remaining MovedFiles property is an MSBuild output. This will contain a list of the files that were moved. Let's have a look at how the output, MovedFiles, is declared:
|
private ITaskItem[] movedFiles;
[Output]
public ITaskItem [] MovedFiles
{
get { return this.movedFiles; }
}
|
Figure 9 Move Class Diagram
This property returns an array of ITaskItem objects, and you know that it is available as an output because it has the Output attribute (Microsoft.Build.Framework.OutputAttribute) attached to it. Every output must have this attribute, and it also must have a public get accessor. The input properties are shown in Figure 10.

Figure 10 Move Input Properties
|
private ITaskItem[] sourceFiles;
private ITaskItem[] destFiles;
private ITaskItem destFolder;
[Required]
public ITaskItem [] SourceFiles
{
get { return this.sourceFiles; }
set { this.sourceFiles = value; }
}
public ITaskItem [] DestinationFiles
{
get { return this.destFiles; }
set { this.destFiles = value; }
}
public ITaskItem DestinationFolder
{
get { return this.destFolder; }
set { this.destFolder = value; }
}
|
The three input properties look like standard properties, with one exception. The SourceFiles property has the Required attribute (Microsoft.Build.Framework.RequiredAttribute). By using this attribute, the task will not be executed unless that input parameter has been given a value. The other input properties are optional. Since these are ITaskItems, you can access their metadata using the GetMetadata and SetMetadata methods. The Execute method simply looks at what properties have been set, does some error checking, and then moves the files one by one (creating the destination directories as necessary), as shown in Figure 11.

Figure 11 The Execute Method in Move Task
|
public override bool Execute()
{
Log.LogMessageFromText("Starting move", MessageImportance.Normal);
bool allSucceeded = true;
//Now we need to actually implement the move
if (this.SourceFiles == null || this.SourceFiles.Length <= 0)
{
//if nothing to move just leave quietly
this.DestinationFiles = new ITaskItem[0];
Log.LogMessageFromText("Nothing to move",
MessageImportance.Normal);
return true;
}
if (this.DestinationFiles == null && this.DestinationFolder == null)
{
Log.LogError("Unable to determine destination for files");
return false;
}
if (this.DestinationFiles != null && this.DestinationFolder != null)
{
Log.LogError("Both DestinationFiles & DestinationFolder " +
"were specified, only one can be defined");
return false;
}
if (this.DestinationFiles != null &&
(this.DestinationFiles.Length != this.SourceFiles.Length) )
{
//# of items in source & dest don't match up
Log.LogError("SourceFiles and DestinationFiles differ in length");
return false;
}
if (this.DestinationFiles == null)
{
//populate from DestinationFolder
this.DestinationFiles = new ITaskItem[this.SourceFiles.Length];
for (int i = 0; i < this.SourceFiles.Length; i++)
{
string destFile;
try
{
destFile = Path.Combine(
this.DestinationFolder.ItemSpec,Path.GetFileName(
this.SourceFiles[i].ItemSpec));
}
catch(Exception ex)
{
Log.LogError("Unable to move files; " + ex.Message,null);
this.DestinationFiles = new ITaskItem[0];
return false;
}
this.DestinationFiles[i] = new TaskItem(destFile);
this.SourceFiles[i].CopyMetadataTo(this.DestinationFiles[i]);
}
}
this.movedFiles = new ITaskItem[this.SourceFiles.Length];
//now we can go through and move all the files
for (int i = 0; i < SourceFiles.Length; i++)
{
string sourcePath = this.SourceFiles[i].ItemSpec;
string destPath = this.DestinationFiles[i].ItemSpec;
try
{
string message = string.Format(
"Moving file {0} to {1}", sourcePath, destPath);
Log.LogMessageFromText(message, MessageImportance.Normal);
FileInfo destFile = new FileInfo(destPath);
DirectoryInfo parentDir = destFile.Directory;
if (!parentDir.Exists) parentDir.Create();
File.Move(sourcePath, destPath);
this.movedFiles[i] = new TaskItem(destPath);
}
catch (Exception ex)
{
Log.LogError("Unable to move file: " + sourcePath + " to " +
destPath + "\n" + ex.Message);
allSucceeded = false;
}
}
return allSucceeded;
}
|
At this point I have a completed task and can use it in an MSBuild project. I created a blank project file named MoveFiles.proj for this purpose. To use this task I have to build the assembly that contains it. My assembly is named Tasks.dll. After building the assembly, I place it in a known directory. In this case I'm placing the assembly in a directory named SharedTasks. When you want to use a custom task in your projects, you have to tell MSBuild what task you intend to use and where it is located. You do this with the UsingTask element. In the case of my example project, the declaration looks like this:
|
<PropertyGroup>
<!-- Location of the shared tasks directory -->
<SharedTasksDir>..\..\SharedTasks</SharedTasksDir>
<SourceFolder>SourceFiles</SourceFolder>
<DestFolder>DestFolder</DestFolder>
</PropertyGroup>
<UsingTask AssemblyFile="$(SharedTasksDir)\Tasks.dll" TaskName="Move"/>
|
The UsingTask declaration states where to find the needed assembly (AssemblyFile) and the name of the task (TaskName). Instead of the AssemblyFile attribute, you can use the AssemblyName attribute. AssemblyFile causes MSBuild to use Assembly.LoadFrom to load the target assembly, whereas using AssemblyName causes MSBuild to use Assembly.Load. See the UsingTask documentation for details about the difference.
To demonstrate this task in action I have created a target, MoveFiles:
|
<Target Name="MoveFiles">
<Move SourceFiles=
"@(SourceFiles->'%(FullPath)')"
DestinationFiles=
"@(SourceFiles->'$(DestFolder)\
%(Filename)%(Extension)')">
<Output TaskParameter="MovedFiles"
ItemName="FilesMoved"/>
</Move>
<!-- Print out the files so we can see
where they moved to -->
<Message Text=
"Moved files:%0d%0a
@(FilesMoved->'%(FullPath)','%0d%0a ')"/>
</Target>
|
In this target I call the Move task just like I would any other task. I specify the input properties as parameters and capture the output in the Output element. Now I can call this target and verify that this task does indeed work (see Figure 12).
Figure 12 MoveFiles Target Output
Included in the source code for this article, available for download from the MSDN Magazine Web site, you'll find three other custom tasks: GetDate, GetRegKey, and TempFile. The GetDate task populates a property with the current date and time, based on a specified format. The GetRegKey task retrieves a registry value. The TempFile task creates a temporary file. Take a look at these tasks and their corresponding example project files for more insight on how to create custom tasks.
Conclusion
MSBuild is designed to work behind the scenes, and as a result many developers don't even realize that it exists. After having read this article you should feel confident about customizing your build process to suit your particular needs. I've given a broad overview of MSBuild here, but like many technologies, it cannot be completely covered in a single article. The best way to learn about MSBuild is to start using it.
This is not all there is to know about writing custom tasks, but from the information provided here you should be able to write many tasks that will make your work easier. From this point forward you can write your own custom tasks to take care of various build steps that you need executed. Some other issues that I did not discuss but are worth investigating include tasks executing in their own application domains and assigning metadata to items inside of a task.
There are many different resources available on the Web that you can turn to when you are having difficulties. A list of some of these resources is provided in the "Web Resources" sidebar. The MSBuild forum is a great place to go when you need a specific question answered.
Sayed Ibrahim Hashimi has a computer engineering degree from the University of Florida. He is a developer and architect in Jacksonville, Florida. He is an expert in the financial, education, and collection industries. His primary focus is working with .NET.