简体   繁体   中英

How to enforce same nuget package version across multiple c# projects?

I have a bunch of small C# projects which use a couple of NuGet packages. I'd like to be able to update version of a given package automatically. More then that: I'd like to be warned if a project uses different version from the others.

How do I enforce same version dependency across multiple C# projects?

I believe I have found a setup which solves this (and many other) problem(s).

I just realized one can use a folder as nuget source. Here is what I did:

root
  + localnuget
      + Newtonsoft.Json.6.0.1.nupkg
  + nuget.config
  + packages
      + Newtonsoft.Json.6.0.1
  + src
      + project1

nuget.config looks like this:

<configuration>
  <config>
    <add key="repositoryPath" value="packages" />
  </config>
  <packageSources>
    <add key="local source" value="localnuget">
  </packageSources>
</configuration>

You can add Nuget server to nuget.config to get access to updates or new dependencies during development time:

<add key="nuget.org" value="https://www.nuget.org/api/v2/" /> 

Once you're done, you can copy .nupkg from cache to localnuget folder to check it in.

There are 3 things I LOVE about this setup:

  1. I'm now able to use Nuget features, such as adding props and targets. If you have a code generator (eg protobuf or thrift) this becomes pricesless.

  2. It (partially) solves the problem of Visual Studio not copying all DLLs , because you need to specify dependencies in .nuspec file and nuget loads indirect dependencies automatically.

  3. I used to have a single solution file for all projects so updating nuget packages was easier. I haven't tried yet but I think I solved that problem too. I can have nuget packages for the project I want to export from a given solution.

I don't know how to enforce it, but I've found the "Consolidate" tab to help. This tab shows you packages that have different versions throughout the solution. From there you can select projects and use the install button to install the same package version across them. This tab can be found under "Manage NuGet for solution".

在此处输入图像描述

See Consolidate tab in Microsoft documentation.

As I haven't found another way to enforce this, I've written a unit test which will fail if different package versions are being found in any packages.config in any subfolder. As this might be useful for others, you'll find the code below. You'll have to adapt the resolution of the root folder done in GetBackendDirectoryPath().

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Xml;

using NUnit.Framework;

namespace Base.Test.Unit
{
    [TestFixture]
    public class NugetTest
    {
        private const string PACKAGES_CONFIG_FILE_NAME = "packages.config";
        private const string BACKEND_DIRECTORY_NAME = "DeviceCloud/";

        private const string PACKAGES_NODE_NAME = "packages";
        private const string PACKAGE_ID_ATTRIBUTE_NAME = "id";
        private const string PACKAGE_VERSION_ATTRIBUTE_NAME = "version";

        /// <summary>
        /// Tests that all referenced nuget packages have the same version by doing:
        /// - Get all packages.config files contained in the backend
        /// - Retrieve the id and version of all packages
        /// - Fail this test if any referenced package has referenced to more than one version accross projects
        /// - Output a message mentioning the different versions for each package 
        /// </summary>
        [Test]
        public void EnforceCoherentReferences()
        {
            // Act
            IDictionary<string, ICollection<PackageVersionItem>> packageVersionsById = new Dictionary<string, ICollection<PackageVersionItem>>();
            foreach (string packagesConfigFilePath in GetAllPackagesConfigFilePaths())
            {
                var doc = new XmlDocument();
                doc.Load(packagesConfigFilePath);

                XmlNode packagesNode = doc.SelectSingleNode(PACKAGES_NODE_NAME);
                if (packagesNode != null && packagesNode.HasChildNodes)
                {
                    foreach (var packageNode in packagesNode.ChildNodes.Cast<XmlNode>())
                    {
                        if (packageNode.Attributes == null)
                        {
                            continue;
                        }

                        string packageId = packageNode.Attributes[PACKAGE_ID_ATTRIBUTE_NAME].Value;
                        string packageVersion = packageNode.Attributes[PACKAGE_VERSION_ATTRIBUTE_NAME].Value;

                        if (!packageVersionsById.TryGetValue(packageId, out ICollection<PackageVersionItem> packageVersions))
                        {
                            packageVersions = new List<PackageVersionItem>();
                            packageVersionsById.Add(packageId, packageVersions);
                        }

                        //if (!packageVersions.Contains(packageVersion))
                        if(!packageVersions.Any(o=>o.Version.Equals(packageVersion)))
                        {
                            packageVersions.Add(new PackageVersionItem()
                            {
                                SourceFile = packagesConfigFilePath,
                                Version = packageVersion
                            });
                        }

                        if (packageVersions.Count > 1)
                        {
                            //breakpoint to examine package source
                        }
                    }
                }
            }

            List<KeyValuePair<string, ICollection<PackageVersionItem>>> packagesWithIncoherentVersions = packageVersionsById.Where(kv => kv.Value.Count > 1).ToList();

            // Assert
            string errorMessage = string.Empty;
            if (packagesWithIncoherentVersions.Any())
            {
                errorMessage = $"Some referenced packages have incoherent versions. Please fix them by adapting the nuget reference:{Environment.NewLine}";
                foreach (var packagesWithIncoherentVersion in packagesWithIncoherentVersions)
                {
                    string packageName = packagesWithIncoherentVersion.Key;
                    string packageVersions = string.Join("\n  ", packagesWithIncoherentVersion.Value);
                    errorMessage += $"{packageName}:\n  {packageVersions}\n\n";
                }
            }

            Assert.IsTrue(packagesWithIncoherentVersions.Count == 0,errorMessage);
            //Assert.IsEmpty(packagesWithIncoherentVersions, errorMessage);
        }

