Inline Tasks

This section documents MSBuild 18.9, the .NET SDK 10.0.402 (with its dotnet CLI) and NuGet 7.9, as published in the MSBuild, .NET and NuGet documentation on learn.microsoft.com, which is the reference these pages are written and verified against. Material about .NET 11 / NuGet 8.0 is still in preview and is always flagged as such — never presented as baseline.

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The C# Reference bibliography lists the reference material consulted while preparing these pages.

An inline task is a task whose C# (or Visual Basic) source lives in the project file. MSBuild compiles it the first time it is needed and then calls it like any other task. Inline tasks fill the gap between "a few Exec calls" and "a task assembly in its own project": a dozen lines of logic, versioned with the build, with no package to publish. They are compiled on every build that uses them, can only reference assemblies that ship with MSBuild or that you point to, and are hard to debug, so anything larger belongs in a custom task. Examples use MSBuild 18.9 (.NET SDK 10.0.402).

RoslynCodeTaskFactory and CodeTaskFactory

Factory Works in Notes

RoslynCodeTaskFactory

dotnet build and msbuild.exe, every OS

Since MSBuild 15.8. Compiles with the Roslyn compiler that ships with MSBuild against .NET Standard reference assemblies.

CodeTaskFactory

msbuild.exe only (.NET Framework)

The original factory, based on CodeDOM. Fails on the .NET MSBuild.

Using the old factory with dotnet msbuild:

~/inline/legacy.proj(62,5): error MSB4801: The task factory "CodeTaskFactory" is not supported on the .NET Core version of MSBuild.
~/inline/legacy.proj(62,5): error MSB4036: The "CountLedgerLines" task was not found. Check the following: 1.) The name of the task in the project file is the same as the name of the task class. 2.) The task class is "public" and implements the Microsoft.Build.Framework.ITask interface. 3.) The task is correctly declared with <UsingTask> in the project file, or in the *.tasks files located in the "<sdk>" directory.

Replacing TaskFactory="CodeTaskFactory" by TaskFactory="RoslynCodeTaskFactory" is usually all a migration needs. Projects that must still load in MSBuild versions older than 15.8 can choose the factory with a Choose on $(MSBuildVersion), as the documentation shows.

Structure

<UsingTask TaskName="Name" TaskFactory="RoslynCodeTaskFactory" AssemblyFile="$(MSBuildToolsPath)/Microsoft.Build.Tasks.Core.dll">
  <ParameterGroup>
    <Input ParameterType="System.String" Required="true" />
    <Result ParameterType="System.Int32" Output="true" />
  </ParameterGroup>
  <Task>
    <Reference Include="path/to/extra.dll" />
    <Using Namespace="System.IO" />
    <Code Type="Fragment" Language="cs">…</Code>
  </Task>
</UsingTask>
  • TaskName is the element name used in targets; TaskFactory and AssemblyFile (or AssemblyName) locate the factory.

  • ParameterGroup declares the parameters. ParameterType is a .NET type — System.String, System.Int32, System.Boolean, System.DateTime, arrays of these, Microsoft.Build.Framework.ITaskItem and ITaskItem[] for items with metadata. Required="true" makes MSBuild fail when the caller omits it; Output="true" makes it readable with <Output TaskParameter="…" />. A parameter without ParameterType is a System.String.

  • Using adds namespaces (a using directive cannot be written inside a fragment); Reference adds assemblies beyond those MSBuild and .NET Standard provide.

  • Code holds the source, inline (in a CDATA section when it contains <, & or >), or in a file with Source="…". Type is:

    • Fragment — the body of Execute(); parameters are properties, Log is available, returning is automatic;

    • Method — a complete public override bool Execute() method;

    • Class — a complete class deriving from Task (or implementing ITask); the ParameterGroup is then taken from the class’s properties.

A Ledgerline line counter

Two inline tasks over the Ledgerline journals: a Fragment that counts the data lines of each file, and a Class that counts distinct entries:

