Inline Tasks
|
This section documents MSBuild 18.9, the .NET SDK 10.0.402 (with its 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 |
|---|---|---|
|
|
Since MSBuild 15.8. Compiles with the Roslyn compiler that ships with MSBuild against .NET Standard reference assemblies. |
|
|
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>
-
TaskNameis the element name used in targets;TaskFactoryandAssemblyFile(orAssemblyName) locate the factory. -
ParameterGroupdeclares the parameters.ParameterTypeis a .NET type —System.String,System.Int32,System.Boolean,System.DateTime, arrays of these,Microsoft.Build.Framework.ITaskItemandITaskItem[]for items with metadata.Required="true"makes MSBuild fail when the caller omits it;Output="true"makes it readable with<Output TaskParameter="…" />. A parameter withoutParameterTypeis aSystem.String. -
Usingadds namespaces (ausingdirective cannot be written inside a fragment);Referenceadds assemblies beyond those MSBuild and .NET Standard provide. -
Codeholds the source, inline (in aCDATAsection when it contains<,&or>), or in a file withSource="…".Typeis:-
Fragment— the body ofExecute(); parameters are properties,Logis available, returning is automatic; -
Method— a completepublic override bool Execute()method; -
Class— a complete class deriving fromTask(or implementingITask); theParameterGroupis 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
Source: RoslynCodeTaskFactory: Hello world.
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
-
CodeTaskFactoryin a project built withdotnet—MSB4801. -
A
usingdirective inside a fragment — not allowed; use<Using Namespace>. -
Code with
<or&outsideCDATA— 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
Referencepaths) can be used.