Why NuGet Ignores Directory.Build.props in Package Restore
Understand why NuGet restore skips MSBuild properties from Directory.Build.props, favoring Directory.Build.targets updates. Learn NuGet-MSBuild phases and best practices like Central Package Management for packagereference versions.
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:
<Project>
<PropertyGroup>
<SkiaSharpVersion>1.68.1</SkiaSharpVersion>
</PropertyGroup>
</Project>
Then reference it in .csproj:
<ItemGroup>
<PackageReference Include="SkiaSharp" Version="$(SkiaSharpVersion)" />
</ItemGroup>
Working Workaround
Use Directory.Build.targets:
<Project>
<ItemGroup>
<PackageReference Update="SkiaSharp" Version="1.68.1" />
</ItemGroup>
</Project>
And simplify .csproj to:
<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 tooling skips properties like <SkiaSharpVersion>1.68.1</SkiaSharpVersion> from Directory.Build.props during package restore because this phase uses a stripped-down MSBuild evaluation that loads only essential project files—props come too late. Instead, it resolves PackageReference Version="$(SkiaSharpVersion)" statically, treating unresolved properties as literals or errors, which is why the Directory.Build.targets workaround with <PackageReference Update="SkiaSharp" Version="1.68.1" /> succeeds: targets evaluate after restore needs the info. For centralizing package versions across solutions, the modern best practice is Central Package Management via Directory.Packages.props, letting projects use versionless <PackageReference Include="SkiaSharp" />.
Contents
- Why NuGet Ignores MSBuild Properties from Directory.Build.props During Package Restore
- NuGet Restore Process and MSBuild Evaluation Phases Explained
- Directory.Build.props vs. Directory.Build.targets: NuGet Targets Workaround
- Centralizing NuGet Package Versions with Directory.Packages.props
- MSBuild NuGet Integration: Packagereference Best Practices
- Common Errors and Troubleshooting NuGet Restore Issues
- Visual Studio and CLI Support for Centralized Management
- Sources
- Conclusion
Why NuGet Ignores MSBuild Properties from Directory.Build.props During Package Restore
Picture this: you’ve set up a clean property in Directory.Build.props, referenced it neatly in your .csproj as Version="$(SkiaSharpVersion)", and run dotnet restore. Nothing. The package restore chugs along, oblivious, pulling whatever default or failing outright. Why?
It boils down to timing in the MSBuild NuGet dance. NuGet restore doesn’t fire up the full MSBuild engine. Instead, it spins up a lightweight “restore graph” evaluation—think of it as MSBuild on a diet, skipping the buffet of props files like Directory.Build.props. This file imports early in the build process, sure, but restore happens before that full import sequence kicks in. Properties defined there? Invisible to nuget restore.
Your naive approach fails because $(SkiaSharpVersion) resolves to a literal string during this static phase, not the value “1.68.1”. No dynamic evaluation, no magic. As Filip W. explains in his Strathweb post, NuGet needs concrete versions upfront to build the dependency graph—properties are promises for later, not now.
And yeah, it stings when you’re aiming for solution-wide consistency. But this isn’t a bug; it’s by design for speed and determinism in CI/CD pipelines.
NuGet Restore Process and MSBuild Evaluation Phases Explained
Nuget restore isn’t just “grab packages.” It’s a multi-stage ritual tied tightly to MSBuild phases. Let’s break it down—no fluff.
First, dotnet restore (or nuget restore) launches MSBuild in “restore” mode. This evaluates a minimal project graph: only the .csproj, SDK props/targets, and basics. No Directory.Build.props. Why? Restore must compute the exact dependency closure without side effects like custom tasks or conditionals that could vary by machine.
Key phases:
- Restore evaluation: Static graph (MSBuild 16.6+). Items like
PackageReferenceget collected, butVersion="$(Prop)"stays unresolved. MSBuild NuGet here builds assets forproject.assets.json. - Build evaluation: Full load. Now
Directory.Build.propsimports, properties resolve, but restore’s done—too late for package resolution.
Contrast with build: dotnet build loads everything, so properties work fine post-restore. But if restore failed, you’re stuck.
Microsoft’s PackageReference docs note this static approach ensures reproducible restores, especially with packages.lock.json. Dynamic props? They’d break that.
Quick test? Add a bogus prop value—restore logs “unresolved” or skips. Real-world pain in monorepos.
Directory.Build.props vs. Directory.Build.targets: NuGet Targets Workaround
So props flop. Enter Directory.Build.targets. Your workaround shines here.
Directory.Build.props imports in the initial props phase—pre-restore. Directory.Build.targets? Imports at the end, during targets phase, which restore does peek into for item updates.
<!-- Directory.Build.targets -->
<Project>
<ItemGroup>
<PackageReference Update="SkiaSharp" Version="1.68.1" />
</ItemGroup>
</Project>
In .csproj:
<PackageReference Include="SkiaSharp" />
The Update metadata overrides Include items post-initial evaluation. Restore sees the version because targets contribute to the final item list used for the graph. Sneaky, effective.
But caveats: unconditional Update hits every project. Stack Overflow threads like this one from Ayushmati warn it can duplicate refs or clash with explicit versions. Fine for shared libs like SkiaSharp, risky otherwise.
Props for props: great for compile defines, analyzers. Targets for late tweaks like nuget targets. Horses for courses.
Centralizing NuGet Package Versions with Directory.Packages.props
Ditch workarounds. Enter Central Package Management (CPM)—NuGet’s official fix for your exact itch.
Create Directory.Packages.props at solution root:
<Project>
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="SkiaSharp" Version="1.68.1" />
</ItemGroup>
</Project>
Projects simplify to:
<PackageReference Include="SkiaSharp" />
Restore pulls versions from central file. No props ignored, no updates needed. The NuGet GitHub wiki (from Anand Gaurav and Cristina Manu) calls this the future—supports lock files, transitive deps.
Why better? Versionless refs avoid duplication. Update once, restore everywhere. SDK-style only, .NET 6+ ideal.
Migration tip: dotnet add package auto-versionless with CPM enabled.
MSBuild NuGet Integration: Packagereference Best Practices
Packagereference version management thrives on these MSBuild NuGet patterns:
- CPM first: As above. Scales to hundreds of projects.
- Targets for legacy: Your
Updatehack—use sparingly. - Conditionals:
<PackageReference Include="SkiaSharp" Version="1.68.1" Condition="'$(Configuration)' == 'Release'" />. - Lock files:
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>for determinism. - Multi-targeting: Central versions respect
<TargetFrameworks>.
Avoid global props for versions—embrace CPM. Microsoft’s CPM guide stresses this for enterprise.
Pro tip: Git submodules or shared props for multi-repo? CPM still wins.
Common Errors and Troubleshooting NuGet Restore Issues
Restore bombing with props? Symptoms:
- “Package SkiaSharp 1.68.1 is not compatible” (unresolved literal).
- Duplicate refs post-targets.
- CLI fails, VS succeeds (UI caches differently).
Fixes:
dotnet nuget locals all --clear—nuke caches.- Check import order: Solution-level files first.
- Logs:
dotnet restore -v detailed. - Verify no
PrivateAssetsclashes.
If CPM: Ensure <ManagePackageVersionsCentrally> propagates (inherits automatically).
Stumped? MSBuild graph viz tools like msbuild /pp dump evaluated files.
Visual Studio and CLI Support for Centralized Management
Visual Studio 17.4+? NuGet UI respects CPM—add packages versionlessly, edit in Directory.Packages.props.
CLI: dotnet add SkiaSharp (auto-central if enabled). dotnet remove too. nuget restore lags slightly—prefer dotnet.
Cross-platform? All good. VS Code with C# extension mirrors.
Limitations: PackageReference-only, no packages.config.
Sources
- Strathweb: Solution-wide NuGet package version handling — Explains MSBuild phases and Directory.Build.targets workaround: https://www.strathweb.com/2018/07/solution-wide-nuget-package-version-handling-with-msbuild-15/
- NuGet GitHub Wiki: Centrally managing NuGet package versions — Official guide to CPM with Directory.Packages.props: https://github.com/NuGet/Home/wiki/Centrally-managing-NuGet-package-versions
- Stack Overflow: Setting NuGet package versions centrally — Discusses props pitfalls and targets updates: https://stackoverflow.com/questions/63918167/setting-nuget-package-versions-centrally-for-a-solution-directory-build-props
- Microsoft Learn: Package references in project files — Details NuGet restore static evaluation: https://learn.microsoft.com/en-us/nuget/consume-packages/package-references-in-project-files
- Microsoft Learn: Central Package Management — Best practices for version centralization: https://learn.microsoft.com/en-us/nuget/consume-packages/central-package-management
Conclusion
NuGet’s restore phase skips Directory.Build.props properties to keep things fast and reproducible, forcing workarounds like targets Update—but that’s yesterday’s news. Go with Central Package Management in Directory.Packages.props for true solution-wide packagereference version control: simpler .csprojs, one-stop updates, full tool support. You’ll save hours debugging restores, and your team will thank you. Start small—migrate one solution, watch the magic.

