TL;DR - Microsoft has quietly shipped the ability to centrally manage package versions in the latest .NET Core SDK, in this post we'll look at some of the details
You know the problem, your .NET solution grows to 20 projects, and the versions of packages you use starts to drift amongst them.
This can make it hard to reason about what package you are actually using at runtime, and is just a general hassle to manage as your solution gets larger.
This is such a problem that Visual Studio even has some dedicated UI to help manage it; "Consolidate" under NuGet Package Management.
Centralised Package Versions
Having a centralised file to express the version of all packages used across a solution makes a lot of sense, as has been evident with package managers such as Paket, regardless of your solution size.
How it works
So provided we have .NET Core SDK 3.1.300 or above, we can enable central package versioning by adding a
Directory.Packages.props file, like this:
<Project><ItemGroup><PackageVersion Include="System.Text.Json" Version="4.7.2" /></ItemGroup></Project>
Notice the item is named
PackageVersion, rather than the familiar
As you may have guessed, the
Directory.Packages.props file works like the other
Directory.* files, so projects will continue to search parent directories until a file is found.
As it stands today, we still need to switch the feature on, we do this by setting the
ManagePackageVersionsCentrally MSBuild property to
true, we can do this for all projects using a
Or we could add this on a project-by-project basis. In the future this might be changed to on by default, and you'd set this to
false to opt-out.
Now let's say we try to build our projects without making any changes...
error NU1008: Projects that use central package version management should not define the version on the PackageReference items but on the PackageVersion items: System.Text.Json
It's basically telling us off for specifying package versions at a project level with this feature enabled. Now let's update our project and remove the
Version attributes from any package references:
<Project Sdk="Microsoft.NET.Sdk"><PropertyGroup><TargetFramework>netstandard2.0</TargetFramework></PropertyGroup><ItemGroup><PackageReference Include="System.Text.Json" /></ItemGroup></Project>
Now everything builds, and we have no package version info in our projects 🙂
This also has the added benefit that our
Directory.Packages.props file must list all the packages used by our solution, and their versions, so it becomes a great way to keep tabs on what is being used.
This solution is being built by the NuGet team and will be an out-of-the-box solution, but this isn't the first Microsoft effort at building this, there is also the great Microsoft.Build.CentralPackageVersions that this design was influenced by.
Paket is an alternative package manager to NuGet for .NET projects, it has some great features which may make it preferable to NuGet. Central package versioning is one of Pakets defining features, and it is certainly worth a look.
There are a couple of MSBuild tricks people often do to solve this problem today. One is to have a props file with version numbers in (like this), then reference those from the project files.
The other popular option is to update the
PackageReference items in the
Directory.Build.targets file (like this). This is quite a clever approach, and it leans on how MSBuild works, however people need to understand how it's working (I even forgot the loading order in writing this!), and just like the previous approach it isn't enforceable. Nothing prevents a developer from coming along and ignoring the centralised approach.
This feature is currently available with the latest .NET Core SDK release (3.1.300 at time of publishing), but the NuGet team do want feedback, so I encourage you to try it and and feedback to the them here.
As this practice becomes more prevalent in the eco-system, we can expect tooling to follow, today some of the GUI tools within .NET IDEs won't know how to deal with this format. The same goes for tools like dependabot, however thanks to David Driscoll this is already supported by NuKeeper, and how I became aware that this feature was shipped in the SDK (thanks David!).
For more info on this feature, you can take a further read of the proposal on the NuGet wiki.