name: target-authoring description: "Canonical patterns for writing custom MSBuild targets. Only activate in MSBuild/.NET build context. USE FOR: diagnosing and fixing custom target authoring anti-patterns, reviewing MSBuild target definitions for correctness, diagnosing broken SDK target chains across files (e.g., Directory.Build.targets silently redefining SDK targets), fixing targets that replace CompileDependsOn instead of extending it with $(CompileDependsOn), fixing query targets that return stale results due to Outputs vs Returns misuse, fixing missing Inputs/Outputs causing unnecessary rebuilds, fixing missing FileWrites registration. Covers DependsOnTargets vs BeforeTargets vs AfterTargets, the Build→CoreBuild three-level pattern, hooking into the build pipeline, the $(XxxDependsOn) chain-extension pattern. DO NOT USE FOR: incremental build tuning (use incremental-build), parallelization (use build-parallelism), general anti-patterns (use msbuild-antipatterns), non-MSBuild build systems."
Custom Target Authoring Patterns
Canonical patterns from Microsoft.Common.CurrentVersion.targets in the MSBuild repository.
The Three-Level Target Chain
Every major entry point (Build, Rebuild, Clean) delegates to a property listing its dependencies, which chains through Before → Core → After:
<PropertyGroup>
<BuildDependsOn>
BeforeBuild;
CoreBuild;
AfterBuild
</BuildDependsOn>
</PropertyGroup>
<Target Name="Build"
Condition=" '$(_InvalidConfigurationWarning)' != 'true' "
DependsOnTargets="$(BuildDependsOn)"
Returns="@(TargetPathWithTargetPlatformMoniker)" />
<!-- Empty extensibility targets — users override these -->
<Target Name="BeforeBuild" />
<Target Name="AfterBuild" />
CoreBuild delegates to $(CoreBuildDependsOn) and includes error handlers:
<Target Name="CoreBuild" DependsOnTargets="$(CoreBuildDependsOn)">
<OnError ExecuteTargets="_TimeStampAfterCompile;PostBuildEvent"
Condition="'$(RunPostBuildEvent)' == 'Always'" />
<OnError ExecuteTargets="_CleanRecordFileWrites" />
</Target>
Rules
- Delegate to a property (
DependsOnTargets="$(MyTargetDependsOn)"), not hardcoded targets. OnErrorgoes inside the orchestrating target to ensure cleanup runs even on failure.- Empty Before/After targets are extensibility points. Users override them; SDKs never put logic in them.
Chain Extension — Append, Never Overwrite
When adding a custom target to an existing chain, append to the DependsOn property:
<!-- GOOD: Append to existing chain -->
<PropertyGroup>
<CompileDependsOn>$(CompileDependsOn);MyCodeGenTarget</CompileDependsOn>
</PropertyGroup>
<!-- BAD: Overwrites the entire chain, dropping SDK targets -->
<PropertyGroup>
<CompileDependsOn>MyCodeGenTarget</CompileDependsOn>
</PropertyGroup>
DependsOnTargets vs BeforeTargets vs AfterTargets
| Mechanism | Defined in | Best for |
|---|---|---|
DependsOnTargets |
The target that needs deps | Target explicitly requires others |
BeforeTargets |
The injecting target | Insert before a target you don't own |
AfterTargets |
The injecting target | Insert after a target you don't own |
Validation targets use BeforeTargets to intercept all entry points:
<Target Name="_CheckForInvalidConfigurationAndPlatform"
BeforeTargets="$(BuildDependsOn);Build;$(RebuildDependsOn);Rebuild;$(CleanDependsOn);Clean">
</Target>
Rules:
- Use
DependsOnTargetswhen your target needs specific prerequisites. - Use
BeforeTargets/AfterTargetswhen injecting into a pipeline you don't own. - Prefer
BeforeTargets="CoreCompile"over modifying$(CompileDependsOn)when you don't control the targets file.
Returns vs Outputs
<!-- Build returns items for consumption by referencing projects -->
<Target Name="Build"
DependsOnTargets="$(BuildDependsOn)"
Returns="@(TargetPathWithTargetPlatformMoniker)" />
<!-- GetTargetPath is a lightweight query target -->
<Target Name="GetTargetPath" Returns="@(TargetPathWithTargetPlatformMoniker)" />
Returnsspecifies what the MSBuild task receives when calling this project. Use for inter-project communication.Outputson inner targets is for incrementality (timestamp checks). Use for up-to-date detection.- Never mix the two purposes. Query targets (
GetTargetPath,GetTargetFrameworks) should useReturns, notOutputs.
Target Naming Conventions
| Pattern | Meaning | Example |
|---|---|---|
_PrefixedName |
Internal/private target | _TimeStampBeforeCompile |
CoreXxx |
The actual implementation | CoreBuild, CoreCompile |
BeforeXxx / AfterXxx |
Empty extensibility hooks | BeforeBuild, AfterCompile |
PrepareXxx |
Setup/validation phase | PrepareForBuild |
ResolveXxx |
Discovery/resolution phase | ResolveReferences |
GetXxx |
Lightweight query (no side effects) | GetTargetPath |
Complete Custom Target Template
<!-- 1. Define the DependsOn chain for extensibility -->
<PropertyGroup>
<MyFeatureDependsOn>
_ValidateMyFeatureInputs;
BeforeMyFeature;
CoreMyFeature;
AfterMyFeature
</MyFeatureDependsOn>
</PropertyGroup>
<!-- 2. Outer target with Returns for inter-project communication -->
<Target Name="MyFeature"
DependsOnTargets="$(MyFeatureDependsOn)"
Returns="@(MyFeatureOutput)" />
<!-- 3. Empty extensibility points -->
<Target Name="BeforeMyFeature" />
<Target Name="AfterMyFeature" />
<!-- 4. Core implementation with Inputs/Outputs for incrementality -->
<Target Name="CoreMyFeature"
Inputs="$(MSBuildAllProjects);@(MyFeatureInput)"
Outputs="$(IntermediateOutputPath)myfeature.generated.cs">
<Exec Command="my-tool.exe -o $(IntermediateOutputPath)myfeature.generated.cs" />
<!-- 5. Register outputs for clean tracking -->
<ItemGroup>
<Compile Include="$(IntermediateOutputPath)myfeature.generated.cs" />
<FileWrites Include="$(IntermediateOutputPath)myfeature.generated.cs" />
</ItemGroup>
</Target>
<!-- 6. Validation target runs first in the dependency chain -->
<Target Name="_ValidateMyFeatureInputs">
<Error Text="MyFeatureInput items are required."
Condition="'@(MyFeatureInput)' == ''" />
</Target>
Common Pitfalls
- Overwriting
DependsOnproperties drops SDK targets silently. Always include$(ExistingProperty)when appending. - Using
Outputson query targets causes MSBuild to skip them when "up to date," returning stale data. UseReturns. - Defining targets in
.propsmeansBeforeTargetson SDK targets have nothing to hook into yet. Move targets to.targets. - Forgetting
OnErrorin orchestrating targets means file tracking fails on build errors, breaking subsequent incremental builds.