In a solution with many projects, NuGet package versions end up scattered across every .csproj file. Sooner or later 2 projects reference different versions of the same package, and you start seeing version drift, NU1605 package downgrade errors, and runtime binding failures. Upgrading a package means hunting through dozens of files and hoping you didn't miss one.
Central Package Management (CPM) fixes this by declaring every package version once, in a single Directory.Packages.props file.
CPM is a NuGet feature (available since NuGet 6.2 / .NET SDK 6.0.300 / Visual Studio 2022 17.2) that moves all package versions out of your project files and into one Directory.Packages.props file at the root of your repository:
<PackageReference>Directory.Packages.props declares what version every package resolves toNU1008) if a project tries to specify its own versionThis works the same way as Directory.Build.props - it applies automatically to every project below it in the directory tree.
Directory.Packages.props:
<Project><PropertyGroup><ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally></PropertyGroup><ItemGroup><PackageVersion Include="Microsoft.EntityFrameworkCore" Version="10.0.0" /><PackageVersion Include="Serilog.AspNetCore" Version="10.0.0" /><PackageVersion Include="xunit" Version="2.9.2" /></ItemGroup></Project>
Then remove the Version attributes from your project files:
<ItemGroup><PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.1" /><PackageReference Include="Serilog.AspNetCore" Version="8.0.0" /></ItemGroup>
❌ Bad example - Versions repeated (and already drifting) in every csproj
<ItemGroup><PackageReference Include="Microsoft.EntityFrameworkCore" /><PackageReference Include="Serilog.AspNetCore" /></ItemGroup>
✅ Good example - Projects declare dependencies, Directory.Packages.props owns the versions
If a single project genuinely needs a different version (e.g. during a migration), use VersionOverride rather than abandoning CPM:
<PackageReference Include="Serilog.AspNetCore" VersionOverride="9.0.0" />
Use this sparingly - every override is a little piece of version drift you've chosen to keep.
Packages that every project should consume (analyzers, source linking, banned API checks) can be declared once as a GlobalPackageReference:
<ItemGroup><GlobalPackageReference Include="Microsoft.SourceLink.GitHub" Version="8.0.0" /></ItemGroup>
CPM can also pin transitive dependencies - packages you don't reference directly - which is useful for forcing patched versions of vulnerable transitive packages:
<PropertyGroup><CentralPackageTransitivePinningEnabled>true</CentralPackageTransitivePinningEnabled></PropertyGroup>
For small solutions, move the versions by hand. For anything bigger, use the community CentralisedPackageConverter tool:
dotnet tool install -g CentralisedPackageConvertercentral-pkg-converter .
If you are planning a .NET migration, consolidating package versions with CPM before you start makes the upgrade dramatically easier - see Do you have a .NET migration plan?.