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
- ITask interface and Task base class implementation
- ToolTask for wrapping external CLI tools
- IIncrementalTask for engine-filtered incremental execution
- Inline tasks with CodeTaskFactory
- UsingTask registration and task parameters
- Task debugging and NuGet packaging
Out of scope
- MSBuild project system authoring (targets, props, items, conditions) -- see [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
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 |
For detailed code examples (ToolTask, IIncrementalTask, task parameters, inline tasks, UsingTask, debugging, NuGet
packaging), see examples.md in this skill directory.
Agent Gotchas
Returning false without logging an error. If Execute() returns false but Log.LogError was never called,
MSBuild reports a generic "task failed" with no actionable message. Always log an error before returning false.
Using Console.WriteLine instead of Log.LogMessage. Console output bypasses MSBuild's logging infrastructure
and may not appear in build logs, binary logs, or IDE error lists. Always use Log.LogMessage, Log.LogWarning, or
Log.LogError.
Referencing IIncrementalTask without version-gating. This interface requires MSBuild 17.8+ (.NET 8 SDK). Tasks
referencing it will fail to load on older MSBuild versions with a TypeLoadException. If supporting older SDKs, use
target-level Inputs/Outputs instead. If the task must support both old and new MSBuild, ship separate task
assemblies per MSBuild version range or use #if conditional compilation with a version constant.
Placing task DLLs in the NuGet lib/ folder. This adds the assembly as a compile reference to consuming
projects, polluting their type namespace. Set IncludeBuildOutput=false and pack into tools/ instead.
Forgetting PrivateAssets="all" on MSBuild framework package references. Without it, Microsoft.Build.Framework
and Microsoft.Build.Utilities.Core become transitive dependencies of consuming projects, causing version conflicts.
Using AssemblyFile with a path relative to the project. In NuGet packages, the .targets file is in a
different location than the consuming project. Use $(MSBuildThisFileDirectory) to build paths relative to the
.targets file itself.
Leaving Debugger.Launch() in release builds. Shipping a task with unconditional Debugger.Launch() halts
builds on CI/CD servers. Guard with #if DEBUG or remove before packaging.
Inline tasks with complex dependencies. CodeTaskFactory compiles code at build time with limited assembly
references. For tasks that need NuGet packages or complex type hierarchies, compile a standalone task assembly
instead.
References
Code Navigation (Serena MCP)
Primary approach: Use Serena symbol operations for efficient code navigation:
- Find definitions:
serena_find_symbol instead of text search
- Understand structure:
serena_get_symbols_overview for file organization
- Track references:
serena_find_referencing_symbols for impact analysis
- Precise edits:
serena_replace_symbol_body for clean modifications
When to use Serena vs traditional tools:
- Use Serena: Navigation, refactoring, dependency analysis, precise edits
- Use Read/Grep: Reading full files, pattern matching, simple text operations
- Fallback: If Serena unavailable, traditional tools work fine
Example workflow:
# Instead of:
Read: src/Services/OrderService.cs
Grep: "public void ProcessOrder"
# Use:
serena_find_symbol: "OrderService/ProcessOrder"
serena_get_symbols_overview: "src/Services/OrderService.cs"
1---2name: dotnet-msbuild-tasks3category: developer-experience4subcategory: msbuild5description: Writes custom MSBuild tasks. ITask, ToolTask, IIncrementalTask, inline tasks, UsingTask.6license: MIT7targets: ['*']8tags: [foundation, dotnet, skill]9version: '0.0.1'10author: 'dotnet-agent-harness'11invocable: true12claudecode:13 allowed-tools: ['Read', 'Grep', 'Glob', 'Bash', 'Write', 'Edit']14codexcli:15 short-description: '.NET skill guidance for foundation tasks'16opencode:17 allowed-tools: ['Read', 'Grep', 'Glob', 'Bash', 'Write', 'Edit']18copilot: {}19geminicli: {}20antigravity: {}21---2223# dotnet-msbuild-tasks2425Guidance for authoring custom MSBuild tasks: implementing the `ITask` interface, extending `ToolTask` for CLI wrappers,26using `IIncrementalTask` (MSBuild 17.8+) for incremental execution, defining inline tasks with `CodeTaskFactory`,27registering tasks via `UsingTask`, declaring task parameters, debugging tasks, and packaging tasks as NuGet packages.2829**Version assumptions:** .NET 8.0+ SDK (MSBuild 17.8+). `IIncrementalTask` requires MSBuild 17.8+ (VS 2022 17.8+, .NET 830SDK). All examples use SDK-style projects. All C# examples assume `using Microsoft.Build.Framework;` and31`using Microsoft.Build.Utilities;` are in scope unless shown explicitly.3233## Scope3435- ITask interface and Task base class implementation36- ToolTask for wrapping external CLI tools37- IIncrementalTask for engine-filtered incremental execution38- Inline tasks with CodeTaskFactory39- UsingTask registration and task parameters40- Task debugging and NuGet packaging4142## Out of scope4344- MSBuild project system authoring (targets, props, items, conditions) -- see [skill:dotnet-msbuild-authoring]4546Cross-references: [skill:dotnet-msbuild-authoring] for custom targets, import ordering, items, conditions, and property47functions.4849---5051## ITask Interface5253All MSBuild tasks implement `Microsoft.Build.Framework.ITask`. The simplest approach is to inherit from54`Microsoft.Build.Utilities.Task`, which provides default implementations for `BuildEngine` and `HostObject`.5556### Minimal Custom Task5758```csharp59using Microsoft.Build.Framework;60using Microsoft.Build.Utilities;6162public class GenerateFileHash : Task63{64 [Required]65 public string InputFile { get; set; } = string.Empty;6667 [Output]68 public string Hash { get; set; } = string.Empty;6970 public override bool Execute()71 {72 if (!File.Exists(InputFile))73 {74 Log.LogError("Input file not found: {0}", InputFile);75 return false;76 }7778 using var stream = File.OpenRead(InputFile);79 var bytes = System.Security.Cryptography.SHA256.HashData(stream);80 Hash = Convert.ToHexString(bytes).ToLowerInvariant();8182 Log.LogMessage(MessageImportance.Normal,83 "SHA-256 hash for {0}: {1}", InputFile, Hash);84 return true;85 }86}87```8889### ITask Contract9091| Member | Purpose |92| ------------- | ------------------------------------------------------------- |93| `BuildEngine` | Provides logging, error reporting, and build context |94| `HostObject` | Host-specific data (rarely used) |95| `Execute()` | Runs the task. Return `true` for success, `false` for failure |9697The `Task` base class exposes a `Log` property (`TaskLoggingHelper`) with convenience methods:9899| Method | When to use |100| --------------------------------- | ---------------------------------------- |101| `Log.LogMessage(importance, msg)` | Informational output (Normal, High, Low) |102| `Log.LogWarning(msg)` | Non-fatal issues |103| `Log.LogError(msg)` | Fatal errors (causes build failure) |104| `Log.LogWarningFromException(ex)` | Warning from caught exception |105| `Log.LogErrorFromException(ex)` | Error from caught exception |106107---108109For detailed code examples (ToolTask, IIncrementalTask, task parameters, inline tasks, UsingTask, debugging, NuGet110packaging), see `examples.md` in this skill directory.111112## Agent Gotchas1131141. **Returning `false` without logging an error.** If `Execute()` returns `false` but `Log.LogError` was never called,115 MSBuild reports a generic "task failed" with no actionable message. Always log an error before returning `false`.1161171. **Using `Console.WriteLine` instead of `Log.LogMessage`.** Console output bypasses MSBuild's logging infrastructure118 and may not appear in build logs, binary logs, or IDE error lists. Always use `Log.LogMessage`, `Log.LogWarning`, or119 `Log.LogError`.1201211. **Referencing `IIncrementalTask` without version-gating.** This interface requires MSBuild 17.8+ (.NET 8 SDK). Tasks122 referencing it will fail to load on older MSBuild versions with a `TypeLoadException`. If supporting older SDKs, use123 target-level `Inputs`/`Outputs` instead. If the task must support both old and new MSBuild, ship separate task124 assemblies per MSBuild version range or use `#if` conditional compilation with a version constant.1251261. **Placing task DLLs in the NuGet `lib/` folder.** This adds the assembly as a compile reference to consuming127 projects, polluting their type namespace. Set `IncludeBuildOutput=false` and pack into `tools/` instead.1281291. **Forgetting `PrivateAssets="all"` on MSBuild framework package references.** Without it, `Microsoft.Build.Framework`130 and `Microsoft.Build.Utilities.Core` become transitive dependencies of consuming projects, causing version conflicts.1311321. **Using `AssemblyFile` with a path relative to the project.** In NuGet packages, the `.targets` file is in a133 different location than the consuming project. Use `$(MSBuildThisFileDirectory)` to build paths relative to the134 `.targets` file itself.1351361. **Leaving `Debugger.Launch()` in release builds.** Shipping a task with unconditional `Debugger.Launch()` halts137 builds on CI/CD servers. Guard with `#if DEBUG` or remove before packaging.1381391. **Inline tasks with complex dependencies.** `CodeTaskFactory` compiles code at build time with limited assembly140 references. For tasks that need NuGet packages or complex type hierarchies, compile a standalone task assembly141 instead.142143---144145## References146147- [MSBuild Task Writing](https://learn.microsoft.com/en-us/visualstudio/msbuild/task-writing)148- [MSBuild Task Reference](https://learn.microsoft.com/en-us/visualstudio/msbuild/msbuild-task-reference)149- [ToolTask Class](https://learn.microsoft.com/en-us/dotnet/api/microsoft.build.utilities.tooltask)150- [MSBuild Inline Tasks](https://learn.microsoft.com/en-us/visualstudio/msbuild/msbuild-inline-tasks)151- [UsingTask Element](https://learn.microsoft.com/en-us/visualstudio/msbuild/usingtask-element-msbuild)152- [MSBuild Task Parameters](https://learn.microsoft.com/en-us/visualstudio/msbuild/task-writing#task-parameters)153- [Creating a NuGet Package with MSBuild Tasks](https://learn.microsoft.com/en-us/nuget/create-packages/creating-a-package-msbuild)154- [Debugging MSBuild Tasks](https://learn.microsoft.com/en-us/visualstudio/msbuild/how-to-debug-msbuild-custom-task)155156## Code Navigation (Serena MCP)157158**Primary approach:** Use Serena symbol operations for efficient code navigation:1591601. **Find definitions**: `serena_find_symbol` instead of text search1612. **Understand structure**: `serena_get_symbols_overview` for file organization1623. **Track references**: `serena_find_referencing_symbols` for impact analysis1634. **Precise edits**: `serena_replace_symbol_body` for clean modifications164165**When to use Serena vs traditional tools:**166167- **Use Serena**: Navigation, refactoring, dependency analysis, precise edits168- **Use Read/Grep**: Reading full files, pattern matching, simple text operations169- **Fallback**: If Serena unavailable, traditional tools work fine170171**Example workflow:**172173```text174# Instead of:175Read: src/Services/OrderService.cs176Grep: "public void ProcessOrder"177178# Use:179serena_find_symbol: "OrderService/ProcessOrder"180serena_get_symbols_overview: "src/Services/OrderService.cs"181```