How to enforce same nuget package version across multiple c# projects?
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.)
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:
I'm now able to use Nuget features, such as adding props and targets. If you have a code generator (e.g. protobuf or thrift) this becomes pricesless.
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.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.
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}";
}
}
}