Claude
Skills
Sign in
Back

dotnet-msbuild-tasks

Included with Lifetime
$97 forever

Writing custom MSBuild tasks. ITask, ToolTask, IIncrementalTask, inline tasks, UsingTask.

Writing & Docs

What this skill does


# dotnet-msbuild-tasks

Guidance for authoring custom MSBuild tasks: implementing the `ITask` interface, extending `ToolTask` for CLI wrappers, using `IIncrementalTask` (MSBuild 17.8+) for incremental execution, defining inline tasks with `CodeTaskFactory`, registering tasks via `UsingTask`, declaring task parameters, debugging tasks, and packaging tasks as NuGet packages.

**Version assumptions:** .NET 8.0+ SDK (MSBuild 17.8+). `IIncrementalTask` requires MSBuild 17.8+ (VS 2022 17.8+, .NET 8 SDK). All examples use SDK-style projects. All C# examples assume `using Microsoft.Build.Framework;` and `using Microsoft.Build.Utilities;` are in scope unless shown explicitly.

**Scope boundary:** This skill owns custom MSBuild task authoring -- ITask, ToolTask, IIncrementalTask, inline tasks, UsingTask, parameters, debugging, and NuGet packaging. MSBuild project system authoring (targets, props, items, conditions) is owned by [skill:dotnet-msbuild-authoring].

Cross-references: [skill:dotnet-msbuild-authoring] for custom targets, import ordering, items, conditions, and property functions.

---

## ITask Interface

All MSBuild tasks implement `Microsoft.Build.Framework.ITask`. The simplest approach is to inherit from `Microsoft.Build.Utilities.Task`, which provides default implementations for `BuildEngine` and `HostObject`.

### Minimal Custom Task

```csharp
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;

public class GenerateFileHash : Task
{
    [Required]
    public string InputFile { get; set; } = string.Empty;

    [Output]
    public string Hash { get; set; } = string.Empty;

    public override bool Execute()
    {
        if (!File.Exists(InputFile))
        {
            Log.LogError("Input file not found: {0}", InputFile);
            return false;
        }

        using var stream = File.OpenRead(InputFile);
        var bytes = System.Security.Cryptography.SHA256.HashData(stream);
        Hash = Convert.ToHexString(bytes).ToLowerInvariant();

        Log.LogMessage(MessageImportance.Normal,
            "SHA-256 hash for {0}: {1}", InputFile, Hash);
        return true;
    }
}
```

### ITask Contract

| Member | Purpose |
|---|---|
| `BuildEngine` | Provides logging, error reporting, and build context |
| `HostObject` | Host-specific data (rarely used) |
| `Execute()` | Runs the task. Return `true` for success, `false` for failure |

The `Task` base class exposes a `Log` property (`TaskLoggingHelper`) with convenience methods:

| Method | When to use |
|---|---|
| `Log.LogMessage(importance, msg)` | Informational output (Normal, High, Low) |
| `Log.LogWarning(msg)` | Non-fatal issues |
| `Log.LogError(msg)` | Fatal errors (causes build failure) |
| `Log.LogWarningFromException(ex)` | Warning from caught exception |
| `Log.LogErrorFromException(ex)` | Error from caught exception |

---

## ToolTask Base Class

`ToolTask` extends `Task` for wrapping external command-line tools. It handles process invocation, output capture, and exit code interpretation.

```csharp
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;

public class RunLintTool : ToolTask
{
    [Required]
    public string SourceDirectory { get; set; } = string.Empty;

    public string Severity { get; set; } = "warning";

    // Required: name of the executable
    protected override string ToolName => "dotnet-lint";

    // Required: full path or tool name (OS resolves via PATH)
    protected override string GenerateFullPathToTool()
    {
        // Return tool name; the OS resolves it via PATH at process start
        return ToolName;
    }

    // Required: build the command-line arguments
    protected override string GenerateCommandLineCommands()
    {
        var builder = new CommandLineBuilder();
        builder.AppendSwitch("--check");
        builder.AppendSwitchIfNotNull("--severity ", Severity);
        builder.AppendFileNameIfNotNull(SourceDirectory);
        return builder.ToString();
    }

    // Optional: interpret non-zero exit codes
    protected override bool HandleTaskExecutionErrors()
    {
        Log.LogError("{0} found lint violations in {1}",
            ToolName, SourceDirectory);
        return false;
    }
}
```

