Targets and Build Order
|
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. |
A target is a named list of tasks. A build is a request to run some targets, and MSBuild works out from their attributes which other targets must run, in which order, and which can be skipped. Hooking custom work into the right place of the SDK’s build — before the compiler, after packing, once per target framework or once per project — is a matter of choosing the right attribute and the right anchor target. This page shows the rules running, with MSBuild 18.9 (.NET SDK 10.0.402).
Declaring and choosing targets
<Target Name="…"> declares a target anywhere in the project or its imports; a later declaration with the same name
replaces the earlier one completely. Which targets run first:
-
the targets in the
InitialTargetsattribute of theProjectelement (all of them, from every imported file, in import order) — always; -
then the targets passed with
-t:(-target:); if none, those inDefaultTargets; if none, the first target in the file after imports are expanded. For SDK projects that isBuild.
The order algorithm
For each target it is asked to build, MSBuild applies the same steps; a target that has already run in this build (for the same project and global properties) is never run again.
A demonstration with every attribute (the numbers in the messages are the expected order):
<Project InitialTargets="Init" DefaultTargets="Report">
<Target Name="Init">
<Message Importance="high" Text="1 Init (InitialTargets)" />
</Target>
<Target Name="ReadJournals">
<Message Importance="high" Text="3 ReadJournals (DependsOnTargets of Report)" />
</Target>
<Target Name="CheckBalance" BeforeTargets="Report">
<Message Importance="high" Text="4 CheckBalance (BeforeTargets=Report)" />
</Target>
<Target Name="Report" DependsOnTargets="Prepare;ReadJournals" Returns="@(ReportFile)">
<ItemGroup>
<ReportFile Include="out/trial-balance.json" />
</ItemGroup>
<Message Importance="high" Text="5 Report" />
</Target>
<Target Name="Prepare">
<Message Importance="high" Text="2 Prepare (first in DependsOnTargets; runs once even if requested again)" />
</Target>
<Target Name="Publish" AfterTargets="Report" DependsOnTargets="Prepare">
<Message Importance="high" Text="6 Publish (AfterTargets=Report)" />
</Target>
<Target Name="Skipped" BeforeTargets="Report" Condition="'$(Audit)' == 'true'" DependsOnTargets="AuditPrep">
<Message Importance="high" Text=" Skipped ran" />
</Target>
<Target Name="AuditPrep">
<Message Importance="high" Text=" AuditPrep ran" />
</Target>
</Project>
dotnet msbuild order.proj -nologo -v:m
dotnet msbuild order.proj -nologo -v:m -p:Audit=true
1 Init (InitialTargets)
2 Prepare (first in DependsOnTargets; runs once even if requested again)
3 ReadJournals (DependsOnTargets of Report)
4 CheckBalance (BeforeTargets=Report)
5 Report
6 Publish (AfterTargets=Report)
---
1 Init (InitialTargets)
2 Prepare (first in DependsOnTargets; runs once even if requested again)
3 ReadJournals (DependsOnTargets of Report)
4 CheckBalance (BeforeTargets=Report)
AuditPrep ran
Skipped ran
5 Report
6 Publish (AfterTargets=Report)
Publish depends on Prepare too, but Prepare had already run. Skipped and its dependency AuditPrep ran only when
its condition was true; when false, its DependsOnTargets were not run either. Two BeforeTargets targets for the same
anchor run in declaration order.
Returns, Outputs and -getTargetResult
Returns names the items a target hands back to its caller — the MSBuild task that invoked it (as
ResolveProjectReferences does with GetTargetPath), or the command line. If no target in the file has Returns,
Outputs is used instead, which mixes two concerns (incremental checks and return values); the SDK sets Returns on
its public targets. -getTargetResult (MSBuild 17.8+) builds a target and prints its result and returned items:
dotnet msbuild order.proj -nologo -t:Report -getTargetResult:Report
{
"TargetResults": {
"Report": {
"Result": "Success",
"Items": [
{
"Identity": "out/trial-balance.json",
"FullPath": "~/targets/out/trial-balance.json",
…
"Filename": "trial-balance",
"Extension": ".json",
…
}
]
}
}
}
Hooking into an SDK build safely
The SDK targets come after the project body (from the implicit Sdk.targets import), so a project cannot redefine
BeforeBuild or AfterBuild and expect it to win, and the SDK’s targets call many internal ones whose names start with
an underscore and may change. The documented approach: give your target a descriptive name and attach it with
BeforeTargets/AfterTargets to a public target such as BeforePublish, CoreCompile, Build or Pack.
The anchor matters more than it looks. Build itself is empty: its work is in its DependsOnTargets (CoreBuild), and
the algorithm runs DependsOnTargets before BeforeTargets. A hook written as BeforeTargets="Build" therefore
runs after compilation — and, in a multi-targeted project, once per inner build plus once in the outer build. Added
to the Ledgerline library (three TFMs):
<Project>
<Target Name="LedgerlineBeforeBuild" BeforeTargets="Build">
<Message Importance="high" Text="LedgerlineBeforeBuild: TargetFramework='$(TargetFramework)'" />
</Target>
</Project>
Ledgerline -> ~/ledgerline/src/Ledgerline/bin/Debug/net8.0/Ledgerline.dll
LedgerlineBeforeBuild: TargetFramework='net8.0'
Ledgerline -> ~/ledgerline/src/Ledgerline/bin/Debug/net10.0/Ledgerline.dll
LedgerlineBeforeBuild: TargetFramework='net10.0'
Ledgerline -> ~/ledgerline/src/Ledgerline/bin/Debug/netstandard2.0/Ledgerline.dll
LedgerlineBeforeBuild: TargetFramework='netstandard2.0'
LedgerlineBeforeBuild: TargetFramework=''
Four runs, each after its DLL was written. The fix depends on the intent:
<Project>
<!-- Once per target framework, before the compiler runs. -->
<Target Name="LedgerlineBeforeCompile" BeforeTargets="CoreCompile">
<Message Importance="high" Text="LedgerlineBeforeCompile: $(TargetFramework)" />
</Target>
<!-- Once per build of the multi-targeted project, before the inner builds start. -->
<Target Name="LedgerlineOnce" BeforeTargets="DispatchToInnerBuilds">
<Message Importance="high" Text="LedgerlineOnce: TargetFrameworks='$(TargetFrameworks)'" />
</Target>
</Project>
LedgerlineOnce: TargetFrameworks='net10.0;net8.0;netstandard2.0'
LedgerlineBeforeCompile: netstandard2.0
Ledgerline -> ~/ledgerline/src/Ledgerline/bin/Debug/netstandard2.0/Ledgerline.dll
LedgerlineBeforeCompile: net8.0
LedgerlineBeforeCompile: net10.0
Ledgerline -> ~/ledgerline/src/Ledgerline/bin/Debug/net8.0/Ledgerline.dll
Ledgerline -> ~/ledgerline/src/Ledgerline/bin/Debug/net10.0/Ledgerline.dll
DispatchToInnerBuilds exists only in the outer build of a multi-targeted project. For a target that must run exactly
once whether the project has one TFM or several, the documentation’s recipe combines both: a target with
BeforeTargets="DispatchToInnerBuilds" for the multi-targeted case, and a target with BeforeTargets="BeforeBuild"
that calls the outer project through the MSBuild task with RemoveProperties="TargetFramework" — MSBuild then runs
the "once" target in that outer instance only once, because a target never runs twice for the same project and global
properties. Other common anchors:
| To run… | Hook |
|---|---|
before compiling, per TFM (code generation, validation) |
|
after the output is copied, per TFM |
|
before/after publishing |
|
to add files to a package |
|
after the package is written |
|
The standard targets
Microsoft.Common.CurrentVersion.targets (imported by every SDK project) defines the public targets: Build,
Rebuild, Clean, Publish, ResolveReferences, Compile, CopyFilesToOutputDirectory, the empty extension points
BeforeBuild, AfterBuild, BeforeCompile, AfterCompile, BeforeClean, AfterClean, BeforePublish,
AfterPublish, and the …DependsOn properties (BuildDependsOn, CoreBuildDependsOn, CompileDependsOn) that list
each target’s dependencies and can be extended. NuGet adds Restore and Pack, the test platform VSTest and the
SDK Publish-related and container targets. dotnet msbuild -targets lists every target a project has.
Asking for one that does not exist:
dotnet msbuild -t:Deploy
~/demo/App.csproj : error MSB4057: The target "Deploy" does not exist in the project.
Common mistakes
-
BeforeTargets="Build"for work that must happen before compiling — it runs after; useCoreCompile. -
A hook that runs once per TFM plus once in the outer build — condition on
$(TargetFramework)or useDispatchToInnerBuilds. -
Redefining
BeforeBuild/AfterBuildin an SDK project — the SDK’s empty definitions come later and win. -
Hooking underscore-prefixed targets — internal and subject to change.
-
Expecting a target to run twice — each target runs at most once per project instance; call the project again with different global properties if it really must.
-
Outputsused only to return items — useReturns;Outputsturns on incremental checks.