<Project>
  <!-- Fragment: the body of Execute(); parameters become properties. -->
  <UsingTask TaskName="CountLedgerLines"
             TaskFactory="RoslynCodeTaskFactory"
             AssemblyFile="$(MSBuildToolsPath)/Microsoft.Build.Tasks.Core.dll">
    <ParameterGroup>
      <Files ParameterType="Microsoft.Build.Framework.ITaskItem[]" Required="true" />
      <Total ParameterType="System.Int32" Output="true" />
    </ParameterGroup>
    <Task>
      <Using Namespace="System.IO" />
      <Using Namespace="System.Linq" />
      <Code Type="Fragment" Language="cs"><![CDATA[
        foreach (var file in Files)
        {
            var lines = File.ReadLines(file.ItemSpec).Skip(1).Count(l => l.Length > 0);
            Log.LogMessage(MessageImportance.High, "{0}: {1} journal lines", file.ItemSpec, lines);
            Total += lines;
        }
      ]]></Code>
    </Task>
  </UsingTask>

  <!-- Class: a whole ITask implementation, useful when helpers are needed. -->
  <UsingTask TaskName="LedgerEntryCount"
             TaskFactory="RoslynCodeTaskFactory"
             AssemblyFile="$(MSBuildToolsPath)/Microsoft.Build.Tasks.Core.dll">
    <Task>
      <Code Type="Class" Language="cs"><![CDATA[
using System.IO;
using System.Linq;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;

public class LedgerEntryCount : Task
{
    [Required]
    public ITaskItem[] Files { get; set; }

    [Output]
    public int Entries { get; private set; }

    public override bool Execute()
    {
        Entries = Files.SelectMany(f => File.ReadLines(f.ItemSpec).Skip(1))
                       .Where(l => l.Length > 0)
                       .Select(l => l.Split(',')[1])
                       .Distinct()
                       .Count();
        return !Log.HasLoggedErrors;
    }
}
      ]]></Code>
    </Task>
  </UsingTask>

  <ItemGroup>
    <Journal Include="journals/*.ledger.csv" />
  </ItemGroup>

  <Target Name="Count">
    <CountLedgerLines Files="@(Journal)">
      <Output TaskParameter="Total" PropertyName="LineTotal" />
    </CountLedgerLines>
    <LedgerEntryCount Files="@(Journal)">
      <Output TaskParameter="Entries" PropertyName="EntryTotal" />
    </LedgerEntryCount>
    <Message Importance="high" Text="$(LineTotal) lines in $(EntryTotal) entries" />
  </Target>
</Project>
dotnet msbuild inline.proj -t:Count -nologo -v:m
  journals/feb.ledger.csv: 3 journal lines
  journals/jan.ledger.csv: 2 journal lines
  5 lines in 3 entries

When the code does not compile

The code is compiled the first time the task is used, so a mistake shows up as a build error at the call, with the compiler’s message and the path of the generated source — followed by MSB4036, because the task could not be created. A fragment that uses an undeclared parameter:

<UsingTask TaskName="Broken" TaskFactory="RoslynCodeTaskFactory" AssemblyFile="$(MSBuildToolsPath)/Microsoft.Build.Tasks.Core.dll">
  <Task>
    <Code Type="Fragment" Language="cs">var total = Files.Count;</Code>
  </Task>
</UsingTask>
<temp>/tmp450b797928804c6a9ec15d737ca0eed4.tmp(35,13): error CS0103: The name 'Files' does not exist in the current context [~/inline/broken.proj]
~/inline/broken.proj(7,20): error : The source file for this compilation can be found at: "<temp>/tmp450b797928804c6a9ec15d737ca0eed4.tmp"
~/inline/broken.proj(7,20): error MSB4036: The "Broken" task was not found. …

<temp> is a per-build MSBuildTemp… folder in the temporary directory. A detailed log (-v:d) also shows the exact csc command line the factory used — references to MSBuild’s Microsoft.Build.Framework.dll, Microsoft.Build.Utilities.Core.dll, mscorlib.dll and netstandard.dll, /deterministic+, /target:Library. The documentation says the generated file is normally deleted and that setting MSBUILDLOGCODETASKFACTORYOUTPUT=1 preserves it; with SDK 10.0.402 on Linux the whole MSBuildTemp… folder was gone after the build even with the variable set, so copy the line numbers from the error, or move the logic to a class you can unit-test.

Common mistakes

  • CodeTaskFactory in a project built with dotnet — MSB4801.

  • A using directive inside a fragment — not allowed; use <Using Namespace>.

  • Code with < or & outside CDATA — an XML error, not a C# one.

  • Large inline tasks — compiled on every build, untestable; move them to a task project.

  • Referencing packages from inline code — only assemblies available to MSBuild (plus explicit Reference paths) can be used.