Give .NET Its Props - Central Package Management
It’s a common problem when your solution contains multiple projects: Your NuGet packages drift out of sync. You update a package in one project, but then you forget that you’re also using that package in another project. That leads to bugs that arise from version conflicts, which can often be difficult to track down. But .NET has the solution for your…. solution: central package management. It’s the one file to rule them all.
What is Directory.Packages.props?
Introduced in .NET core 6, the directory.packages.props file is a single file placed at the root of your solution that contains a central list of PackageVersion declarations for every NuGet package that the projects in your solution reference. Instead of defining the versions in the various projects, the projects only contain references to the package name. Then they reference the props file to determine what PackageVersion to use.
For example, here’s a .csproj file from a project in a solution that doesn’t use central package management.
Example 1 - .csproj with versions
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Aspire.Azure.Storage.Blobs" Version="13.1.2" />
<PackageReference Include="Aspire.Npgsql.EntityFrameworkCore.PostgreSQL" Version="13.1.2" />
<PackageReference Include="FastEndpoints" Version="8.0.1" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.5" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.5" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.5" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.5" />
</ItemGroup>
</Project>
And here’s a .csproj file from a project in a solution that does use central package management:
Example 2 - .csproj for a solution with central package management
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<OutputType>Exe</OutputType>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="FastEndpoints" />
<PackageReference Include="FastEndpoints.Swagger" />
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" />
<PackageReference Include="Aspire.Npgsql.EntityFrameworkCore.PostgreSQL" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" />
<PackageReference Include="Syncfusion.XlsIO.Net.Core" />
<PackageReference Include="CsvHelper" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
</Project>
You can see the difference between the two. For the second example, our solution also has a Directory.Packages.props file in the base directory alongside the solution file. our props file for the solution this project is a part of looks like the following:
<Project>
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<!-- Aspire -->
<PackageVersion Include="Aspire.Hosting.PostgreSQL" Version="13.2.2" />
<PackageVersion Include="Aspire.Npgsql.EntityFrameworkCore.PostgreSQL" Version="13.2.2" />
<PackageVersion Include="Microsoft.Extensions.ServiceDiscovery" Version="10.2.0" />
<PackageVersion Include="Microsoft.Extensions.Http.Resilience" Version="10.2.0" />
<!-- OpenTelemetry -->
<PackageVersion Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.15.0" />
<PackageVersion Include="OpenTelemetry.Extensions.Hosting" Version="1.15.0" />
<PackageVersion Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.15.0" />
<PackageVersion Include="OpenTelemetry.Instrumentation.Http" Version="1.15.0" />
<PackageVersion Include="OpenTelemetry.Instrumentation.Runtime" Version="1.15.0" />
<!-- FastEndpoints -->
<PackageVersion Include="FastEndpoints" Version="7.2.0" />
<PackageVersion Include="FastEndpoints.Swagger" Version="7.2.0" />
<!-- Entity Framework Core -->
<PackageVersion Include="Microsoft.EntityFrameworkCore" Version="10.0.2" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.2" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.2" />
<PackageVersion Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.0" />
<!-- FluentValidation -->
<PackageVersion Include="FluentValidation" Version="12.1.1" />
<PackageVersion Include="FluentValidation.DependencyInjectionExtensions" Version="12.1.1" />
<PackageVersion Include="Blazored.FluentValidation" Version="2.2.0" />
<!-- Logging -->
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.0-preview.1.25080.5" />
<!-- Health Checks -->
<PackageVersion Include="AspNetCore.HealthChecks.NpgSql" Version="9.0.0" />
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="10.0.0-preview.1.25120.3" />
<!-- Testing -->
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.0.1" />
<PackageVersion Include="xunit" Version="2.9.3" />
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.5" />
<PackageVersion Include="Shouldly" Version="4.3.0" />
<PackageVersion Include="NSubstitute" Version="5.3.0" />
<PackageVersion Include="NSubstitute.Analyzers.CSharp" Version="1.0.17" />
<PackageVersion Include="bunit" Version="2.5.3" />
<PackageVersion Include="coverlet.collector" Version="6.0.4" />
<!-- Syncfusion Blazor -->
<PackageVersion Include="Syncfusion.Blazor.Grid" Version="32.1.25" />
<PackageVersion Include="Syncfusion.Blazor.Inputs" Version="32.1.25" />
<PackageVersion Include="Syncfusion.Blazor.DropDowns" Version="32.1.25" />
<PackageVersion Include="Syncfusion.Blazor.Calendars" Version="32.1.25" />
<PackageVersion Include="Syncfusion.Blazor.Navigations" Version="32.1.25" />
<PackageVersion Include="Syncfusion.Blazor.Notifications" Version="32.1.25" />
<PackageVersion Include="Syncfusion.Blazor.Schedule" Version="32.1.25" />
<PackageVersion Include="Syncfusion.Blazor.Charts" Version="32.1.25" />
<PackageVersion Include="Syncfusion.Blazor.Themes" Version="32.1.25" />
<PackageVersion Include="Syncfusion.Blazor.Popups" Version="32.1.25" />
<PackageVersion Include="Syncfusion.Blazor.Spinner" Version="32.1.25" />
<PackageVersion Include="Syncfusion.Blazor.Cards" Version="32.1.25" />
<PackageVersion Include="Syncfusion.Blazor.Buttons" Version="32.1.25" />
<PackageVersion Include="Syncfusion.Blazor.ProgressBar" Version="32.1.25" />
<PackageVersion Include="Syncfusion.XlsIO.Net.Core" Version="32.1.25" />
<!-- CSV Parsing -->
<PackageVersion Include="CsvHelper" Version="33.1.0" />
<!-- Identity -->
<PackageVersion Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="10.0.2" />
<PackageVersion Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.2" />
<!-- Integration Testing -->
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.2" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.InMemory" Version="10.0.2" />
<PackageVersion Include="Aspire.Hosting.Testing" Version="13.2.2" />
<!-- Code Analysis -->
<PackageVersion Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="9.0.0" />
</ItemGroup>
</Project>
The props file contains references to all of the NuGet packages that are used throughout the solution. And with each one a Version= reference is included that tells all of the projects in the solution what version to use if they have a reference to one of the listed NuGet packages. This creates a single, central file that tracks the versions.
Setting It Up
So what do we do if we want to set up central package management for our solution. It’s actually pretty easy to set up, even for an existing and well developed solution.
First up, in the same root folder where your .sln or .slnx file resides, create a file called Directory.Packages.props. Add the following content to the file:
<Project>
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
</Project>
Then go through each of your .csproj files throughout your solution. In each, find all of your NuGet package references. For each one, add an entry inside of an <ItemGroup> block after the <ManagePackageVersionsCentrally> block in the file. You don’t have to keep the same <ItemGroup> groupings as there are in the .csproj files. You can group them however you like. But for each make sure you aren’t creating duplicate entries in the props file. Be sure to add the Version= segment into the props file as well.
The final step is to remove the Version= reference from all the entries in the various .csproj files. It’s important that you do this because if you enable central package management, having a Version value set in a .csproj file will result in a build error (NU1008).
Our first example above should look like the following:
Example 1 - Revised After Adding Central Package Management
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Aspire.Azure.Storage.Blobs" />
<PackageReference Include="Aspire.Npgsql.EntityFrameworkCore.PostgreSQL" />
<PackageReference Include="FastEndpoints" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" />
<PackageReference Include="Microsoft.EntityFrameworkCore" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" />
</ItemGroup>
</Project>
And the new Directory.Packages.props file for this solution:
<Project>
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="Aspire.Azure.Storage.Blobs" Version="13.1.2" />
<PackageVersion Include="Aspire.Npgsql.EntityFrameworkCore.PostgreSQL" Version="13.1.2" />
<PackageVersion Include="FastEndpoints" Version="8.0.1" />
<PackageVersion Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.5" />
<PackageVersion Include="Microsoft.AspNetCore.OpenApi" Version="10.0.5" />
<PackageVersion Include="Microsoft.EntityFrameworkCore" Version="10.0.5" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.5" />
</ItemGroup>
</Project>
Overriding Versions
If there is a reason that you need to have a different version of a package for a specific project than what is defined in the props file, you can do so in the .csproj file by adding the VersionOverride attribute. Typically this occurs when you need to support some legacy library or block of code. Just try not to do it too often or it can become difficult to maintain the solution.
Example of VersionOverride
<!-- LegacyApp.csproj -->
<PackageReference Include="Microsoft.EntityFrameworkCore" VersionOverride="8.0.0" />
Global Package References
There are times when you need to have certain NuGet packages in every single project in a solution. This is easily done by defining a GlobalPackageReference in your props file. When you do this, that package gets added to every .NET project in the solution and you don’t need to have references in the various .csproj files throughout the solution. You would typically want to do this for things like analyzers, logging, collectors, and so forth.
Example of GlobalPackageReference
<!-- Directory.Packages.props -->
<GlobalPackageReference Include="coverlet.collector" Version="6.0.4" />
Full Real-World Example
If we were to implement a full, real-world sample solution utilizing Clean Architecture, with API, core, infrastructure, and tests projects, we might come up with the following design.
Our solution structure would look something like the following:
MyApp/
├── Directory.Packages.props
├── Directory.Build.props
├── MyApp.sln
├── src/
│ ├── MyApp.Api/
│ │ └── MyApp.Api.csproj
│ ├── MyApp.Core/
│ │ └── MyApp.Core.csproj
│ └── MyApp.Infrastructure/
│ └── MyApp.Infrastructure.csproj
└── tests/
├── MyApp.UnitTests/
│ └── MyApp.UnitTests.csproj
└── MyApp.IntegrationTests/
└── MyApp.IntegrationTests.csproj
Our files:
Directory.Packages.props
<Project>
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup Label="Ardalis">
<PackageVersion Include="Ardalis.GuardClauses" Version="4.6.0" />
<PackageVersion Include="Ardalis.Result" Version="8.1.0" />
<PackageVersion Include="Ardalis.Result.AspNetCore" Version="8.1.0" />
<PackageVersion Include="Ardalis.Specification" Version="8.0.0" />
<PackageVersion Include="Ardalis.Specification.EntityFrameworkCore" Version="8.0.0" />
</ItemGroup>
<ItemGroup Label="API">
<PackageVersion Include="FastEndpoints" Version="5.30.0" />
<PackageVersion Include="FastEndpoints.Swagger" Version="5.30.0" />
</ItemGroup>
<ItemGroup Label="Messaging">
<PackageVersion Include="MediatR" Version="12.4.1" />
<PackageVersion Include="MediatR.Extensions.Microsoft.DependencyInjection" Version="11.1.0" />
</ItemGroup>
<ItemGroup Label="Data">
<PackageVersion Include="Microsoft.EntityFrameworkCore" Version="10.0.1" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.SqlServer" Version="10.0.1" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.1" />
</ItemGroup>
<ItemGroup Label="Logging & Telemetry">
<PackageVersion Include="OpenTelemetry.Extensions.Hosting" Version="1.11.0" />
<PackageVersion Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.11.0" />
<PackageVersion Include="Serilog.AspNetCore" Version="10.0.0" />
</ItemGroup>
<ItemGroup Label="Testing">
<PackageVersion Include="xunit" Version="2.9.3" />
<PackageVersion Include="xunit.runner.visualstudio" Version="3.0.1" />
<PackageVersion Include="FluentAssertions" Version="8.2.0" />
<PackageVersion Include="NSubstitute" Version="5.3.0" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.InMemory" Version="10.0.1" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.1" />
</ItemGroup>
<ItemGroup Label="Global Analyzers">
<GlobalPackageReference Include="coverlet.collector" Version="6.0.4" />
<GlobalPackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
</ItemGroup>
</Project>
MyApp.Core.csproj
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Ardalis.GuardClauses" />
<PackageReference Include="Ardalis.Result" />
<PackageReference Include="Ardalis.Specification" />
<PackageReference Include="MediatR" />
</ItemGroup>
</Project>
MyApp.Infrastructure.csproj
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\MyApp.Core\MyApp.Core.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Ardalis.Specification.EntityFrameworkCore" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" />
</ItemGroup>
</Project>
MyApp.Api.csproj
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\MyApp.Core\MyApp.Core.csproj" />
<ProjectReference Include="..\MyApp.Infrastructure\MyApp.Infrastructure.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Ardalis.Result.AspNetCore" />
<PackageReference Include="FastEndpoints" />
<PackageReference Include="FastEndpoints.Swagger" />
<PackageReference Include="OpenTelemetry.Extensions.Hosting" />
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" />
<PackageReference Include="Serilog.AspNetCore" />
</ItemGroup>
</Project>
MyApp.UnitTests.csproj
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\MyApp.Core\MyApp.Core.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="FluentAssertions" />
<PackageReference Include="NSubstitute" />
<PackageReference Include="xunit" />
<PackageReference Include="xunit.runner.visualstudio" />
</ItemGroup>
</Project>
MyApp.IntegrationTests.csproj
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\MyApp.Api\MyApp.Api.csproj" />
<ProjectReference Include="..\..\src\MyApp.Infrastructure\MyApp.Infrastructure.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="FluentAssertions" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" />
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" />
<PackageReference Include="NSubstitute" />
<PackageReference Include="xunit" />
<PackageReference Include="xunit.runner.visualstudio" />
</ItemGroup>
</Project>
Let’s note a few things here:
- There are no
Versionattributes on any of thePackageReferenceitems in the project files. Those are all governed byDirectory.Packages.props. GlobalPackageReferencehandlescoverlet.collectorandMicrosoft.NET.Test.Sdkso the test projects don’t need to repeat them.- Labels on the
ItemGroupblocks make it easy to identify related items in the props file at a glance. It can be a great way to organize the file for readability.
Tips & Gotchas
A few things to keep in mind when working with central package management. It works fine with dotnet restore and the NuGet management tools in Visual Studio and Rider. However, the CLI dotnet add package will not update the Directory.Packages.props file and still requires you to manually update files yourself after you add the package.
Directory.Packages.props pairs well with the Directory.Build.props file, which is used to provide central build configuration information that is used for MS Build across all the projects in a solution.
Also keep in mind that central package management is still fairly new to .NET and a lot of tools like source generators and analyzers may not support it without some special handling.
AI coding agents can be an excellent partner to help you update and maintain your props files, especially during initial setup. For instance, a prompt such as the following will get most agents to successfully implement the setup for you:
I want to add central package management to this .NET solution. Create a Directory.Packages.props file and update it with all the NuGet packages from the projects in this solution. Update the project .csproj files appropriately after creating the props file to remove version references.
Conclusion
Central package management can bring a lot of value to your .NET solutions and workflow. First, it prevents version mismatches and version creep, keeping all of your NuGet packages in line. Second, it makes PR reviews easier by only requiring you to review a single file for version updates. Third, it makes it configuration easier for tools like Dependabot & Renovate, keeping your code with the right versions. And lastly, it forces intentional version alignment across your solution, ensuring that teams are always implementing the correct and approved versions of NuGet packages.
It’s easy to set up. It’s easy to use. Make it your standard for package management for both greenfield and existing solution work on all of your projects. Check out the official docs for more about Central Package Management for .NET solutions.


