Central Package Management in .NET: Simplify NuGet Dependencies
Learn Central Package Management (CPM) in .NET to manage all NuGet versions in one Directory.Packages.props file. Simple guide with diagrams and examples.
A shared shopping list for the whole family
Imagine a big joint family living in one house. Everybody buys groceries. The eldest brother buys rice, the younger sister buys rice, and the cousin upstairs also buys rice. Each person goes to a different shop and pays a slightly different price. Soon the kitchen has five bags of rice, three of them are different brands, and nobody knows which one to use.
Now imagine the family fixes this. They keep one shopping list on the fridge. The list says: "Rice — Brand A, 5 kg." Everyone reads the same list. Nobody guesses. If the family wants to switch to a cheaper brand, they change one line on that list, and the whole house follows.
Central Package Management (CPM) in .NET does exactly this for your NuGet packages. Instead of each project picking its own version of a package, you keep one list at the top of your repository. Every project reads that list. Change one line, and every project updates together.
In this guide we will go slow and use small, clear examples. By the end you will know what CPM is, why it saves you from version headaches, and how to switch your solution over in a few minutes.
The problem before CPM
A .NET solution often has many projects. A web project, a core library, a tests project, maybe a worker service. Each project has its own .csproj file. And in the old way, each .csproj lists its own package versions.
Here is what a normal project file looked like:
<!-- MyApp.Web.csproj -->
<ItemGroup>
<PackageReference Include="Serilog" Version="3.1.1" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="FluentValidation" Version="11.9.0" />
</ItemGroup>That looks fine for one project. The trouble starts when you have ten projects. Now the same package, say Newtonsoft.Json, is listed in many files. One project says 13.0.1, another says 13.0.3, a third forgot to update and is still on 12.0.2.
This mismatch is called version drift. It causes real bugs that are hard to find. Two projects in the same app load two different versions of the same library, and suddenly things break at runtime for no obvious reason.
To upgrade Serilog everywhere, you had to open every project file, find the line, and edit it by hand. Miss one, and the drift comes back. On a big solution this is slow and easy to get wrong.
The CPM idea in one picture
CPM moves all the versions into one file. That file is called Directory.Packages.props and it sits at the root of your repository. Each project keeps its <PackageReference> lines, but those lines no longer carry a version. The version lives only in the central file.
How CPM splits the job
Steps
Project files
List package names only, no versions
Central file
Directory.Packages.props sets every version
Restore
NuGet joins name + version at build time
So the project file answers "which packages do I need," and the central file answers "what version does the whole repo use." Two clear jobs, two clear places.
Setting it up step by step
Let us turn on CPM for a solution. It takes three small steps.
Step 1: Create the central file
Create a file named Directory.Packages.props at the root of your repo, next to your .sln file. The name and spelling matter, so copy it carefully.
You can create it by hand, or let the CLI do it for you:
dotnet new packagespropsInside the file, set one switch and then list your versions:
<Project>
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="Serilog" Version="3.1.1" />
<PackageVersion Include="Newtonsoft.Json" Version="13.0.3" />
<PackageVersion Include="FluentValidation" Version="11.9.0" />
<PackageVersion Include="xunit" Version="2.7.0" />
</ItemGroup>
</Project>Notice the tag is <PackageVersion>, not <PackageReference>. The ManagePackageVersionsCentrally switch set to true is what turns the whole feature on.
Step 2: Remove versions from each project
Now open each .csproj and delete the Version from every package line. Keep the Include part. That is all.
<!-- MyApp.Web.csproj -->
<ItemGroup>
<PackageReference Include="Serilog" />
<PackageReference Include="Newtonsoft.Json" />
<PackageReference Include="FluentValidation" />
</ItemGroup>The project still says it needs these three packages. But it no longer says which version. NuGet will read the version from the central file.
Step 3: Restore and build
Run a restore and build as usual:
dotnet restore
dotnet buildNuGet reads Directory.Packages.props, matches each package name to its central version, and pulls in the right files. If you left a Version on a PackageReference while CPM is on, NuGet warns you, because that version is now ignored or causes an error. The fix is simple: remove it.
What changed, side by side
Here is the same package in both styles, so you can see the move clearly.
| Topic | Old way (per project) | CPM way (central) |
|---|---|---|
| Where versions live | In every .csproj | In one Directory.Packages.props |
| Tag used for version | PackageReference ... Version= | PackageVersion ... Version= |
| Project reference | Name and version | Name only |
| Upgrade a package | Edit every file by hand | Edit one line, once |
| Risk of version drift | High | Almost none |
The biggest win is the upgrade row. To move Serilog from 3.1.1 to 4.0.0 across twenty projects, you now change exactly one line in the central file. Every project follows on the next restore.
How NuGet decides the version
It helps to picture what NuGet does during restore. For each <PackageReference> it finds, it looks up the matching <PackageVersion> in the central file and uses that version. The flow is the same for every project.
Because every project asks the same central file, every project gets the same answer. That is how CPM kills version drift at the source.
Overriding a version for one project
Sometimes one project genuinely needs a different version. Maybe a test project needs an older library to reproduce a bug, or one service is not ready for a newer release. CPM allows this through VersionOverride.
<!-- This one project pins its own version -->
<ItemGroup>
<PackageReference Include="Serilog" VersionOverride="3.0.1" />
</ItemGroup>Now this single project uses 3.0.1, while every other project still uses the central 3.1.1. The override wins only inside the file where you write it.
Use this carefully. If half your projects use overrides, you are back to the old mess. Treat VersionOverride as a rare, short-term escape hatch, not a habit. A good rule: every override should have a comment saying why it exists and when it can be removed.
Transitive pinning: control the packages you did not ask for
Every package you install can pull in other packages behind the scenes. These hidden helpers are called transitive dependencies. You never named them, but they come along for the ride.
Think of ordering a thali. You ask for the thali, and the kitchen also brings rice, dal, roti, and pickle that you did not order one by one. Those extras are transitive. Usually that is fine. But what if one of those extras has a security problem and you must force a safer version?
That is what transitive pinning does. You turn it on in the central file:
<Project>
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
<CentralPackageTransitivePinningEnabled>true</CentralPackageTransitivePinningEnabled>
</PropertyGroup>
<ItemGroup>
<!-- A package you never reference directly, pinned for safety -->
<PackageVersion Include="System.Text.Json" Version="8.0.5" />
</ItemGroup>
</Project>With pinning on, if any of your packages tries to pull in an older System.Text.Json, NuGet promotes your central version and uses 8.0.5 instead. You patched a hidden dependency from one place, across the whole repo.
Transitive pinning to the rescue
Steps
Direct package
You install a library you need
Pulls in old helper
It drags an old, risky transitive package
Pin overrides it
Central pin forces the safe version everywhere
Global package references for build-only tools
Some packages are needed by every project, not for your app logic, but for the build itself. Things like analyzers, source generators, or Microsoft.SourceLink.GitHub. Instead of adding them to each project, you can declare them once in the central file with <GlobalPackageReference>.
<ItemGroup>
<GlobalPackageReference Include="Microsoft.SourceLink.GitHub" Version="8.0.0" />
</ItemGroup>Now this package is applied to every project in the repo automatically. This is great for tooling that should be consistent everywhere and that no single project should be allowed to skip.
Here is a quick map of the three special features and when to reach for each.
| Feature | What it does | Reach for it when |
|---|---|---|
VersionOverride | Lets one project use a different version | One project must differ, just for now |
| Transitive pinning | Forces a version on hidden dependencies | You must patch an indirect package safely |
GlobalPackageReference | Adds a package to every project | A build tool must run everywhere |
A real folder layout
It can help to see where everything sits. CPM follows MSBuild's "directory" convention: the Directory.Packages.props file applies to every project below it in the folder tree.
Because the file lives at the root, every project under it is covered without any extra wiring. If you ever need a different set of versions for one sub-folder, you can place another Directory.Packages.props inside that folder, and it takes over for the projects below it. Most teams never need that, though. One file at the root is usually enough.
Tips for a smooth move
Switching an existing solution to CPM is mostly mechanical, but a few habits make it painless.
- Move in one commit. Create the central file, strip versions from every project, and restore, all in one change. A half-converted repo is confusing.
- Let tools help. Modern Visual Studio, Rider, and the
dotnetCLI can convert a solution for you. Look for the "manage packages centrally" or migration option rather than editing by hand. - Sort the central file. Keep
<PackageVersion>lines in alphabetical order. It makes the file easy to scan and keeps merge conflicts small. - Group related versions. When several packages from one family must move together (like all the
Microsoft.Extensions.*packages), upgrade them in the same commit so they stay in step. - Watch the warnings. If NuGet warns that a
Versionis being ignored, that is a leftover from a project file. Remove it.
One more comfort: CPM is not all-or-nothing across your machine. It is turned on per repository by that one switch. If you do not create the central file, nothing changes. So you can try it on a small repo first and grow your confidence.
A note on package licensing
While you tidy up dependencies, it is a good moment to check licenses too. Some popular .NET packages changed their terms recently. For example, MediatR and MassTransit are now commercially licensed for many uses. CPM does not change licensing in any way, but since you will be looking at your whole package list in one file, it is a handy chance to confirm each package still fits your project's rules.
Quick recap
- Central Package Management keeps all NuGet versions in one file,
Directory.Packages.props, at the root of your repo. - Turn it on with
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>. - List versions there using
<PackageVersion Include="..." Version="..." />. - In each project, keep
<PackageReference Include="..." />with no version. - This stops version drift and makes upgrades a one-line change.
- Use
VersionOverridewhen one project truly needs a different version, but use it rarely. - Use transitive pinning to force safe versions on hidden, indirect dependencies.
- Use
<GlobalPackageReference>for build tools that every project needs. - You need NuGet 6.2+ (transitive pinning needs 6.4+), which any modern .NET SDK has.
CPM is a small change with a big payoff. One shopping list on the fridge, and the whole house stays in sync.
References and further reading
- Central Package Management (CPM) — Microsoft Learn
- Introducing Central Package Management — .NET Blog
- Understand NuGet Central Package Management and Transitive Pinning — NDepend Blog
- Central Package Management in .NET — Milan Jovanović
Related Posts
Building File-Based Apps in .NET With Multi-File Support
Learn how to run C# without a project file using dotnet run app.cs in .NET 10, split code across files with #:include, and add packages with directives.
5 Hidden EF Core NuGet Packages That Make Your .NET Code Better
Five lesser-known EF Core NuGet packages for clean exceptions, naming conventions, bulk speed, dynamic queries, and auditing — with simple examples and diagrams.
The 3 C# PDF Libraries Every Developer Must Know
A friendly guide to QuestPDF, PDFsharp, and iText for C#. Learn what each does, their licensing, code examples, and how to pick the right one.
Synchronous vs Asynchronous Communication in Microservices (.NET Guide)
A simple, friendly guide to synchronous vs asynchronous communication in microservices, with .NET examples, diagrams, tables, and clear rules on when to use each.
Understanding Microservices: Core Concepts and Benefits for .NET
A beginner-friendly guide to microservices in .NET: what they are, the core ideas behind them, their real benefits and trade-offs, and when to use them.
Building Your First Use Case With Clean Architecture in .NET
A beginner-friendly, step-by-step guide to building your first use case in .NET Clean Architecture: command, handler, repository, and endpoint, with diagrams.