理解c# .NET中程序集的加载方式 返回

无论好坏,高级.net开发人员都需要了解.net运行时如何加载程序集。我们一直在处理库和NuGet包。这些库依赖于其他流行的库,并且有很多共享依赖。有了一个足够大的依赖网络,你最终会陷入冲突或困难的情况。
处理这类问题的最好方法是了解机制内部是如何工作的。
在本文中,您将看到. net进程如何以及何时加载被引用的程序集。
您将了解加载了哪个库版本,当有多个可用版本时会发生什么,以及为什么有时会由于版本冲突而出现问题。
您将看到如何调试这些类型的问题,请参阅程序集绑定日志(融合日志),并了解一些解决冲突的方法。
程序集、模块和引用
让我们从一些关于。net进程的基本术语开始。
. net中的程序集是DLL或EXE文件。Visual Studio解决方案中的每个项目都被编译为一个程序集。每个程序集可以包含多个模块,但在实践中,我们几乎总是在一个程序集中有一个模块,该模块的名称将与程序集相同。
在Visual Studio中启动进程或按F5时,启动项目程序集将被执行。它将是除。net Framework或。net Core程序集外加载的第一个程序集。之后,该流程将根据其需要在运行时加载其他程序集。只有在需要调用某个方法或使用该程序集的类型时,它才会惰性地加载程序集。
下面是为一个简单的“Hello World”. net框架项目加载的模块(模块和程序集对于我们所有的意图和目的都是相同的)。MyStartup.dll是这里的启动项目:

当从另一个项目引用一个项目时,在生成时,引用的项目的DLL或EXE将复制到启动项目的Bin文件夹中。它通常是二进制调试或二进制释放。在运行时,当您第一次使用引用项目中的类型时,CLR会在应用程序目录中查找具有相同名称和版本的DLL文件。然后,它将该程序集加载到流程中。这也称为绑定到程序集。
这里有一个例子:
假设我们有一个名为MyStartup的简单控制台应用程序,它引用了另一个名为Lib1的项目。MyStartup使用Lib1程序集中的一些类。
在MyStartup:
class Program
{
static void Main(string[] args)
{
int a = int.Parse(Console.ReadLine());
int b = int.Parse(Console.ReadLine());
Console.WriteLine("A + B = " + Add(a, b));
}
private static int Add(int a, int b)
{
var calculator = new Lib1.Calculator();
return calculator.Sum(a, b);
}
}
//In Lib1:
public class Calculator
{
public int Sum(int a, int b)
{
return a + b;
}
}当进入Main方法时,Lib1程序集还没有被加载。但是当输入Add方法时,CLR会尝试解析Calculator类型,找出它在一个引用的程序集Lib1中,然后尝试加载该程序集。
.NET中的程序集绑定
当CLR需要加载程序集时,逻辑实际上比在Bin文件夹中查找要复杂一些。下面是实际执行的逻辑(参见Microsoft的详细说明文档):
根据配置文件(app.config或web.config)确定需要加载的程序集版本。配置文件的名称将是(在构建之后)[可执行文件名].exe。配置或web . config。这里绑定重定向起作用了(稍后详细介绍)。
看看程序集是否已经加载。如果加载了不同的版本,则会抛出一个FileLoadException,除非它是一个强命名程序集,可以在多个版本中并排加载。
如果是强命名程序集,请检查全局程序集缓存(GAC)。GAC是机器上为多个应用程序共享程序集的地方。程序集缓存。它只能存储强名称程序集。它可以存储同一程序集的不同版本。你可以自己用gacutil.exe把它安装到GAC中。
如果它是一个强命名程序集,并且配置文件包含<codeBase>节点,它将检查那里的程序集位置。如果<codeBase>节点存在而没有找到程序集,则将引发FileNotFoundException异常。
根据启发式算法检查程序集DLL或EXE。这个过程叫做探测。算法如下:
检查文件夹[应用程序库]/[程序集名称].dll。应用程序库是应用程序可执行文件所在的地方。通常你的Bin\Debug或Bin\Release文件夹。
检查[应用程序库]/[程序集名称]/[程序集名称].dll
如果为引用的程序集指定了区域性信息,则只会检查以下目录:[应用程序库]/[区域性]/[程序集名称].dll[应用程序库]/[区域性]/[程序集名称]/[程序集名称].dll
如果配置文件中存在< exploration >节点,那么它将在该节点的privatePath属性指定的文件夹中查找程序集。
他们为什么要把一切都搞得这么复杂?
实际上,这个逻辑非常有助于我们发展,而不是使事情变得困难。它的存在是为了实现一些重要的目标:
要确保如果引用了特定的程序集和版本,则将加载确切的版本。否则,将引发异常。如果您知道自己在做什么,那么您可以在配置文件中指定覆盖规则(绑定重定向)。
允许灵活地加载程序集。例如,如果您希望根据不同的文化(语言)加载不同的程序集,那么您可以很容易地做到这一点。或者如果您想根据客户配置加载不同的程序集,也可以。
为了安全起见,我们使用了强命名的程序集。它们确保你不能“伪造”程序集。例如,如果一个进程希望加载Lib1 v4.5,那么您将无法加载具有相同名称和版本的恶意程序集。加载时将引发异常。这就是为机器上的所有进程共享的GAC只接受强命名程序集的原因。
在大多数应用程序中,您不需要记住程序集加载和探测的复杂逻辑。您不需要知道或考虑GAC、强命名程序集或操作配置文件。您基本上根本不需要考虑库的版本,因为可能的冲突可以通过一种称为绑定重定向的机制自动解决。