如何解决NuGet依赖地狱
本文关键字:NuGet 依赖 地狱 解决 何解决 | 更新日期: 2023-09-27 18:14:03
我开发了一个名为CompanyName.SDK
的函数库,该函数必须集成在公司项目CompanyName.SomeSolution
中
CompanyName.SDK.dll
必须通过NuGet包部署。CompanyName.SDK
包依赖第三方NuGet包。举个例子,让我们以Unity
为例。当前依赖于Unity
的v3.5.1405-prerelease
。
CompanyName.SomeSolution.Project1
依赖于Unity
v2.1.505.2
。CompanyName.SomeSolution.Project2
依赖于Unity
v3.0.1304.1
将CompanyName.SDK
集成到此解决方案中会增加对Unity
v3.5.1405-prerelease
的依赖。让我们假设CompanyName.SomeSolution
有一个可运行的输出项目CompanyName.SomeSolution.Application
,它依赖于上面两个项目和CompanyName.SDK
问题开始了。所有Unity
程序集在没有版本说明符的所有包中都具有相同的名称。在目标文件夹中,Unity
程序集将只有一个版本:app.config
中的v3.5.1405-prerelease
到bindingRedirect
。
Project1
, Project2
和SDK
中的代码如何使用他们编码,编译和测试的依赖包的确切版本?
注1: Unity
只是一个例子,实际情况是10倍的第三方模块依赖于另一个第三方模块,反过来同时有3-4个版本。
注2:我不能将所有包升级到它们的最新版本,因为有些包依赖于另一个包的非最新版本。
注3:假设依赖包在版本之间有破坏性的更改。这是我问这个问题的真正问题。
注4:我知道关于相同依赖程序集的不同版本之间冲突的问题,但是那里的答案并不能解决问题的根源-他们只是隐藏它。
NOTE5:承诺的"DLL地狱"问题解决方案到底在哪里?它只是从另一个位置重新出现。
NOTE6:如果你认为使用GAC是一种选择,请写一步一步的指南或给我一些链接。
Unity
包不是一个很好的例子,因为您应该只在一个称为组合根的地方使用它。Composition Root
应该尽可能接近应用程序入口点。在您的示例中,它是CompanyName.SomeSolution.Application
除此之外,我现在工作的地方,出现了完全相同的问题。在我看来,这个问题通常是由诸如伐木之类的横切关注点引入的。您可以应用的解决方案是将第三方依赖转换为第一方依赖。您可以通过为这些概念引入抽象来做到这一点。实际上,这样做还有其他好处,比如:
- 更多可维护代码 <
- 更好em>可测试性
- 摆脱不必要的依赖(
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.Project1
和CompanyName.SomeSolution.Project2
应使用ILogger
接口。它们依赖于ILogger
接口的第一方依赖。现在你将.NET Logging
库保存在一个地方,并且很容易执行更新,因为你必须在一个地方进行更新。此外,版本之间的破坏性更改不再是一个问题,因为使用了.NET Logging
库的一个版本。
ILogger
接口的实际实现应该在不同的汇编中,它应该只在引用.NET Logging
库的地方。在您编写应用程序的CompanyName.SomeSolution.Application
中,您现在应该将ILogger
抽象映射到具体实现。
我们正在使用这种方法,我们也使用NuGet
来分发我们的抽象和实现。不幸的是,版本问题可能出现在您自己的包中。为了避免这些问题,在您通过NuGet
为您的公司部署的包中应用语义版本控制。如果在通过NuGet
分发的代码库中发生了某些更改,则应该更改通过NuGet
分发的所有包。例如,我们在本地NuGet
服务器中有:
-
DomainModel
-
Services.Implementation.SomeFancyMessagingLibrary
(引用DomainModel
和SomeFancyMessagingLibrary
) - 和更多…
DomainModel
中修改了Version,则在Services.Implementation.SomeFancyMessagingLibrary
中修改了相同的版本。如果我们的应用程序需要更新我们的内部包,所有依赖项都会更新到相同的版本。
您可以在编译后的程序集级别使用…
来解决此问题。选项1
你可以尝试用ILMerge
合并程序集ilmerge/target:winexe/out:SelfContainedProgram.exe Program.exe
结果将是一个程序集,它是项目及其所需依赖项的总和。这有一些缺点,比如牺牲单声道支持和丢失程序集标识(名称,版本,区域性等),所以当所有要合并的程序集都是由您构建时,这是最好的。
所以来了…
选项2
您可以将依赖项作为资源嵌入到您的项目中,如本文所述。以下是相关部分:
在运行时,CLR将无法找到依赖的DLL集合,这是一个问题。要解决这个问题,当您的应用程序初始化,注册一个回调方法到AppDomainResolveAssembly 事件。代码应该看起来像这样:
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[]作为参数。
我想如果我是你,我会选择这个选项,牺牲一些初始启动时间。
更新
看这里。您还可以尝试在每个项目的app.config中使用这些<probing>
标记来定义一个自定义子文件夹,以便在CLR搜索程序集时查看。