如何解决NuGet依赖地狱问题

58

我开发了一个名为 CompanyName.SDK 的功能库,必须将其集成到公司项目 CompanyName.SomeSolution 中。

CompanyName.SDK.dll 必须通过 NuGet 包部署。而 CompanyName.SDK 包又依赖于第三方 NuGet 包。举个好例子,比如说 Unity。当前依赖于 v3.5.1405-prerelease 版本的 Unity

CompanyName.SomeSolution.Project1 依赖于 Unityv2.1.505.2 版本,CompanyName.SomeSolution.Project2 依赖于 Unityv3.0.1304.1 版本。

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

问题在这里开始。所有的 Unity 程序集都没有版本说明符,所有包中的程序集名称都是相同的。而在目标文件夹中,只会有一个版本的 Unity 程序集:v3.5.1405-prerelease 版本,通过 app.config 中的 bindingRedirect 实现。

如何让 Project1Project2SDK 代码使用它们编写、编译和测试的确切版本的依赖包?

注1:这里举的只是 Unity 的例子,实际情况要复杂10倍,因为第三方模块依赖于另外的第三方模块,而这些模块又同时拥有3-4个不同的版本。

注意2:我不能将所有软件包升级到它们的最新版本,因为有些软件包具有依赖于另一个软件包的非最新版本的问题。

注意3:假设依赖软件包在版本之间存在重大更改。这就是我提出这个问题的真正问题所在。

注意4:我知道有一个关于“发现同一依赖程序集的不同版本之间的冲突”的问题,但那里的答案并没有解决问题的根源--它们只是隐藏了它。

注意5:那个承诺的“DLL地狱”问题的解决方案到哪里去了?它只是从另一个位置重新出现了。

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


  1. DLL地狱是指在系统上不能同时存在两个不同版本的相同DLL的问题。这个问题已经得到解决。但这并不意味着所有与DLL相关的问题都已经解决:D 依赖关系仍然很棘手,而且它们永远都会是棘手的。版本是否向后兼容?如果是这种情况,可以使用程序集绑定重定向。
- Luaan
@Luaan 请看NOTE3。当绑定重定向无法正常工作时,我必须要求其他开发人员更新他们项目中的旧包,但这并不是一个解决方案。他们只是没有时间和预算来做这件事。 - v.karbovnichy
1
注意2是致命的。如果您无法解决它,我只能建议将Project1、Project2和SDK部署到它们自己的运行时目录中,以便它们可以拥有特定版本的第三方依赖项。 - Polyfun
是的,破坏性变更总是一个问题。没有真正的解决方案 - 你有一个不兼容的依赖链。在同一进程和同一AppDomain中加载相同程序集的不同版本是可能的,但这是一个非常丑陋的hack,并且会以微妙的方式破坏。如果您可以将代码分离到不同的进程或至少AppDomains中,这将变得更加容易 - 您只需要维护几个接口库即可。 - Luaan
各位,你们觉得这个技术怎么样? - v.karbovnichy
显示剩余9条评论
2个回答

14

Unity包不是一个好的例子,因为您应该只在称为组合根的一个地方使用它。而且组合根应该尽可能靠近应用程序入口点。在您的示例中,它是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中也有相同的版本。如果我们的应用程序需要更新内部软件包,所有依赖项都将更新为相同的版本。


5
事实上,我需要在每个第三方库之上构建一个抽象层吗?在典型的项目中,我有15个库,它们之间有深度集成的调用(例如System.Collections.Immutable包)。我不确定在所有情况下都是否可行。 - v.karbovnichy
我认为这个解决方案适用于每个跨领域关注点的第三方库。最近我也不得不使用记录器做同样的事情。 - Moby Disk
1
@kpa6uk,你不需要为第三方库中的每个类提供一个1-1方法映射的包装器。你应该始终提供一个描述你的业务逻辑的包装器,只有偶然情况下才会包含第三方库。这样,你的抽象层总是描述了需要满足的需求。如何满足是次要的,是低级实现细节。 - BartoszKP
1
@BartoszKP 我认为这是一种非常冒险的“解决”问题的方式。我甚至可以说,这不是解决问题,而只是将其移动到第一方。如果您仅使用自定义接口映射覆盖第三方库的一部分,在未来,每当您需要原始库的另一个方法时,都必须重新编写包装库。而且,有可能某些所需的方法和方法在原始库的某个版本中确实存在。 - maiksaray
2
我认同Unity只应该被应用程序的顶层引用,所以依赖层应该完全不知道它的存在。你忘了提到这是一个典型的外观模式示例,通常用于将复杂的API简化为仅包含应用程序感兴趣的成员(但不必一一映射)。总的来说,这是一个很好的抽象第三方库处理横切关注点的方法。 - NightOwl888
显示剩余2条评论

14

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

选项1

您可以尝试使用ILMerge合并程序集

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

结果将是一个总和为您的项目及其所需依赖项的程序集。这带来了一些缺点,例如牺牲mono支持和失去程序集身份(名称、版本、文化等),因此当所有要合并的程序集都是由您构建时,这是最好的选择。

所以这里有...

选项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文件中使用那些标签来定义CLR搜索程序集时要查找的自定义子文件夹。

哇,这是一种非常好的方式来管理架构上卡在旧库版本上的项目的依赖关系。但从解决方案的角度来看,我会说“Dll地狱加剧了”。当然,我知道我们不能用现有工具和API不断变化的情况下做任何事情。另外,这不是超级慢且没有装配缓存吗?我们是否应该每次手动AOT编译这些程序集以使性能接近常规依赖项? - maiksaray
@maiksaray 请看这里:https://msdn.microsoft.com/zh-cn/library/4191fzwb(v=vs.110).aspx。你可以尝试在每个项目的.config文件中使用<probing>标签来定义自定义子文件夹,以便CLR在搜索程序集时查找。 - beppe9000

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接