NuGet restore uses static graph evaluation (MSBuild 16.6+), which performs a minimal MSBuild evaluation before full project properties like those in Directory.Build.props are loaded. This means dynamic MSBuild properties such as $(SkiaSharpVersion) are unavailable during nuget restore, causing the PackageReference Version to be unresolved. Instead, use item updates in Directory.Build.targets or modern Central Package Management (CPM) for centralized versions.
NuGet recommends Central Package Management (CPM) via Directory.Packages.props for centralizing NuGet package versions. Define <PackageVersion Include="SkiaSharp" Version="1.68.1" /> centrally, then use versionless <PackageReference Include="SkiaSharp" /> in projects with <ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>. This works with dotnet nuget add/remove, Visual Studio UI, and <RestorePackagesWithLockFile>true</RestorePackagesWithLockFile> for SDK-style projects only, avoiding Directory.Build.props property issues during restore.
Directory.Build.props with properties like <SkiaSharpVersion>1.68.1</SkiaSharpVersion> and <PackageReference Include="SkiaSharp" Version="$(SkiaSharpVersion)" /> conflicts with Central Package Versions (CPV) by unconditionally adding references to every project, causing duplicates or errors post-SDK evaluation during NuGet restore. Use Directory.Build.targets with <PackageReference Update="SkiaSharp" Version="1.68.1" /> for selective updates, or prefer MSBuild SDKs like MSBuildSdks.CentralPackageVersions.
NuGet restore runs a minimal MSBuild evaluation before Directory.Build.props properties load, ignoring $(SkiaSharpVersion) in PackageReference Version. The working workaround uses Directory.Build.targets with <PackageReference Update="SkiaSharp" Version="1.68.1" />, as items are modified during the later NuGet build phase visible to restore. This centralizes versions across solutions but lacks full Visual Studio NuGet UI support.

Modern Central Package Management (CPM) in Directory.Packages.props resolves NuGet package version centralization issues by allowing projects to declare <PackageReference Include="SkiaSharp" /> without Version; restore resolves from central <PackageVersion Include="SkiaSharp" Version="1.68.1" />. This bypasses Directory.Build.props timing problems entirely. Supports SDK-style projects with dotnet restore and Visual Studio.

