There is some ongoing work in NuGet itself to add support for managing nuget packages in a central manner, make sure you read about the indented design since you may want to wait for that to ship in stable form if you require the advanced features outlined there
This showcases how you can adopt this now, with stuff that is already shipping and stable, with acceptable IDE support and some useful out of the box behavior, like supporting version overrides and friendly error messages.
I think this is also a very good way of understanding a bit more about MSBuild capabilities.
So let’s first see what (my) requirements are:
- Seamless integration with existing MSBuild-based projects, no need to change much (if anything) in existing projects.
- Acceptable IDE support (i.e. no “broken dependencies” icon in solution explorer).
- Minimal cognitive overhead over existing PackageReference semantics and syntax.
With that in mind, my approach is:
- We keep
<PackageReference>as-is: you can specify a
Versionif you want to.
- Centrally managed
- Specify a
Version, which will force a version for every project that declares the package reference.
- Specify a
DefaultVersion, which will allow overriding individually, but provide a default if
Versionis not set.
- Specify a
Packages.props (the built-in support will be
Directory.Packages.props instead) to define the central package versions, and this is an example of it:
<Project> <ItemGroup> <PackageReference Update="GitInfo" Version="2.0.20" PrivateAssets="all" /> <PackageReference Update="Newtonsoft.Json" DefaultVersion="12.0.3" /> </ItemGroup> </Project>
Note we do an
Update: this means there needs to be an existing
PackageReference in the project file in order for it to be updated. This does not define a new package reference that is added to all projects. To do that, you could just do an
Include instead, as usual.
Given the following .csproj:
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <TargetFramework>netstandard2.0</TargetFramework> </PropertyGroup> <ItemGroup> <PackageReference Include="GitInfo" /> <PackageReference Include="Newtonsoft.Json" /> </ItemGroup> </Project>
Both should be defaulted as:
If we update the references like so:
<ItemGroup> <PackageReference Include="GitInfo" Version="2.0.0" /> <PackageReference Include="Newtonsoft.Json" Version="11.0.2" /> <PackageReference Include="Xunit" /> </ItemGroup>
We should see:
- GitInfo causes a build error, since there is a central package version and it’s not a mere default.
- Newtonsoft.Json version override is allowed.
- Xunit also causes a build error, since no version was specified but there is no central package version (default or otherwise).
The implementation ended up being extremely simple, involving just minor adjustments to the (likely already present) “global” directory targets:
Directory.Build.targets: here we’ll first preserve the original version (if any) specified in the .csproj before we update it, and then import the package targets:
<ItemGroup> <PackageReference Update="@(PackageReference)" OriginalVersion="%(Version)" /> </ItemGroup> <Import Project="Packages.props" /> <Import Project="Packages.targets" />
Packages.targetsimplements the actual logic given the requirements:
<Project> <ItemDefinitionGroup> <PackageReference> <DefaultVersion /> <Version /> </PackageReference> </ItemDefinitionGroup> <Target Name="UpdatePackageVersions" BeforeTargets="_GenerateRestoreGraph;_GenerateRestoreGraphProjectEntry;CollectPackageReferences;PrepareForBuild"> <Error Code="DL001" Text="Package reference '%(PackageReference.Identity)' cannot specify a version because it is centrally managed." Condition="%(PackageReference.OriginalVersion) != '' And %(PackageReference.Version) != %(PackageReference.OriginalVersion)" /> <ItemGroup> <PackageReference Version="$([MSBuild]::ValueOrDefault('%(Version)', '%(DefaultVersion)'))" /> </ItemGroup> <Error Code="DL002" Text="Package reference '%(PackageReference.Identity)' must specify a version since a centrally managed default version has not been provided." Condition="%(PackageReference.Version) == '' And %(PackageReference.Identity) != ''" /> </Target> </Project>
ItemDefinitionGroup ensures that all
PackageReference items have both
DefaultVersion metadata for the comparisons in the target.
OriginalVersion we saved in step 1 before importing the
Packages.props works because the
PackageReference items aren’t updated until the import, and once they are updated we can use the original version to check for mismatches. We also leverage the
ValueOrDefault property function to only set the
DefaultVersion if it doesn’t have a value already.
BeforeTargets basically are:
_GenerateRestore*: these are called by
Restorewhen building the graph of package references, so we need to run before that to update the versions.
CollectPackageReferences: this is called by the IDE during design-time build, like right after opening the project and doing the initial automatic restore.
PrepareForBuild: just in case any targets in your project inspect the versions, we also run before builds.
At least for my scenarios, this satisfies all my requirements and is simple enough that I will be able to reason about this in the future if needed.