        private static IEnumerable<string> GetAllPackagesConfigFilePaths()
        {
            return Directory.GetFiles(GetBackendDirectoryPath(), PACKAGES_CONFIG_FILE_NAME, SearchOption.AllDirectories)
                .Where(o=>!o.Contains(".nuget"));
        }

        private static string GetBackendDirectoryPath()
        {
            string codeBase = Assembly.GetExecutingAssembly().CodeBase;
            var uri = new UriBuilder(codeBase);
            string path = Uri.UnescapeDataString(uri.Path);
            return Path.GetDirectoryName(path.Substring(0, path.IndexOf(BACKEND_DIRECTORY_NAME, StringComparison.Ordinal) + BACKEND_DIRECTORY_NAME.Length));
        }

    }

    public class PackageVersionItem
    {
        public string SourceFile { get; set; }
        public string Version { get; set; }

        public override string ToString()
        {
            return $"{Version} in {SourceFile}";
        }
    }
}

Thank you for asking this - so I am not alone. I put considerable time into ensuring all projects in my solution use the same package version. The NuGet user interface (and also the command line interface) also contribues to having different versions among the projects within a solution. In particular when a new project is added to the solution and package X shall be added to the new project, NuGet is overly greedy to download the latest version from nuget.org instead of using the local version first, which would be the better default handling.

I completely agree with you, that NuGet should warn if different versions of a package are used within a solution. And it should help avoiding this and fixing such version maze.

The best I found to do now is to enumerate all packages.config files within the solution folder (your projects-root) which look like

<?xml version="1.0" encoding="utf-8"?>
<packages>
  <package id="Newtonsoft.Json" version="6.0.6" targetFramework="net451" />
  ...
</packages>

then sorting the xml-nodes by id and analysing the version numbers.

If any package occurs with different version numbers, making them all equal and afterwards running the NuGet command

Update-Package -ProjectName 'acme.lab.project' -Reinstall

should fix wrong package versions.

(Since NuGet is open source it would certainly be a cool thing to get our hands dirty and implement the missing version-conflict avoidance utility.)

Additionally to the "Consolidate" tab in VS, you can use powershell Sync-Package https://docs.microsoft.com/en-us/nuget/reference/ps-reference/ps-ref-sync-package .

Examples:

# Sync the Elmah package installed in the default project into the other projects in the solution
Sync-Package Elmah

# Sync the Elmah package installed in the ClassLibrary1 project into other projects in the solution
Sync-Package Elmah -ProjectName ClassLibrary1

# Sync Microsoft.Aspnet.package but not its dependencies into the other projects in the solution
Sync-Package Microsoft.Aspnet.Mvc -IgnoreDependencies

# Sync jQuery.Validation and install the highest version of jQuery (a dependency) from the package source    
Sync-Package jQuery.Validation -DependencyVersion highest

You could use nuget's new "Central Package Management" feature.

Example problem:

Suppose you have monorepo (ie VS "solution" or VSCode "workspace") with multiple projects.

ProjectA.csproj :

<ItemGroup>
  <PackageReference Include="Foo.Bar.Baz" Version="1.0.0" />
  <PackageReference Include="Spam.Ham.Eggs" Version="4.0.0" />
</ItemGroup>

ProjectB.csproj :

<ItemGroup>
  <PackageReference Include="Foo.Bar.Qux" Version="1.2.3" />
  <PackageReference Include="Spam.Ham.Eggs" Version="4.5.6" />
</ItemGroup>

Some items are the same whereas others differ. And you need to remember to keep the versions in sync - the example shows that you forgot to do that!

Step 1: remove the versions

ProjectA.csproj :

<ItemGroup>
  <PackageReference Include="Foo.Bar.Baz" />
  <PackageReference Include="Spam.Ham.Eggs" />
</ItemGroup>

ProjectB.csproj :

<ItemGroup>
  <PackageReference Include="Foo.Bar.Qux" />
  <PackageReference Include="Spam.Ham.Eggs" Version="4.0.0" />  <!-- note version override for this project -->
</ItemGroup>

Step 2: add file named Directory.Packages.props to your repo's root

<Project>
  <PropertyGroup>
    <ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
  </PropertyGroup>
  <ItemGroup>
    <!-- use 'PackageVersion' rather than 'PackageReference' -->
    <PackageVersion Include="Foo.Bar.Baz" Version="1.2.3" />
    <PackageVersion Include="Foo.Bar.Qux" Version="1.2.3" />
    <PackageVersion Include="Spam.Ham.Eggs" Version="4.5.6" />
  </ItemGroup>
</Project>

Step 3: restore

For each project:

  • clear build output: dotnet clean
  • restore packages: dotnet restore

All your projects will now use the versions you've specified in the config file.

There are more options, like version overrides, and transitive dependency pinining - read the docs for more.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM