Skip to content

Commit ff90fb3

Browse files
committed
Add article about custom MSBuild tasks
1 parent 89249ff commit ff90fb3

File tree

1 file changed

+191
-0
lines changed

1 file changed

+191
-0
lines changed
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
---
2+
title: Custom MSBuild Tasks
3+
description: How to write custom MSBuild tasks in C#
4+
date: 2025-10-08
5+
topics:
6+
- msbuild
7+
- dotnet
8+
- csharp
9+
---
10+
11+
There are three ways to write a custom MSBuild task:
12+
13+
1. A .NET assembly
14+
1. Inline inside a `.tasks` file
15+
1. In a C# file imported inside a `.tasks` file
16+
17+
## Variations
18+
19+
### .NET Assembly
20+
21+
**A basic C# task:**
22+
23+
```c#
24+
using Microsoft.Build.Framework;
25+
26+
using Task = Microsoft.Build.Utilities.Task;
27+
28+
namespace MyTask.MSBuild;
29+
30+
public sealed class HelloTask : Task
31+
{
32+
[Required]
33+
public string? Name { get; set; }
34+
35+
/// <inheritdoc />
36+
public override bool Execute()
37+
{
38+
this.Log.LogMessage(MessageImportance.High, $"Hello, {this.Name}!");
39+
return true;
40+
}
41+
}
42+
```
43+
44+
**The .csproj file:**
45+
46+
```xml
47+
<Project Sdk="Microsoft.NET.Sdk">
48+
49+
<PropertyGroup>
50+
<TargetFramework>netstandard2.0</TargetFramework>
51+
<LangVersion>latest</LangVersion>
52+
<ImplicitUsings>enable</ImplicitUsings>
53+
<Nullable>enable</Nullable>
54+
</PropertyGroup>
55+
56+
<ItemGroup>
57+
<PackageReference Include="Microsoft.Build.Utilities.Core" Version="17.14.8" />
58+
</ItemGroup>
59+
60+
</Project>
61+
```
62+
63+
Then build the project.
64+
65+
Now, you can **import and use it**:
66+
67+
```xml
68+
<Project>
69+
70+
<UsingTask TaskName="MyTask.MSBuild.HelloTask"
71+
AssemblyFile="PATH_TO_YOUR_TASK_DLL"
72+
/>
73+
74+
<Target Name="_RunCustomTask" AfterTargets="Build">
75+
<HelloTask Name="World" />
76+
</Target>
77+
78+
</Project>
79+
```
80+
81+
> [!WARNING]
82+
> Once Visual Studio has processed the `<UsingTask>` element, the **dll is permanently loaded** in Visual Studio. Because of this, you **can't change the implementation** of the task - until you restart Visual Studio.
83+
84+
### Inline
85+
86+
**Define the task in a `.tasks` file:**
87+
88+
```xml
89+
<Project>
90+
91+
<UsingTask TaskName="HelloTask"
92+
TaskFactory="RoslynCodeTaskFactory"
93+
AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.Core.dll">
94+
<ParameterGroup>
95+
<Name ParameterType="System.String" Required="true" />
96+
</ParameterGroup>
97+
<Task>
98+
<Using Namespace="System" />
99+
<Code Type="Fragment" Language="cs">
100+
<![CDATA[
101+
Log.LogMessage(MessageImportance.High, $"Hello, {Name}!");
102+
]]>
103+
</Code>
104+
</Task>
105+
</UsingTask>
106+
107+
</Project>
108+
```
109+
110+
**Use it:**
111+
112+
```xml
113+
<Project>
114+
115+
<Import Project="$(MSBuildThisFileDirectory)MyTask.tasks" />
116+
117+
<Target Name="_RunCustomTask" AfterTargets="Build">
118+
<HelloTask Name="World" />
119+
</Target>
120+
121+
</Project>
122+
```
123+
124+
### Inline with C# File
125+
126+
This is a variation of the inline definition.
127+
128+
**Define the task in a C# file:**
129+
130+
```csharp
131+
using Microsoft.Build.Framework;
132+
133+
using Task = Microsoft.Build.Utilities.Task;
134+
135+
namespace TarTask.MSBuild;
136+
137+
#nullable enable
138+
139+
public sealed class HelloTask : Task
140+
{
141+
[Required]
142+
public string? Name { get; set; }
143+
144+
/// <inheritdoc />
145+
public override bool Execute()
146+
{
147+
this.Log.LogMessage(MessageImportance.High, $"Hello, {this.Name}!");
148+
return true;
149+
}
150+
}
151+
```
152+
153+
**Import and use the task:**
154+
155+
```xml
156+
<Project>
157+
158+
<UsingTask TaskName="HelloTask"
159+
TaskFactory="RoslynCodeTaskFactory"
160+
AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.Core.dll">
161+
<ParameterGroup>
162+
<Name ParameterType="System.String" Required="true" />
163+
</ParameterGroup>
164+
<Task>
165+
<Using Namespace="System" />
166+
<Code Type="Class" Language="cs" Source="$(MSBuildThisFileDirectory)HelloTask.cs" />
167+
</Task>
168+
</UsingTask>
169+
170+
<!-- Make VS rebuild project when task has changed. -->
171+
<ItemGroup>
172+
<UpToDateCheckInput Include="$(MSBuildThisFileDirectory)HelloTask.cs" />
173+
</ItemGroup>
174+
175+
<Target Name="_RunCustomTask" AfterTargets="Build">
176+
<HelloTask Name="World" />
177+
</Target>
178+
179+
</Project>
180+
```
181+
182+
## Comparison
183+
184+
This how these approaches differ:
185+
186+
| What | Assembly | Inline | Inline with C# File
187+
| ----------------------------- | ------------------------- | ------ | -------------------
188+
| Change Implementation | ❌ (requires VS restart) | ✅ | ✅
189+
| Code Completion | ✅ | ❌ | ❌
190+
| C# Syntax Highlighting | ✅ | ✅ | ❌
191+
| More than one file possible | ✅ | ❌ | ❌

0 commit comments

Comments
 (0)