### Key ToolTask Overrides

| Override | Purpose |
|---|---|
| `ToolName` | Executable file name (e.g., `dotnet-lint`) |
| `GenerateFullPathToTool()` | Full path to executable, or return `ToolName` to let the OS resolve via `PATH` |
| `GenerateCommandLineCommands()` | Build argument string for the tool |
| `GenerateResponseFileCommands()` | Arguments written to a response file (for long command lines) |
| `HandleTaskExecutionErrors()` | Custom handling of non-zero exit codes |
| `StandardOutputLoggingImportance` | Log level for stdout (default: `Low`) |
| `StandardErrorLoggingImportance` | Log level for stderr (default: `Normal`) |

### Response Files for Long Command Lines

When the argument list is too long for the OS command line (common with many source files), use `GenerateResponseFileCommands()` to write arguments to a temporary response file:

```csharp
protected override string GenerateResponseFileCommands()
{
    var builder = new CommandLineBuilder();
    // These arguments go into a @response.rsp file
    foreach (var source in SourceFiles)
    {
        builder.AppendFileNameIfNotNull(source.ItemSpec);
    }
    return builder.ToString();
}

protected override string GenerateCommandLineCommands()
{
    // These arguments stay on the command line (before the @file ref)
    var builder = new CommandLineBuilder();
    builder.AppendSwitchIfNotNull("--config ", ConfigFile);
    return builder.ToString();
}
```

MSBuild creates the response file, passes `@responsefile.rsp` to the tool, and cleans up afterward. The tool must support `@file` syntax (most .NET tools do).

**When to use ToolTask vs Task:** Use `ToolTask` when wrapping an external CLI tool. Use `Task` (ITask) when the logic is pure .NET code with no external process.

---

## IIncrementalTask

`Microsoft.Build.Framework.IIncrementalTask` (MSBuild 17.8+, VS 2022 17.8+, .NET 8 SDK) signals to the MSBuild engine that a task supports receiving pre-filtered inputs. When a target declares `Inputs`/`Outputs` and the engine determines which inputs have changed, it passes only the changed items to an `IIncrementalTask`-implementing task instead of the full item list.

### Version Gate

`IIncrementalTask` requires:
- MSBuild 17.8+ (ships with VS 2022 17.8+)
- .NET 8.0 SDK or later

Tasks targeting older MSBuild versions must not reference this interface. Use target-level `Inputs`/`Outputs` for incrementality on older versions. See [skill:dotnet-msbuild-authoring] for target-level incremental patterns.

### How It Works

1. The target declares `Inputs` and `Outputs` (required -- the engine uses these for change detection).
2. MSBuild compares timestamps and determines which inputs are out of date.
3. If the task implements `IIncrementalTask`, MSBuild passes only the changed items to the task's `ITaskItem[]` parameters instead of the full set.
4. The task processes only those items -- no manual timestamp logic needed.

The `FailIfIncrementalBuildIsNotPossible` property controls fallback behavior:
- `false` (default): If the engine cannot determine changed inputs (e.g., missing `Outputs`), it falls back to passing all inputs. The task runs in full-rebuild mode.
- `true`: If the engine cannot provide incremental inputs, the task logs an error and fails. Use this when full rebuilds are unacceptably slow.

### Implementation

```csharp
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;

public class TransformTemplates : Task, IIncrementalTask
{
    [Required]
    public ITaskItem[] Templates { get; set; } = [];

    [Output]
    public ITaskItem[] GeneratedFiles { get; set; } = [];

    // IIncrementalTask: if true, the task errors when the engine
    // cannot provide filtered inputs (falls back to full set if false)
    public bool FailIfIncr

Related in Writing & Docs