Using Central Package Management with .NET
It is often the case that a solution with a large number of projects share a lot of the same NuGet packages. Central Package Management is a means to help keep the package versions synchronized across the projects.
Central Package Management
Central Package Management is a new feature introduced with NuGet 6.2 that allows you to centrally
manage all the NuGet package dependencies for the projects in your solution. You create a
Directory.Packages.props
file and use it to store all the packages you are using across all
your projects. Here is a sample:
<Project>
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="{Package Name 1}" Version="{version 1}" />
<PackageVersion Include="{Package Name 2}" Version="{version 2}" />
...
<PackageVersion Include="{Package Name N}" Version="{version N}" />
</ItemGroup>
</Project>
The ManagePackageVersionsCentrally
MSBuild property indicates that the solution should use
the Directory.Packages.props
to track the versions of all packages in one central location.
If you navigate to a project and add a package, a PackageVersion
element will be added to the
Directory.Packages.props
file and in the project’s csproj file, a PackageReference
is added
with only the name of the package provided:
<PackageReference Include="{Package Name}" />
Reference: https://learn.microsoft.com/en-us/nuget/consume-packages/Central-Package-Management
Updating an Existing Solution
Step 1 - Find all the packages referenced in all the projects
It can be useful at the start to get a list of all the packages. You can use this list to see which packages have different versions referenced in different projects.
NOTE Thanks to Milan Jovanovic for inspiring me to look into Central Package Management more deeply and for providing a starting sample of this PowerShell script.
https://www.milanjovanovic.tech/blog/central-package-management-in-net-simplify-nuget-dependencies
# file: analyze-package-versions.ps1
# Scan all .csproj files and collect package references along with their projects
$packageReferences = Get-ChildItem -Filter *.csproj -Recurse | ForEach-Object {
$projectPath = $_.FullName
$projectName = $_.BaseName
# Extract PackageReference information from the .csproj file
Get-Content -Path $projectPath |
Select-String -Pattern '<PackageReference Include="([^"]+)" Version="([^"]+)"' -AllMatches |
ForEach-Object { $_.Matches } |
ForEach-Object {
@{
Package = $_.Groups[1].Value
Version = $_.Groups[2].Value
Project = $projectName
}
}
}
$nonUniqueVersions = @()
# Group packages and format the output
$packageReferences | Group-Object -Property Package | ForEach-Object {
# Group by version for each package
$versions = $_.Group | Group-Object -Property Version
# determine if all references to the package use the same version
$uniqueVersions = $versions | Select-Object -ExpandProperty Name | Select-Object -Unique
if ($uniqueVersions.Count -ne 1) {
# keep track of non-unique versions
$nonUniqueVersions += $_.Name
Write-Host "$($_.Name)" -ForegroundColor Red # Package name
# If there are multiple versions, list each version with its projects
$versions | ForEach-Object {
$version = $_.Name
$projects = $_.Group | Select-Object -ExpandProperty Project
Write-Host "`t$version - $($projects -join ', ')" -ForegroundColor Red
}
}
Write-Output "" # Blank line between packages
}
if ($nonUniqueVersions.Count -eq 0) {
Write-Host "All packages have consistent versions." -ForegroundColor Green
} else {
Write-Host "One or more packages have inconsistent versions" -ForegroundColor Red
exit 1
}
Running the script on my sample solution provides the following:
.\analyze-package-versions.ps1
Swashbuckle.AspNetCore
6.6.2 - WebApplication2
6.9.0 - WebApplication1
One or more packages have inconsistent versions
Step 2 - Add the Directory.Packages.props
file
When updating a solution to use Central Package Management, add an initial Directory.Packages.props
file in the solution root:
<Project>
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
</ItemGroup>
</Project>
The next steps can be done manually, but I have written a PowerShell script to automate the process.
Step 3 - Add package references
We need to scan the projects and add all the project references. Here is the script I wrote to perform that task:
$solutionRoot = Get-Location
# Install NuGet.Versioning package to parse and compare semantic versions of the packages
nuget install NuGet.Versioning -Version 6.12.1 -OutputDirectory .\packages
$assemblyPath = ".\packages\nuget.versioning.6.12.1\lib\netstandard2.0\NuGet.Versioning.dll"
[System.Reflection.Assembly]::LoadFrom($assemblyPath)
# Get the latest semantic version from a list of versions
function Get-LatestSemanticVersion {
param (
[string[]]$versions
)
# Use NuGet.Versioning to parse and compare versions
$versions | Sort-Object -Descending -Property {
[NuGet.Versioning.NuGetVersion]::Parse($_)
} | Select-Object -First 1
}
# Find all PackageReference elements in .csproj files and group them by package name
$packages = Get-ChildItem -Filter *.csproj -Recurse |
Get-Content |
Select-String -Pattern '<PackageReference Include="([^"]+)" Version="([^"]+)"' -AllMatches |
ForEach-Object { $_.Matches } |
Group-Object { $_.Groups[1].Value } |
ForEach-Object {
[PSCustomObject]@{
Name = $_.Name
# Find the most recent semantic version using the custom function
Version = Get-LatestSemanticVersion -versions ($_.Group.ForEach({ $_.Groups[2].Value }) | Select-Object -Unique)
}
} |
Sort-Object { $_.Name }
if ($packages.Count -eq 0) {
Write-Host "No PackageReference elements found in .csproj files"
exit
}
# Create the Directory.Packages.props file
$packageVersions = @(
$packages | ForEach-Object {
" <PackageVersion Include=`"$($_.Name)`" Version=`"$($_.Version)`" />"
}
) -join "`n "
$propsFilePath = Join-Path $solutionRoot 'Directory.Packages.props'
$propsContent = @"
<Project>
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
$packageVersions
</ItemGroup>
</Project>
"@
# Write the props content to the file
Set-Content -Path $propsFilePath -Value $propsContent
Write-Host "Created $propsFilePath"
Step 4 - Remove version attributes in all .csproj files
Lastly we need to go through and remove the version attributes from PackageReference elements in all .csproj files:
Get-ChildItem -Filter *.csproj -Recurse |
ForEach-Object {
$csprojPath = $_.FullName
$content = Get-Content -Path $csprojPath
$updatedContent = $content -replace '<PackageReference Include="([^"]+)" Version="([^"]+)"',
'<PackageReference Include="$1"'
Set-Content -Path $csprojPath -Value $updatedContent
Write-Host "Updated $csprojPath"
}
Don’t Like PowerShell?
I wrote all this in PowerShell for a couple of reasons. One, I wanted to learn all the steps involved and two, I need to practice using PowerShell as often as I can.
However, there is a tool you can install that does all this for you:
https://github.com/Webreaper/CentralisedPackageConverter
dotnet tool install CentralisedPackageConverter --global
central-pkg-converter path/to/solutionroot
It works very well and has some additional feature (like doing a dry run before making any changes, reverting back to non central package management references).
Summary
Central Package Management is a great way to ensure all the projects in your solution are using the same version. Updating an existing solutions to use Central Package Management is not too difficult using the PowerShell scripts or the central-pkg-converter
dotnet tool.