A common problem for multi-project applications, frameworks or libraries is how to manage the versions of NuGet package that are referenced. Often packages will be updated in one project but not others, leading to a state of confusion, particularly if one of the packages causes issuse, or there are incompatibilities between versions. In this post we’re going to walk through using Central Package Management to simplify how you manage versions of your NuGet dependencies.
Let’s start by providing a brief overview of what Central Package Management is. Typically, most solutions are setup so that each project maintains it’s own list of dependencies and in the case of dependencies that come from NuGet (or other package sources) this involves using a PackageReference element in the project file, which includes the name of the package and the version (or constraints) that is referenced. When packages are updated, it’s preferrable to manage them at a solution level, which you can do in Visual Studio by right-clicking on the Solution and selecting Manage NuGet Packages for Solution. However, for whatever reason, sometimes package might get updated on individual projects. If you open the Manage Packages for Solution interface in Visual Studio, you’ll see that there are packages listed on the Consolidate tab, allowing you to align projects with different package versions.
Central Package Management gives you the ability to separate the two concerns of “which packages a project should reference” and “which version of a package should be used”. Individual projects still include PackageReference elements for each package they want to directly reference. However, instead of including the version number in the project file, a separate PackageVersion element is maintained at a solution level, meaning that all project will use the same version of the package.
Whilst in theory you can add the PackageVersion element into either Directory.Build.props or Directory.Build.targets, it’s recommended to add the PackageVersion elements inside an ItemGroup element in a Directory.Packages.props file. You also need to include the ManagePackageVersionsCentrally element (with value set to true) in either the Directory.Build.props file, or the Directory.Packages.props file, in order to enable Central Package Management.
Before we jump in and walk through an example of how to setup Central Package Management, I wanted to just point out that the other reason you might want to use Central Package Management is to maintain a list of all direct dependencies that your application has. Note that this isn’t a comprehensive list of dependencies, since some of the dependencies that your application references directly, may have transitive dependencies. However, I would point out that having the list in one place means that you can restrict updates and changes to both the items in the list, and the versions – this is important if you want to make sure that you do an adequate review of licensing if/when you change any dependencies (if you’re using any OSS libraries, at the point you add/remove/change the versions you should consider how you’re giving back to those projects!!).
Enabling Central Package Management
In order to enable Central Package Management you need to set the ManagePackageVersionsCentrally property to true. Rather than explicitly setting this in every project file, this property should be set in a Directory.Build.props file that resides in the solution folder.
Note: If you haven’t worked with Directory.Build.props file before, you can think of this as a file that contains project properties and other elements that you want to apply to all projects that are nested in sub-folders (unless overridden by a Directory.Build.props in a sub-folder). For example if you wanted to update all projects to use the latest C# language features, you could set the LangVersion in a Directory.Build.props file to specify the C# language version you want to use. Check out the Microsoft documentation for more information on working with Directory.Build.props and Directory.Build.targets.
For the purposes of this walkthrough I’m going to use a new Uno application created from the unoapp template (you can get the dotnet new templates here). At the time of writing the unoapp template doesn’t use Central Package Management and includes six projects (seven if you include the shared project) that reference various packages, so a good example of where you’d want to enable Central Package Management.
The app created from the Uno template doesn’t include a Directory.Build.props file, so we’ll start by creating one in the solution folder that includes the ManagePackageVersionsCentrally property. Right-click on the Solution node in Solution Explorer and select Add, New Item from the context menu.
Set the name of the new file to Directory.Build.props and click OK to create the file. Add the following XML that defines a Project, PropertyGroup and the ManagePackageVersionsCentrally element with value of true.
<Project> <PropertyGroup> <ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally> </PropertyGroup> </Project>
Note: When you first start editing the Directory.Build.props file you may find that Visual Studio doesn’t provide you and intellisense. Once you’ve added the Project element (including close tag), if you close and reopen the file, you should start to see intellisense.
After adding the Directory.Build.props and setting the ManagePackageVersionsCentrally to true, when you do a rebuild of the solution you’ll start to see a bunch of errors stating “
error NU1008: Projects that use central package version management should not define the version on the PackageReference items but on the PackageVersion items:“. As the error quite rightly points out, we’ve enabled Central Package Management but we’re currently defining package versions in the PackageReference elements in each project.
The next step in setting up Central Package Management is to create a Directory.Packages.info file, which will contain the version of each package referenced by any project in the solution. It’s important to note that unlike including a PackageReference element in the Directory.Build.props or targets file, which will include the package reference in every project, the PackageVersion element only defines the version of a specific package that is to be used if a particular project references that package (i.e. not all packages that have a PackageVersion set are included in every project).
There are two parts to this step:
- Add a PackageVersion element in the Directory.Packages.props file for each package reference in every project in the solution.
- Remove the Version attribute from every PackageReference element in every project in the solution.
For the example app, the Directory.Packages.props for the application created from the Uno template would include the following:
<Project> <ItemGroup> <PackageVersion Include="Microsoft.Windows.Compatibility" Version="5.0.0" /> <PackageVersion Include="Microsoft.WindowsAppSDK" Version="1.1.0" /> <PackageVersion Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.22000.196" /> <PackageVersion Include="Microsoft.Extensions.Logging" Version="5.0.0" /> <PackageVersion Include="Microsoft.Extensions.Logging.Console" Version="5.0.0" /> <PackageVersion Include="Uno.Core.Extensions.Logging.Singleton" Version="4.0.1" /> <PackageVersion Include="Uno.Extensions.Logging.OSLog" Version="1.4.0" /> <PackageVersion Include="Uno.Extensions.Logging.WebAssembly.Console" Version="1.3.0" /> <PackageVersion Include="Uno.UI.Adapter.Microsoft.Extensions.Logging" Version="4.4.13" /> <PackageVersion Include="Uno.UniversalImageLoader" Version="1.9.36" /> <PackageVersion Include="Uno.Wasm.Bootstrap" Version="3.3.1" /> <PackageVersion Include="Uno.Wasm.Bootstrap.DevServer" Version="3.3.1" /> <PackageVersion Include="Uno.WinUI" Version="4.5.14" /> <PackageVersion Include="Uno.WinUI.WebAssembly" Version="4.4.13" /> <PackageVersion Include="Uno.WinUI.Skia.Gtk" Version="4.4.13" /> <PackageVersion Include="Uno.WinUI.Skia.Linux.FrameBuffer" Version="4.4.13" /> <PackageVersion Include="Uno.WinUI.Skia.Wpf" Version="4.4.13" /> <PackageVersion Include="Uno.WinUI.RemoteControl" Version="4.4.13" Condition="'$(Configuration)'=='Debug'" /> <PackageVersion Include="Xamarin.Google.Android.Material" Version="18.104.22.168" /> </ItemGroup> </Project>
There are a couple of pointers that are worth noting
- After switching to using Central Package Management you may need to restart Visual Studio in order to for the projects to build
- There is a known issue with multi-target projects, such as the single-project used by dotnet MAUI and Uno (for the mobile targets). The workaround (ie including
<DisableImplicitLibraryPacksFolder>true</DisableImplicitLibraryPacksFolder>) in the GitHub issue works but may require running
dotnet restoreas the command line.
Overriding Package Version
In some cases it may still be necessary for a project to reference a different version of a package than what’s specified in the PackageVersion element. The package version can be overridden in the project file by setting the VersionOverride attribute to the desired version.
<PackageReference Include="Microsoft.Extensions.Logging" VersionOverride="6.0.0"/>
As we’ve walked through in this post, Central Package Management can be enabled in an application with minimal effort. Central Package Management both helps make it easy to keep dependency versions consistency across multiple project, as well as maintaining a list of dependencies for an application. Whilst Central Package Management at the time of writing is still technically a NuGet preview feature, it’s available in both stable and preview versions of Visual Studio and can be used when running build pipelines on both Azure DevOps adn GitHub Actions.