Multi-project .NET solutions tend to grow fast. A Web API here, a few class libraries there, some test projects on top and before you know it the same NuGet packages show up with slightly different versions.
That’s where things start to get messy and you end up with inconsistent builds, runtime issues and maintenance headaches.
Central Package Management (CPM) solves this by introducing a single source of truth for all NuGet package versions in your solution.
Central Package Management
Central Package Management (CPM) is a NuGet feature that moves package version definitions out of individual .csproj files and into a shared solution-level file.
Instead of repeating: Version="10.0.7" on every PackageReference, you define versions once and reference packages by name only in each project.
With CPM:
- Versions are defined once per solution
- Projects reference packages without version numbers
- No version differences between projects
- Dependency upgrades become predictable and safe
CPM has been stable since .NET 6 and is widely used in modern .NET solutions. For the full overview, see the official docs on Central Package Management.
Getting Started
To enable Central Package Management, add a Directory.Packages.props file at the root of your solution:
<Project>
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
</Project>
This enables MSBuild to treat package versions as centrally managed.
You can also generate it using the .NET CLI:
dotnet new packagesprops
Example
In a real-world solution, your Directory.Packages.props looks like this:
<Project>
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
<CentralPackageTransitivePinningEnabled>false</CentralPackageTransitivePinningEnabled>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="Kommand" Version="1.0.0-alpha.1" />
<PackageVersion Include="Mapster" Version="10.0.7" />
<PackageVersion Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.7" />
<PackageVersion Include="Microsoft.AspNetCore.OpenApi" Version="10.0.7" />
<PackageVersion Include="Microsoft.EntityFrameworkCore" Version="10.0.7" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.7" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.7" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.7" />
<PackageVersion Include="Microsoft.Extensions.Options" Version="10.0.7" />
<PackageVersion Include="Microsoft.IdentityModel.JsonWebTokens" Version="8.18.0" />
<PackageVersion Include="Microsoft.IdentityModel.Tokens" Version="8.18.0" />
<PackageVersion Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.23.0" />
<PackageVersion Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.1" />
<PackageVersion Include="Swashbuckle.AspNetCore" Version="10.1.7" />
</ItemGroup>
</Project>
Now your .csproj files become significantly cleaner:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Kommand" />
<PackageReference Include="Mapster" />
<PackageReference Include="Microsoft.EntityFrameworkCore" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Domain\Domain.csproj" />
</ItemGroup>
</Project>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Business\Business.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" />
<PackageReference Include="Microsoft.Extensions.Options" />
<PackageReference Include="Microsoft.IdentityModel.JsonWebTokens" />
</ItemGroup>
</Project>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Business\Business.csproj" />
<ProjectReference Include="..\Domain\Domain.csproj" />
</ItemGroup>
</Project>
Once CPM is in place, upgrading dependencies becomes trivial, instead of updating multiple .csproj files, you only change a single line and every project picks it up on the next restore.
Additionally, you can control transitive dependencies better to avoid unexpected version conflicts with the CentralPackageTransitivePinningEnabled property:
<CentralPackageTransitivePinningEnabled>true</CentralPackageTransitivePinningEnabled>
When enabled, NuGet attempts to align transitive dependencies with centrally defined versions and produce more deterministic and predictable builds.
Centralised Package Converter
Adding CPM to a new project is simple. Migrating an existing solution manually is where the real challenge begins.
Manually you would have to collect every unique package id, pick a version when projects disagree and strip versions from each csproj.
Luckily, Centralised Package Converter automates that. It scans your solution, creates or updates Directory.Packages.props and removes version attributes from project files.
Install it as a global .NET tool:
dotnet tool install CentralisedPackageConverter --global
Then run it against your solution folder or a specific project:
central-pkg-converter C:\path\to\your\solution
Use --dry-run first if you want to preview changes without touching files. The tool also supports reverting back to per-project versions when you need to undo an experiment.
NOTE: If two projects reference the same package with different versions, the converter has to pick one. Resolve those conflicts before or right after migration so you are not surprised at build time.
Conclusion
Central Package Management is a small structural change that pays off heavily in any solution with multiple projects and shared dependencies.
A single Directory.Packages.props file keeps versions consistent, makes upgrades faster and keeps individual csproj files focused on what each project actually uses.
For new projects you can add the file from day one. For existing codebases the Centralised Package Converter gets you most of the way there in one command.
If you want to check out examples I created, you can find the source code here:
Source CodeI hope you enjoyed it, subscribe and get a notification when a new blog is up!
