MSBuild Chain Extension Pattern: Why You Should Never Overwrite DependsOn Properties
You must append to $(…DependsOn) properties using the syntax $(CompileDependsOn);YourTarget rather than overwriting them, because overwriting silently removes SDK-provided targets and breaks the build chain.
The dotnet/skills repository documents a critical pattern for extending MSBuild projects without corrupting the default build pipeline. MSBuild organizes compilation through a three-level target chain where property-based dependency lists like BuildDependsOn, CompileDependsOn, and CoreBuildDependsOn control execution order. Understanding how to safely extend these chains prevents subtle bugs that cause missing compile steps, broken incremental builds, and lost SDK functionality.
The Three-Level Target Chain and Property-Based Dependencies
MSBuild executes the build process as a hierarchical chain: Build → CoreBuild → Clean. Each level relies on semicolon-separated properties—such as BuildDependsOn, CompileDependsOn, and CoreBuildDependsOn—that contain ordered lists of target names. These properties act as extensible hooks defined in plugins/dotnet-msbuild/skills/target-authoring/SKILL.md, allowing the SDK to declare default steps while permitting custom targets to join the sequence.
When MSBuild evaluates a target like Compile, it expands the CompileDependsOn property to determine which targets must execute first. This design creates a chain extension pattern where multiple imports can contribute steps to the same logical phase without knowing about each other.
The Correct MSBuild Chain Extension Pattern
Appending to DependsOn Properties (Correct Approach)
Always preserve the existing property value when adding custom targets. According to the guidance in plugins/dotnet-msbuild/skills/target-authoring/SKILL.md, the canonical pattern uses property reference syntax:
<PropertyGroup>
<!-- ✅ Correct – preserves SDK compile steps and adds custom logic -->
<CompileDependsOn>$(CompileDependsOn);MyCodeGenTarget</CompileDependsOn>
</PropertyGroup>
This approach maintains the full SDK chain—including CoreCompile, GenerateAssemblyInfo, and incremental-build tracking—while inserting your target at the end of the sequence.
Overwriting DependsOn Properties (The Anti-Pattern)
Overwriting the property destroys the chain:
<PropertyGroup>
<!-- ❌ Wrong – drops all SDK compile steps, causing build failures -->
<CompileDependsOn>MyCodeGenTarget</CompileDependsOn>
</PropertyGroup>
When you overwrite CompileDependsOn, MSBuild no longer executes built-in targets like CoreCompile or registers file writes for incremental builds. The project may fail to compile entirely, or silently skip essential tasks such as assembly info generation and error handling. This pattern is documented as unsafe in plugins/dotnet-msbuild/skills/property-patterns/SKILL.md.
When to Use DependsOnTargets, BeforeTargets, and AfterTargets
The MSBuild chain extension pattern differs from other dependency mechanisms. Choose the right approach based on ownership and injection point:
- DependsOnTargets: Define this attribute on the target that requires the dependency. Use it when the target explicitly cannot run without another target completing first.
- BeforeTargets: Define this on the injecting target to execute custom logic immediately before a specific target you do not own.
- AfterTargets: Define this on the injecting target to execute custom logic immediately after a specific target you do not own.
Appending to $(…DependsOn) is preferred when you own the .props or .targets file and want your target to participate in the same logical step as the SDK. Use BeforeTargets or AfterTargets when you cannot modify the original property chain—for example, when consuming third-party SDKs where you do not control the import order.
Complete Example: Extending the Compile Pipeline
The following implementation from dotnet/skills demonstrates safe extension of the compile chain with code generation:
<!-- MyProject.targets -->
<Project>
<PropertyGroup>
<!-- Extend the compile chain – keep SDK logic intact -->
<CompileDependsOn>$(CompileDependsOn);MyCodeGenTarget</CompileDependsOn>
</PropertyGroup>
<Target Name="MyCodeGenTarget"
DependsOnTargets="_ValidateMyFeatureInputs"
Inputs="@(MyFeatureInput)"
Outputs="$(IntermediateOutputPath)MyFeature.generated.cs">
<Exec Command="my-tool.exe -o $(IntermediateOutputPath)MyFeature.generated.cs" />
<ItemGroup>
<Compile Include="$(IntermediateOutputPath)MyFeature.generated.cs" />
<FileWrites Include="$(IntermediateOutputPath)MyFeature.generated.cs" />
</ItemGroup>
</Target>
<Target Name="_ValidateMyFeatureInputs">
<Error Text="MyFeatureInput items are required."
Condition="'@(MyFeatureInput)' == ''" />
</Target>
</Project>
If you cannot modify CompileDependsOn directly, inject a pre-compile step using BeforeTargets:
<Target Name="MyPreCompile" BeforeTargets="CoreCompile">
<!-- Custom pre-compile work executes before CoreCompile -->
</Target>
Summary
- Always append to
$(…DependsOn)properties using the syntax$(PropertyName);YourTargetto preserve SDK functionality. - Never overwrite
BuildDependsOn,CompileDependsOn, orCoreBuildDependsOn, as this removes critical targets and breaks incremental builds. - Use
BeforeTargetsandAfterTargetswhen you cannot modify the property chain directly. - Reference
plugins/dotnet-msbuild/skills/target-authoring/SKILL.mdandplugins/dotnet-msbuild/skills/property-patterns/SKILL.mdfor authoritative guidance on target authoring.
Frequently Asked Questions
What happens if I overwrite CompileDependsOn instead of appending to it?
Overwriting CompileDependsOn replaces the entire semicolon-separated list with your single target, causing MSBuild to skip all SDK-provided compile steps such as CoreCompile and GenerateAssemblyInfo. This results in failed builds, missing assembly attributes, and broken incremental build tracking because the compiler never executes.
When should I use BeforeTargets instead of appending to a DependsOn property?
Use BeforeTargets when you need to inject logic before a specific target but cannot modify the property chain—such as when consuming third-party SDKs or when your custom targets file is imported after the SDK sets the DependsOn properties. BeforeTargets requires no knowledge of the existing chain structure.
Can I append multiple targets to a DependsOn property?
Yes. You can append multiple targets by extending the semicolon-separated list: $(CompileDependsOn);Target1;Target2;Target3. Each target executes in the order listed, after all SDK targets complete.
Where is the official documentation for this pattern?
The official pattern is documented in the dotnet/skills repository at plugins/dotnet-msbuild/skills/target-authoring/SKILL.md and plugins/dotnet-msbuild/skills/property-patterns/SKILL.md, which describe the three-level chain, safe property extension, and the distinction between DependsOnTargets, BeforeTargets, and AfterTargets.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →