Programming

Why NuGet Restore Ignores Directory.Build.props Properties

Learn why NuGet restore skips properties from Directory.Build.props during partial MSBuild evaluation, the Directory.Build.targets workaround with PackageReference Update, and best practices like Central Package Management for nuget package versioning.

6 answers 1 view

Why doesn’t NuGet tooling support setting default PackageReference versions via properties in Directory.Build.props, preferring Directory.Build.targets with Update instead?

Naive Approach (Not Working)

Define a property in Directory.Build.props:

xml
<Project>
 <PropertyGroup>
 <SkiaSharpVersion>1.68.1</SkiaSharpVersion>
 </PropertyGroup>
</Project>

Then reference it in .csproj:

xml
<ItemGroup>
 <PackageReference Include="SkiaSharp" Version="$(SkiaSharpVersion)" />
</ItemGroup>

Working Workaround

Use Directory.Build.targets:

xml
<Project>
 <ItemGroup>
 <PackageReference Update="SkiaSharp" Version="1.68.1" />
 </ItemGroup>
</Project>

And simplify .csproj to:

xml
<ItemGroup>
 <PackageReference Include="SkiaSharp" />
</ItemGroup>

What is the reason NuGet ignores properties from Directory.Build.props during package restore? How does NuGet tooling interact with MSBuild evaluation phases, and are there recommended best practices for centralizing package versions across solutions?

NuGet restore ignores properties defined in Directory.Build.props like <SkiaSharpVersion>1.68.1</SkiaSharpVersion> because it performs a partial MSBuild evaluation that skips importing those files to prevent cycles from package props and targets. This differs from full builds where everything loads up. The reliable workaround uses Directory.Build.targets with <PackageReference Update="SkiaSharp" Version="1.68.1" />, but the modern best practice is Central Package Management via Directory.Packages.props for cleaner nuget package versioning across solutions.


Contents


Why NuGet Restore Ignores Properties from Directory.Build.props

Ever tried centralizing a package version in Directory.Build.props only to watch nuget restore or dotnet restore shrug it off? You’re not alone. The naive approach—defining <SkiaSharpVersion>1.68.1</SkiaSharpVersion> in props and referencing Version="$(SkiaSharpVersion)" in your .csproj—fails during restore because NuGet doesn’t fully evaluate MSBuild properties at that stage.

Here’s the rub: nuget restore parses only the core project file (your .csproj) and its direct imports, skipping solution-level files like Directory.Build.props. Why? To avoid infinite loops. Packages often ship their own .props and .targets that get auto-imported, and loading everything upfront could trigger recursive evaluation nightmares. During restore, NuGet focuses on resolving PackageReference items to generate a lock file or assets, treating properties as unavailable.

In practice, this means your $(SkiaSharpVersion) resolves to empty, forcing fallback to whatever’s hardcoded or default. Full msbuild or dotnet build works fine since it imports Directory.Build.props early. But restore? Nope. This quirk trips up large repos aiming for centralized nuget package management.


NuGet and MSBuild Evaluation Phases Explained

NuGet tooling leans heavily on MSBuild under the hood, but with a twist: distinct evaluation phases for restore versus build. Think of it like pre-flight checks versus takeoff.

Standard MSBuild flow imports files in this order:

  1. Auto-imports (SDK props).
  2. Directory.Build.props.
  3. Project file.
  4. Directory.Build.targets.
  5. Auto-imports (SDK targets).

NuGet restore, however, kicks off a partial evaluation. It uses separate global properties and skips Directory.Build.props entirely. The NuGet MSBuild targets documentation spells it out: restore targets like Restore run in isolation, evaluating just enough to resolve dependencies without full project graph loading. This prevents cycles when a package’s props try importing more props.

What about dotnet restore? Same deal—it’s NuGet-powered and mirrors nuget restore’s partial mode. MSBuild’s /restore flag gets closer to full evaluation but still isn’t identical. Result? Properties from props files stay invisible during the phase that matters most for PackageReference resolution.

Picture a solution with 50 projects. You want one version truth. Props seems perfect, but restore says no. Targets? That’s where it clicks post-restore.


The Working Workaround: Directory.Build.targets with PackageReference Update

Since props won’t fly for restore, flip to Directory.Build.targets. Drop this in:

xml
<Project>
 <ItemGroup>
 <PackageReference Update="SkiaSharp" Version="1.68.1" />
 <PackageReference Update="SkiaSharp.NativeAssets.Linux" Version="1.68.1" />
 </ItemGroup>
</Project>

Your .csproj simplifies to:

xml
<ItemGroup>
 <PackageReference Include="SkiaSharp" />
</ItemGroup>

Why does this work? Directory.Build.targets imports after restore, during full evaluation. The Update attribute on PackageReference overrides versions post-resolution, injecting them into the project graph without altering the restore step. NuGet resolves the include first (maybe with a project-local version or transitive), then MSBuild patches it.

It’s battle-tested for msbuild targets scenarios. But watch for gotchas: if a project omits the include entirely, Update skips it. And transitive deps might conflict. Still, for forcing consistency across a solution, it’s the go-to hack.


Common Issues and GitHub-Reported Bugs in NuGet Restore

This isn’t just theory—it’s a persistent pain point. GitHub’s NuGet repo lights up with complaints. Take issue #12442: properties like $(PureConfiguration) in props vanish during dotnet restore, breaking conditional includes. Workaround? Duplicate props in every project. Ouch.

Or issue #6734: nuget.exe restore ignores Directory.Build.props and .targets, nuking transitive packages over ProjectReferences. MSBuild /restore fares better, but inconsistencies abound. These bugs highlight nuget restore’s limitations with msbuild targets files, especially in monorepos.

Stack Overflow echoes it too. Folks trying centralized versions hit walls because the SDK expects versionless refs during restore, erroring on props-defined ones. Symptoms? Empty versions, failed restores, or exploded package graphs. Pro tip: Always test with dotnet restore --verbosity detailed to spot ignored props.


Best Practice: Central Package Management with Directory.Packages.props

Ditch the hacks. Enter NuGet’s Central Package Management (CPM), rolled out in NuGet 6.2+. Create Directory.Packages.props:

xml
<Project>
 <PropertyGroup>
 <ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
 </PropertyGroup>
 <ItemGroup>
 <PackageVersion Include="SkiaSharp" Version="1.68.1" />
 <PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="1.68.1" />
 </ItemGroup>
</Project>

Projects just say:

xml
<PackageReference Include="SkiaSharp" />

CPM scans up the directory tree for the nearest Directory.Packages.props, resolving versions centrally. Restore sees this because it’s baked into NuGet’s partial evaluation—no props skipping issues. It overrides per-project versions too, perfect for solution-wide control.

The official CPM docs confirm: hierarchical, deterministic, and restore-friendly. Supports packages.lock.json for pinning. Migrate gradually—set the flag, add versions, remove locals.


Visual Studio NuGet Integration and Transitive Dependencies

Visual Studio’s NuGet Package Manager? It calls nuget restore or dotnet restore underneath, inheriting the same phase quirks. Solution right-click → Restore NuGet Packages triggers partial eval, so props properties ghost it. But VS 2022+ with CPM shines—intellisense picks up central versions seamlessly.

Transitive dependencies add spice. Your central SkiaSharp pulls natives transitively. With Update in targets, it overrides fine. CPM handles it natively, trimming duplicates. Issue? Private assets or exclusions—test with dotnet list package --include-transitive.

In team flows, push Directory.Packages.props to source control. VS users get auto-resolution; CLI folks too. One caveat: older NuGet clients need updates.


Centralizing Package Versions Across Solutions: Pros, Cons, and Alternatives

Centralization rocks for maintainability—one bump, everywhere updates. Pros of CPM: restore-safe, hierarchical (per-solution or repo), lockfile support. Targets workaround? Quick, no tooling changes, but brittle on missing includes.

Cons? CPM needs NuGet 6+, MSBuild 17+. Targets can bloat graphs if overused. Alternatives:

  • Global.json with sdk versions (not packages).
  • PackageReference in a shared props file (import manually).
  • Git submodules for shared projects (heavy).

For massive orgs, consider MyGet or Azure Artifacts feeds with version policies. But for most? CPM wins. Weigh your stack—legacy? Stick to targets. Modern .NET? Go central.


Sources

  1. NuGet MSBuild Targets — Explains partial evaluation during nuget restore and property handling: https://learn.microsoft.com/en-us/nuget/reference/msbuild-targets
  2. NuGet/Home Issue #12442 — Details Directory.Build.props ignored in dotnet restore: https://github.com/NuGet/Home/issues/12442
  3. NuGet/Home Issue #6734 — Covers nuget restore skipping props/targets and transitive issues: https://github.com/NuGet/Home/issues/6734
  4. Stack Overflow: Setting NuGet Package Versions Centrally — Discusses props vs targets for packagereference versioning: https://stackoverflow.com/questions/63918167/setting-nuget-package-versions-centrally-for-a-solution-directory-build-props
  5. NuGet Central Package Management — Official guide to Directory.Packages.props and centralized versions: https://learn.microsoft.com/en-us/nuget/consume-packages/central-package-management

Conclusion

NuGet’s restore phase skips Directory.Build.props to dodge evaluation cycles, but Directory.Build.targets with Update bridges the gap reliably. For the future-proof path, embrace Central Package Management—it’s designed for this, slashing duplication across solutions. Test in your setup, update NuGet, and reclaim sanity from version hell. Your builds will thank you.

Microsoft Learn / Documentation Portal

NuGet restore performs a partial MSBuild evaluation distinct from full build, ignoring properties from Directory.Build.props due to separate global properties for restore vs. build phases. This design prevents cycles when packages import their own .props and .targets files. During nuget restore or dotnet restore, only core .csproj PackageReference items are evaluated, making MSBuild properties like versions unavailable. Use Directory.Build.targets with <PackageReference Update> post-restore instead for centralized versioning.

@stgerbb / Developer

During dotnet restore or nuget restore, properties from Directory.Build.props like $(PureConfiguration) are not evaluated, causing failures in PackageReference Include="$(PureConfiguration).Test". Only the .csproj file is parsed during this phase, requiring property duplication as a workaround. This is a known NuGet restore limitation, with the GitHub issue marked as “not planned.” Prefer Directory.Build.targets with PackageReference Update for central version management across projects.

Andrew Arnott / Software Developer

nuget.exe restore skips importing Directory.Build.props or .targets, breaking transitive packages over ProjectReferences when using centralized <PackageReference Update="pkgid" Version="1.2.3" /> in MSBuild targets files. MSBuild /restore succeeds with fuller evaluation. This impacts NuGet package management in large repos; recommend dotnet restore or MSBuild integration for MSBuild targets compatibility.

A

Directory.Build.props with PackageReference Include and Version adds packages to every project unwantedly, unlike Central Package Versions (CPV) which centralizes versions without forcing includes. The SDK evaluates after Directory.Build.props, potentially erroring on versioned PackageReference items expecting versionless ones. Use Directory.Build.targets or CPV for NuGet package versioning control.

Microsoft Learn / Documentation Portal

NuGet’s Central Package Management uses Directory.Packages.props with <ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally> and <PackageVersion> items, ignoring Directory.Build.props as it’s imported post-restore. Projects reference versionless <PackageReference Include="..."/>, resolving from the nearest Directory.Packages.props. This provides a deterministic approach for NuGet restore across MSBuild projects, with per-project overrides possible.

Authors
@stgerbb / Developer
Developer
Andrew Arnott / Software Developer
Software Developer
A
Developer
Sources
Microsoft Learn / Documentation Portal
Documentation Portal
Verified by moderation
NeuroAnswers
Moderation
Why NuGet Restore Ignores Directory.Build.props Properties