Rico Suter's blog.
 


In this article describes how to implement MSBuild tasks with inline C# or in an external .NET assembly. After doing so, we will bundle these tasks in a NuGet package, so that they can easily be distributed and updated via NuGet and executed as part of the MSBuild compilation process. This is a powerfull technique to share and manage MSBuild tasks (e.g. build scripts, code generators, etc.) in bigger environments (enterprises, open-source libraries, common/shared tasks).

Write simple task with inline C

First we implement an MSBuild task with inline C# code. For the purpose of this article, a simple “Hello World” is sufficient:

MyTasks.props:

<?xml version="1.0" encoding="utf-8" ?>
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
    <UsingTask TaskName="MyInlineTask" 
                TaskFactory="CodeTaskFactory" 
                AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.v4.0.dll">
        <ParameterGroup>
            <MyParameter ParameterType="System.String" Required="true" />
        </ParameterGroup>
        <Task>
            <Using Namespace="System"/>
            <Using Namespace="System.Collections.Generic"/>
            <Using Namespace="System.IO"/>
            <Using Namespace="System.Linq"/>
            <Using Namespace="System.Text"/>
            <Using Namespace="System.Text.RegularExpressions"/>            
            <Code Type="Fragment" Language="cs">
                <![CDATA[
                    var myParameter = this.MyParameter;             
                    Log.LogMessage(MessageImportance.High, "Hello from MyInlineTask: " + myParameter);
                ]]>
            </Code>
        </Task>
    </UsingTask>
</Project>

You can now run the implemented task in your .csproj file by importing the .props file and executing the MyInlineTask task:

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

    <Target Name="MyTasks" AfterTargets="AfterBuild">
        <MyInlineTask MyParameter="Foobar"></MyInlineTask>
    </Target>
</Project>

Write task in external assembly

If you implement tasks with more source code, it is recommended to implement them in a separate .NET project. In a new project, reference the GAC assembly Microsoft.Build.Utilities.Core and create a new class which inherits from Microsoft.Build.Utilities.Task:

public class MyExternalTask : Microsoft.Build.Utilities.Task
{
    public string MyParameter { get; set; }

    public override bool Execute()
    {
        Console.WriteLine("Hello from the MyAssembly: " + MyParameter);
        return true; 
    }
}

The task can now be used by importing the assembly with its file path and defining the class with the full qualified name:

MyTasks.props:

<?xml version="1.0" encoding="utf-8" ?>
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
    <UsingTask TaskName="MyExternalTask" TaskFactory="MyAssembly.MyExternalTask" AssemblyFile="MyAssembly.dll"/>
</Project>

As before, the task can be called in your .csproj file by importing the .props file and executing the task:

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

    <Target Name="MyTasks" AfterTargets="AfterBuild">
        <MyExternalTask MyParameter="Foobar"></MyExternalTask>
    </Target>
</Project>

Distribution via NuGet

Now that we have implemented the two tasks, we can distribute them with a NuGet package and automatically run both tasks after the AfterBuild target.

First, we create the MyPackage.props file (the file name must match the package ID, previously called MyProps.props) which contains the custom tasks and which will be added to the project where the NuGet package is installed:

<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
    <UsingTask ... />
    <UsingTask ... />
    <Target Name="MyTasks" AfterTargets="AfterBuild">
        <MyInlineTask MyParameter="Foo" />
        <MyExternalTask MyParameter="Bar" />
    </Target>
</Project>

Next, we create the MyPackage.nuspec NuSpec file which references the MyPackage.props file and the MyAssembly.dll assembly file with the external task:

<?xml version="1.0" encoding="utf-8"?>
<package xmlns="http://schemas.microsoft.com/packaging/2011/08/nuspec.xsd">
    <metadata>
        <id>MyPackage</id>
        <version>1.0.0.0</version>
        <title>MyPackage</title>
        <authors>Author</authors>
        <description>Description</description>
        <tags>MyTags</tags>
        <copyright>Copyright © Company 2014</copyright>
        <developmentDependency>true</developmentDependency>
        <dependencies />
        <references />
    </metadata>
    <files>
        <file src="MyPackage.props" target="build" />
        <file src="MyAssembly.dll" target="build" />
    </files>
</package>

It is important to set developmentDependency to true so that the package is installed and marked as development dependency in the project’s packages.config. This way the package is only installed in the project but not marked as a runtime dependency of the project.

Now we can generate the NuGet package:

nuget pack MyPackage.nuspec

After installing the package in another project, the project’s .csproj file should import the .props file from the NuGet package:

<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.props" />
<Import Project="..\packages\MyPackage.1.0.0.0\build\MyPackage.props" Condition="Exists('..\packages\MyPackage.1.0.0.0\build\MyPackage.props')" />
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
    <PropertyGroup>
    ...

Now the tasks should be executed when the MSBuild project is built. I used NuGet packaged MSBuild tasks in the NSwag project so that the code generators can be run as part of the MSBuild compilation.



Discussion