Stopping Version Drift with NuGet Central Package Management
Stop fighting version drift in multi-project .NET solutions. Learn how to use NuGet Central Package Management (CPM) to define all dependency versions in one place.
01 Aug 2025, 22:28 UTC

The Dependency Drift Problem
In a multi-project .NET solution, it is common for different projects to accidentally diverge on dependency versions. Project A might use Newtonsoft.Json 13.0.1, while Project B uses 13.0.3. This "version drift" often goes unnoticed until a runtime error occurs due to a binary incompatibility or a security vulnerability is patched in one project but missed in another.
The traditional fix is to manually sync .csproj files, but this is error-prone and tedious. The solution is Central Package Management (CPM), a feature introduced in NuGet 6.2 (and .NET SDK 6.0.300) that moves version definitions out of individual project files and into a single, central configuration file.
How CPM Restructures Dependencies
CPM shifts the responsibility of versioning from the project level to the repository level. Instead of every project declaring Version="x.y.z", you create a file named Directory.Packages.props at your root directory. This file acts as the single source of truth for every package used across the entire solution.
When CPM is enabled, the NuGet restore process ignores the Version attribute inside .csproj files and instead looks up the package ID in the central props file. If a project requests a package that isn't defined centrally, the build fails, ensuring that no "shadow dependencies" creep into your codebase.
Implementing Central Package Management
To implement CPM, you must first enable it and then migrate your version declarations. This requires SDK-style projects; legacy packages.config projects are not supported.
1. Create the Central Configuration
Create a file named Directory.Packages.props in your root folder with the following structure:
<Project>
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="Serilog" Version="3.1.1" />
<PackageVersion Include="Newtonsoft.Json" Version="13.0.3" />
</ItemGroup>
</Project>
2. Clean the Project Files
Remove the Version attribute from all PackageReference items in your .csproj files. They should now look like this:
<ItemGroup>
<PackageReference Include="Serilog" />
<PackageReference Include="Newtonsoft.Json" />
</ItemGroup>
Handling Exceptions and Transitives
While consistency is the goal, real-world migrations often require temporary exceptions. If Project C absolutely requires an older version of a library to maintain compatibility with a legacy API, you can use VersionOverride:
<PackageReference Include="Newtonsoft.Json" VersionOverride="12.0.1" />
Warning: Overuse of VersionOverride reintroduces the drift CPM is designed to prevent. Use this only for temporary migration windows.
To further harden your build, you can enable CentralPackageTransitivePinningEnabled in the PropertyGroup. This allows you to use PackageVersion to force a specific version of a transitive dependency (a dependency of your dependency), preventing "surprise" upgrades when a direct dependency updates its own requirements.
Verification and Diagnostics
To verify that CPM is active and enforcing versions, perform these checks using the .NET CLI from the solution root:
- Enforcement Check: Add a
Version="1.0.0"attribute back into a.csprojfile and rundotnet restore. The build should fail with error NU1008, confirming that CPM is blocking inline versions. - Consistency Check: Run
dotnet list package. Every project using the same package should report the exact same version. - Update Check: Change a version in
Directory.Packages.propsand rundotnet list package --outdatedto see how the change propagated across all projects.
Trade-offs and Limitations
| Benefit | Limitation/Risk |
|---|---|
| Single point of update for security patches. | All projects must upgrade simultaneously; no partial rollouts without overrides. |
| Eliminates diamond-dependency conflicts. | Initial migration requires removing versions from every single project file. |
| Simplified Dependabot/Renovate PRs. | Nested solutions may require complex Directory.Build.props imports. |
Closing Action
If your solution has more than three projects, the manual overhead of syncing versions is a technical debt. Move your versions to Directory.Packages.props, remove inline versions, and use dotnet restore to validate the enforcement. This transforms dependency management from a manual auditing task into a configuration-driven process.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.