Custom Target Authoring Patterns
SkillFiles & storageGuides your agent in writing correct custom MSBuild targets, fixing broken dependency chains and rebuild issues.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the Custom Target Authoring Patterns skill
About this capability
Canonical patterns for writing custom MSBuild targets. USE FOR: diagnosing and fixing custom target authoring anti-patterns; broken SDK target chains across files (e.g., Directory.Build.targets silently redefining SDK targets); targets that replace CompileDependsOn instead of extending it with $(Com
What this skill tells your AI
The instructions your AI receives, as published by dotnet/skills in plugins/dotnet-msbuild/skills/target-authoring/SKILL.md and read by ahel’s review.
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.
Signals
- GitHub stars
- 5k
- Forks
- 414
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
target-authoring- Source
- github.com/dotnet/skills