【问题标题】:How to resolve NuGet dependency hell如何解决 NuGet 依赖地狱
【发布时间】:2015-12-20 18:33:39
【问题描述】:

我开发了一个名为CompanyName.SDK 的函数库,它必须集成到公司项目CompanyName.SomeSolution

CompanyName.SDK.dll 必须通过 NuGet 包部署。 CompanyName.SDK 包依赖于第 3 方 NuGet 包。举个很好的例子,让我们以Unity 为例。当前依赖于 v3.5.1405-prereleaseUnity

CompanyName.SomeSolution.Project1 依赖于 Unity v2.1.505.2CompanyName.SomeSolution.Project2 依赖于 Unity v3.0.1304.1

CompanyName.SDK 集成到此解决方案中会增加对Unity v3.5.1405-prerelease 的依赖。 假设CompanyName.SomeSolution 有一个可运行的输出项目CompanyName.SomeSolution.Application,它依赖于上述两个和CompanyName.SDK

问题从这里开始。所有Unity 程序集在所有没有版本说明符的包中都具有相同的名称。在目标文件夹中,它将只有一个版本的Unity 程序集:v3.5.1405-prerelease 通过bindingRedirectapp.config

Project1Project2SDK 中的代码如何使用它们编码、编译和测试时所用的依赖包的完全需要的版本?

注意 1:Unity 只是一个例子,实际情况要糟糕 10 倍,因为 3rdparty 模块依赖于另一个 3rdparty 模块,而后者又同时具有 3-4 个版本。

注意 2: 我无法将所有软件包升级到最新版本,因为有些软件包依赖于其他软件包的非最新版本。

注意 3: 假设依赖包在版本之间有重大更改。这是我问这个问题的真正问题。

注意4:我知道about conflicts between different versions of the same dependent assembly 的问题,但那里的答案并不能解决问题的根源——他们只是隐藏它。

NOTE5: 承诺的“DLL Hell”问题解决方案到底在哪里?它只是从另一个位置重新出现。

注意6:如果您认为使用 GAC 是一种选择,请编写分步指南或给我一些链接。

【问题讨论】:

  • 5) DLL 地狱的问题是您不能在 系统 上拥有同一个 DLL 的两个不同版本。这已经解决了。这并不意味着 DLL 的所有问题都已解决 :D 依赖关系仍然很棘手,而且它们总是很棘手,真的。版本是否向后兼容?如果是这种情况,您可以使用程序集绑定重定向。
  • @Luaan 见注 3。当它不使用 bindingRedirects 时,我必须要求其他开发人员更新他们项目中的旧包,但这不是解决方案。他们只是没有时间和预算。
  • NOTE2 是杀手。如果你不能解决这个问题,那么我只能建议将 Project1、Project2 和 SDK 部署到它们自己的运行时目录中,这样它们就可以拥有特定于版本的 3rd 方依赖项。
  • 是的,破坏性更改总是一个问题。没有真正的解决方案——你有一个不兼容的依赖链。 可以在同一个进程和同一个AppDomain 中加载同一个程序集的不同版本,但这是一个非常丑陋的hack,它会以微妙的方式中断。如果您可以将代码分成不同的进程或至少 AppDomain,这会变得容易得多 - 您只需要维护几个接口库。
  • 想一想,为每个单独的统一版本添加一个代码库元素会起作用吗?像这样:stackoverflow.com/questions/638310/…

标签: c# .net nuget dependency-management


【解决方案1】:

Unity 包不是一个很好的例子,因为你应该只在一个名为Composition Root 的地方使用它。 Composition Root 应该尽可能靠近应用程序入口点。在你的例子中是CompanyName.SomeSolution.Application

除此之外,我现在工作的地方也出现了完全相同的问题。而我所看到的,这个问题通常是由诸如日志记录之类的横切关注点引入的。您可以应用的解决方案是将您的第三方依赖项转换为第一方依赖项。您可以通过为这些概念引入抽象来做到这一点。实际上,这样做还有其他好处,例如:

  • 更多可维护代码
  • 更好的可测试性
  • 摆脱不必要的依赖(CompanyName.SDK 的每个客户端真的需要Unity 依赖?)

那么,让我们以假想的.NET Logging库为例:

CompanyName.SDK.dll 取决于 .NET Logging 3.0
CompanyName.SomeSolution.Project1 取决于 .NET Logging 2.0
CompanyName.SomeSolution.Project2 取决于 .NET Logging 1.0

.NET Logging 的版本之间存在重大变化。

你可以通过引入ILogger接口来创建自己的第一方依赖:

public interface ILogger
{
    void LogWarning();
    void LogError();
    void LogInfo();
} 

CompanyName.SomeSolution.Project1CompanyName.SomeSolution.Project2 应该使用ILogger 接口。它们依赖于ILogger 接口第一方依赖。现在您将.NET Logging 库保留在一个地方,并且很容易执行更新,因为您必须在一个地方进行更新。此外,版本之间的中断更改不再是问题,因为使用了一个版本的 .NET Logging 库。

ILogger 接口的实际实现应该在不同的程序集中,并且应该只在您引用.NET Logging 库的地方。 在您编写应用程序的CompanyName.SomeSolution.Application 中,您现在应该将ILogger 抽象映射到具体实现。

我们正在使用这种方法,并且我们还使用NuGet 来分发我们的抽象和实现。不幸的是,版本问题可能会出现在您自己的软件包中。为避免该问题,请在您通过 NuGet 为您的公司部署的包中应用 Semantic Versioning。如果通过NuGet 分发的代码库发生变化,您应该更改通过NuGet 分发的所有包。例如我们在本地的NuGet 服务器:

  • DomainModel
  • Services.Implementation.SomeFancyMessagingLibrary(引用DomainModelSomeFancyMessagingLibrary
  • 还有更多...

此包之间的版本是同步的,如果DomainModel中的版本发生变化,Services.Implementation.SomeFancyMessagingLibrary中的版本相同。如果我们的应用程序需要更新我们的内部包,所有依赖项都会更新到相同的版本。

【讨论】:

  • 所以实际上我需要在each 3rd 方库之上构建一个抽象层?在典型的项目中,我有 15 个库与它们的深度集成调用(例如 System.Collections.Immutable 包)。不知道是否在所有情况下都可能。
  • 我认为这个解决方案适用于每个跨领域关注的第三方库。我最近不得不对记录器做同样的事情。
  • @kpa6uk 您不必为第 3 方库中的每个类提供具有 1-1 方法映射的包装器。您应该始终提供一个描述您的业务逻辑 的包装器,并且只是偶然地它可以在其中包含一个 3rd 方库。这样,您的抽象总是描述需要得到满足。如何满足是次要关注的低级实现细节。
  • @BartoszKP 我认为这是“解决”问题的一种非常冒险的方式。我什至会说,这不是解决他的问题,而只是将其转移到第一方。如果您仅使用自定义接口映射覆盖 3rd 方库的一部分,那么将来每次您需要原始库的另一种方法时,您都必须重写包装库。并且也可能发生某些需要的方法和方法确实存在于原始库的一个版本中
  • 我同意 Unity 应该只被应用程序的顶层引用,所以依赖层应该完全不知道它。你忘了提这是facade pattern 的经典示例,它通常用于将复杂的 API 简化为仅包含应用程序感兴趣的成员的 API(但不必映射 1-1 )。总而言之,这是一种抽象处理横切关注点的第三方库的好方法。
【解决方案2】:

您可以在编译后汇编级别工作以解决此问题...

选项 1

您可以尝试将程序集与ILMerge 合并

ilmerge /target:winexe /out:SelfContainedProgram.exe Program.exe ClassLibrary1.dll ClassLibrary2.dll

结果将是一个程序集,它是您的项目及其所需依赖项的总和。这会带来一些缺点,比如牺牲单声道支持和丢失程序集标识(名称、版本、文化等),所以当所有要合并的程序集都由您构建时,这是最好的。

所以来了……

选项 2

您可以将依赖项作为资源嵌入到您的项目中,如this article 中所述。以下是相关部分:

在运行时,CLR 将无法找到依赖的 DLL 组件,这是一个问题。要解决此问题,当您的应用程序 初始化,注册一个回调方法到 AppDomain 的 ResolveAssembly 事件。代码应如下所示:

AppDomain.CurrentDomain.AssemblyResolve += (sender, args) => {

   String resourceName = "AssemblyLoadingAndReflection." +

      new AssemblyName(args.Name).Name + ".dll";

   using (var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(resourceName)) {

      Byte[] assemblyData = new Byte[stream.Length];

      stream.Read(assemblyData, 0, assemblyData.Length);

      return Assembly.Load(assemblyData);

   }

};

现在,第一次线程调用一个引用类型的方法 一个依赖的 DLL 文件,AssemblyResolve 事件将被引发并且 上面显示的回调代码将找到所需的嵌入式 DLL 资源 并通过调用 Assembly 的 Load 方法的重载来加载它 将 Byte[] 作为参数。

如果我在你的情况下,我认为这是我会使用的选项,牺牲一些初始启动时间。

更新

看看here。您还可以尝试在每个项目的 app.config 中使用那些 <probing> 标记来定义一个自定义子文件夹,以便在 CLR 搜索程序集时查看。

【讨论】:

  • 哇,这是管理在架构上被旧 lib 版本卡住的项目的依赖项的真正好方法。但从解决方案的角度来看,我会说“地狱愈演愈烈”。当然,我知道我们无法使用我们拥有的工具和不断变化的 API 来解决这个问题。另外,它不是很慢而且没有缓存程序集吗?我们是否应该每次手动 AOT 编译这些程序集以使性能接近常规依赖项?
  • @maiksaray 看看here。您可以尝试在每个项目的 .config-文件中使用那些 <probing> 标记来定义自定义子文件夹,以便在 CLR 搜索程序集时查看。
猜你喜欢
  • 1970-01-01
  • 2015-12-21
  • 1970-01-01
  • 1970-01-01
  • 2017-11-21
  • 2021-07-16
  • 1970-01-01
  • 2019-07-18
  • 2017-07-20
相关资源
最近更新 更多