Guides work on the SDK-MSBuild integration boundary. Consult when authoring or modifying SDK targets, working on dotnet CLI to MSBuild invocation, handling project-reference protocol, coordinating cross-repo changes with dotnet/sdk, debugging property resolution or import ordering, working on restore/build/publish/pack target chains, or dealing with Directory.Build.props/targets interaction.
MSBuild operates as a component within the .NET SDK. This boundary is the most complex integration point in the .NET build stack, spanning MSBuild (engine), SDK (target implementations), NuGet (restore), and Roslyn (compilation).
Understanding MSBuild's evaluation order is critical for SDK target authoring:
1. Environment variables
2. Global properties (from CLI: -p:Foo=Bar)
3. Project-level properties (file order, with imports):
┌─ Sdk.props (SDK defaults)
├─ Directory.Build.props (user overrides BEFORE project)
├─ <Project> properties (the .csproj itself)
├─ Directory.Build.targets (user overrides AFTER project)
└─ Sdk.targets (SDK target definitions)
4. Item definitions
5. Items (including SDK default globs)
Directory.Build.props is imported from Microsoft.Common.props as an early user extension point after core defaults are computed — use it for solution-wide customizationCondition="'$(Prop)' == ''"<!-- CORRECT: SDK default that respects user override -->
<OutputType Condition="'$(OutputType)' == ''">Library</OutputType>
<!-- WRONG: Unconditional set clobbers user's .csproj -->
<OutputType>Library</OutputType>
Restore and Build must never run in the same evaluation. The restore phase generates .g.props and .g.targets files that must be imported during evaluation — but they don't exist until restore completes.
dotnet build implicitly runs restore then build as separate invocationsdotnet build --no-restore skips restore, assuming it already happened/t:Restore;Build) is a known anti-pattern that causes intermittent failuresThe project-reference protocol spans MSBuild, SDK, and NuGet. It is the most complex integration boundary.
_GetProjectReferenceTargetFrameworkProperties to determine inner build parametersTargetFramework (singular) for each referenced projectGetTargetPath returns the output assembly for the referencing project to consume<TargetFrameworks>) dispatch multiple inner buildsSetTargetFramework is how the outer build communicates the chosen framework to inner buildsSDK provides well-known extension points for targets:
| Extension Point | Use For |
|---|---|
$(BuildDependsOn) | Adding to the Build chain |
$(CompileDependsOn) | Pre-compilation steps |
$(PublishDependsOn) | Publish pipeline additions |
$(PackDependsOn) | NuGet pack pipeline additions |
BeforeTargets="Build" | Use sparingly; prefer DependsOnTargets |
DependsOnTargets for required predecessors — it's explicit and predictableBeforeTargets/AfterTargets should be used sparingly — they create implicit ordering that's hard to debugInputs and Outputs — incorrect declarations cause either rebuild-every-time or stale-output bugsTargetFramework in the inner buildChanges that touch the MSBuild-SDK boundary often require coordinated PRs:
Visual Studio uses design-time builds with different target contracts:
ResolveProjectReferences but not Build$(DesignTimeBuild)=true and $(BuildingProject)=false| Symptom | Likely Cause |
|---|---|
| Property has wrong value | Import ordering — check if SDK prop overrides user setting |
| Target runs in wrong order | Missing DependsOnTargets declaration |
| Build works, restore fails | Evaluation-time dependency on restore-generated files |
| Works single-target, fails multi-target | Target assumes single $(TargetFramework) |
| CLI build works, VS build fails | Design-time build target contract violation |