Supporting multiple versions of NuGet package in o

2020-03-12 05:59发布

问题:

What I want

I want my library to work with a range of versions of a NuGet package with breaking changes in API between the changes. I haven't investigated it further, but this path looks promising:

  • Reference all versions of the library with different APIs by specifying extern namespace aliases.
  • Create proxies for the needed classes, with flags/exceptions/whatever to tell what's actually supported.
  • Choose the correct proxy at runtime depending on what version is actually loaded into the application.
  • Code which depends on non-existent API won't be called, so everything should work fine.

While this may seem complicated, it has many benefits over a more straightforward approach of supporting every version in a separate assembly:

  • Version of my library won't become a mess like 1.2.3-for-2.3.4-to-2.6.8. I don't even know how versioning is supposed to work in this case.
  • NuGet users won't have to choose between several packages, one package fits all.
  • Upgrading versions would be straightforward, won't require removing and adding my package.

Problem

However, it's unclear whether it's possible at all. Even before getting to proxies and detecting current version, I'm stuck with the basics.

I can't even add multiple PackageReference nodes to my .csproj, only one reference actually works. There's a workaround for adding extern aliases which aren't supported by NuGet directly, but I can't get to that point because I can't get two references. And if I somehow get two, I won't be able to tell them apart.

Questions

  1. Can support for multiple versions be implemented this way, using extern namespace aliases and proxies?
  2. If yes, how to add references to multiple versions of a NuGet package and use them in code?
  3. If not, what is the correct approach then?

Background

I'm working on CsConsoleFormat library for formatting Console output. I want to support all relevant versions of popular command-line packages directly, so that pretty command line help and stuff like this could be added with almost no coding, no matter what command line parsing library is used.

I guess declaring "I support only the latest version" is somewhat acceptable in my case, but I'd rather have wider support even if it's more complicated. Ideally, I want a NuGet package which declares dependency on the lowest supported version, but supports everything up to the latest version.

Progress so far

I kinda got it to work, but with many issues. See issue on GitHub NuGet Home for more details.

回答1:

If you insist on extern aliases - you can add multiple version references directly, as dll file, not as nuget package.

Suppose I want to take a dependency on Newtonsoft.Json package version 10.0.3+. However if user has version 11 installed - I want to use generic JsonConverter<T> class available only in this version (11). Then my csproj might look like this:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>netstandard2.0</TargetFramework>
    <Version>1.0.4</Version>
  </PropertyGroup>
  <ItemGroup>
    <!-- Nuget reference -->
    <!-- Only this one will be included as dependency to the packed nuget -->
    <PackageReference Include="Newtonsoft.Json" Version="10.0.3" />
  </ItemGroup>
  <ItemGroup>
    <!-- Direct reference to the specific version -->
    <Reference Include="Newtonsoft.Json, Version=11.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed">
      <!-- Path to v11 dll -->
      <HintPath>Newtonsoft.Json.v11.dll</HintPath>
      <Aliases>js11</Aliases>
      <SpecificVersion>true</SpecificVersion>
    </Reference>    
  </ItemGroup>
</Project>

Then I have proxy interface:

public interface ISerializer {
    string Serialize<T>(T obj);
}

And two implementations, v10 (uses global, non-aliased namespace):

using System;
using global::Newtonsoft.Json;

namespace NugetRefMain {
    internal class Js10Serializer : ISerializer
    {
        public string Serialize<T>(T obj)
        {
            Console.WriteLine(typeof(JsonConvert));
            return JsonConvert.SerializeObject(obj);
        }
    }
}

And v11

extern alias js11;
using System;
using js11::Newtonsoft.Json;

namespace NugetRefMain {
    internal class Js11Serializer : ISerializer {
        public string Serialize<T>(T obj) {
            // using JsonConverter<T>, only available in v11
            Console.WriteLine(typeof(JsonConverter<T>));
            return JsonConvert.SerializeObject(obj);
        }
    }
}

And finally factory which creates serializer depending on current available json.net version:

public static class Serializers {
    public static ISerializer Create() {
        var version = typeof(JsonConvert).Assembly.GetName().Version;
        if (version.Major == 10)
            return new Js10Serializer();
        return new Js11Serializer();
    }
}

Now if I pack this as nuget - it will have single dependency on Newtonsoft.Json version 10.0.3 and that's all. However, if user installs Newtonsoft.Json of version 11 - it will use capabilities available in this version.

Drawbacks:

  • Visual Studio \ Resharper intellisense doesn't like this approach sometimes and shows intellisense errors when in reality everything compiles just fine.

  • You might have "version conflict" warnings on compilation.



回答2:

NuGet only resolves single package versions.

If you declare a dependency on the minimum supported version, any referencing project can upgrade the dependency to a newer version.

As long as the authors of the dependent package don't introduce breaking changes, it should work find.

Even if you use reflection to look at the actual assembly versions used, you will find that many package authors don't change the assembly version between releases. This is to avoid the need for binding redirects in classic .NET Framework projects as all versions are the same and NuGet will select the right DLL based on the resolved package version of the consuming project. Again, this is good for as long as there are no breaking changes.

A pattern you could use to support different packages is to provide many "platform" packages that the consumer can choose from. The platform-specific package would then reference a common package with shareable logic.

The "platform" would then be e.g. "MyLogic.XUnit" or "MyLogic.NUnit" (assuming test helpers as example) referencing "MyLogic.Common"



回答3:

This is not a complete answer but I noticed on your GitHub issue page that you are referencing both .NET Standard and .NET Framework libraries in your project. This is known to NOT work correctly.

Quoting .NET Standard team's announcement,

.. Another symptom is warnings at build time regarding assembly versions ..

which may be what you are running into.