Custom MSBuild Tasks

How to write custom MSBuild tasks in C#
8 Oct 2025 8 Oct 2025 2 min read MSBuild .NET C#

There are three ways to write a custom MSBuild task:

  1. A .NET assembly
  2. Inline inside a .tasks file
  3. In a C# file imported inside a .tasks file

Variations 

.NET Assembly 

A basic C# task:


using Microsoft.Build.Framework;

using Task = Microsoft.Build.Utilities.Task;

namespace MyTask.MSBuild;

public sealed class HelloTask : Task
{
    [Required]
    public string? Name { get; set; }

    /// <inheritdoc />
    public override bool Execute()
    {
        this.Log.LogMessage(MessageImportance.High, $"Hello, {this.Name}!");
        return true;
    }
}

The .csproj file:


<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>netstandard2.0</TargetFramework>
    <LangVersion>latest</LangVersion>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.Build.Utilities.Core" Version="17.14.8" />
  </ItemGroup>

</Project>

Then build the project.

Now, you can import and use it:


<Project>

  <UsingTask TaskName="MyTask.MSBuild.HelloTask"
             AssemblyFile="PATH_TO_YOUR_TASK_DLL"
             />

  <Target Name="_RunCustomTask" AfterTargets="Build">
    <HelloTask Name="World" />
  </Target>

</Project>

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.

Inline 

Define the task in a .tasks file:


<Project>

  <UsingTask TaskName="HelloTask"
             TaskFactory="RoslynCodeTaskFactory"
             AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.Core.dll">
    <ParameterGroup>
      <Name ParameterType="System.String" Required="true" />
    </ParameterGroup>
    <Task>
      <Using Namespace="System" />
      <Code Type="Fragment" Language="cs">
        <![CDATA[
          Log.LogMessage(MessageImportance.High, $"Hello, {Name}!");
        ]]>
      </Code>
    </Task>
  </UsingTask>

</Project>

Use it:


<Project>

  <Import Project="$(MSBuildThisFileDirectory)MyTask.tasks" />

  <Target Name="_RunCustomTask" AfterTargets="Build">
    <HelloTask Name="World" />
  </Target>

</Project>

Inline with C# File 

This is a variation of the inline definition.

Define the task in a C# file:


using Microsoft.Build.Framework;

using Task = Microsoft.Build.Utilities.Task;

namespace TarTask.MSBuild;

#nullable enable

public sealed class HelloTask : Task
{
    [Required]
    public string? Name { get; set; }

    /// <inheritdoc />
    public override bool Execute()
    {
        this.Log.LogMessage(MessageImportance.High, $"Hello, {this.Name}!");
        return true;
    }
}

Import and use the task:


<Project>

  <UsingTask TaskName="HelloTask"
             TaskFactory="RoslynCodeTaskFactory"
             AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.Core.dll">
    <ParameterGroup>
      <Name ParameterType="System.String" Required="true" />
    </ParameterGroup>
    <Task>
      <Using Namespace="System" />
      <Code Type="Class" Language="cs" Source="$(MSBuildThisFileDirectory)HelloTask.cs" />
    </Task>
  </UsingTask>

  <!-- Make VS rebuild project when task has changed. -->
  <ItemGroup>
    <UpToDateCheckInput Include="$(MSBuildThisFileDirectory)HelloTask.cs" />
  </ItemGroup>

  <Target Name="_RunCustomTask" AfterTargets="Build">
    <HelloTask Name="World" />
  </Target>

</Project>

Comparison 

This how these approaches differ:

What Assembly Inline Inline with C# File
Change Implementation ❌ (requires VS restart)
Code Completion
C# Syntax Highlighting
More than one file possible