关于 System.Runtime.Loader.AssemblyLoadContext

AssemblyLoadContext 类是在 .NET Core 中引入的,在 .NET Framework 中不可用。 本文使用概念性信息来补充 AssemblyLoadContext API 文档。

本文与实现动态加载的开发人员(尤其是动态加载框架开发人员)相关。

什么是 AssemblyLoadContext?

每个 .NET 5+ 和 .NET Core 应用程序都隐式使用 AssemblyLoadContext。 它是运行时用于查找和加载依赖项的提供程序。 每当加载依赖项时,就会调用实例 AssemblyLoadContext 来查找它。

  • AssemblyLoadContext 提供查找、加载和缓存托管程序集和其他依赖项的服务。
  • 为了支持动态代码加载和卸载,它会创建一个独立上下文,用于在自己的 AssemblyLoadContext 实例中加载代码及其依赖项。

版本控制规则

单个 AssemblyLoadContext 实例限制为每个Assembly只加载 的一个版本。 当针对已加载同名程序集的 AssemblyLoadContext 实例解析程序集引用时,会将请求的版本与加载的版本进行比较。 仅当加载的版本等于或高于所请求的版本时,解析才会成功。

何时需要多个 AssemblyLoadContext 实例?

动态加载代码模块时,单个 AssemblyLoadContext 实例只能加载一个程序集版本的限制可能会成为问题。 每个模块都是独立编译的,模块可能依赖于不同版本的模块 Assembly。 当不同的模块依赖于常用库的不同版本时,这通常是一个问题。

为了支持动态加载代码,AssemblyLoadContext API 提供在同一个应用程序中加载不同版本的 Assembly 的功能。 每个 AssemblyLoadContext 实例提供一个唯一字典,用于将每个 AssemblyName.Name 字典映射到特定 Assembly 实例。

它还提供了一种方便的机制,用于对与代码模块相关的依赖项进行分组,以便以后卸载。

AssemblyLoadContext.Default 实例

实例 AssemblyLoadContext.Default 在启动时由运行时自动填充。 它使用 默认探测 来定位和查找所有静态依赖项。

它解决了最常见的依赖项加载方案。

动态依赖项

AssemblyLoadContext 具有可替代的各种事件和虚函数。

AssemblyLoadContext.Default 实例仅支持替代事件。

托管程序集加载算法附属程序集加载算法非托管(本机)库加载算法的文章详细介绍了所有可用的事件和虚拟函数。 文章显示加载算法中的每个事件和函数的相对位置。 本文不会重现该信息。

本部分介绍相关事件和函数的一般原则。

  • 可重复。 特定依赖项的查询必须始终产生相同的响应。 必须返回相同的加载依赖项实例。 此要求对于缓存一致性至关重要。 具体而言,对于托管程序集,我们将创建 Assembly 缓存。 缓存键是一个简单的程序集名称。 AssemblyName.Name
  • 通常不引发。 当找不到请求的依赖项时,这些函数应返回 null 而不是引发。 引发将提前结束搜索,并将异常传播到调用方。 应将引发限制为针对意外错误,如程序集损坏或内存不足等情况。
  • 避免递归。 请注意,这些函数和处理程序实现用于查找依赖项的加载规则。 实现不应调用触发递归的 API。 代码通常应调用需要特定路径或内存引用参数的 AssemblyLoadContext 加载函数。
  • 加载到正确的 AssemblyLoadContext。 加载依赖项的位置的选择特定于应用程序。 选择由这些事件和函数实现。 当代码调用 AssemblyLoadContext 按路径加载函数时,请在要加载代码的实例上调用它们。 有时返回 null 并让 AssemblyLoadContext.Default 处理负载可能是最简单的选项。
  • 注意线程争用。 加载可由多个线程触发。 AssemblyLoadContext 通过以原子方式将程序集添加到其缓存来处理线程争用。 将丢弃争用失败方的实例。 在实现逻辑中,不要添加无法正确处理多个线程的额外逻辑。

动态依赖项是如何隔离的?

每个AssemblyLoadContext实例代表Assembly实例和Type定义的唯一范围。

这些依赖项之间没有二进制隔离。 它们只是因为无法通过名字找到彼此而被孤立。

在每个 AssemblyLoadContext 中:

共享依赖项

可以在 AssemblyLoadContext 实例之间轻松共享依赖项。 常规模型用于一个 AssemblyLoadContext 来加载依赖项。 另一个通过使用对加载的程序集的引用来共享依赖项。

此共享是运行时程序集所必需的。 这些程序集只能加载到 AssemblyLoadContext.DefaultASP.NETWPFWinForms 等框架也是如此。

建议将共享依赖项加载到AssemblyLoadContext.Default。 此共享是常见的设计模式。

共享是在自定义 AssemblyLoadContext 实例的编码中实现的。 AssemblyLoadContext 具有可替代的各种事件和虚函数。 当其中任一函数返回对在另一个Assembly实例中加载的实例的引用时,该AssemblyLoadContext实例会被共享。 标准加载算法会将加载交由AssemblyLoadContext.Default,以简化常见的共享模式。 有关详细信息,请参阅 托管程序集加载算法

类型转换问题

当两个 AssemblyLoadContext 实例包含具有相同 name 的类型定义时,它们不是同一类型。 只有当它们来自同一实例时,它们才是相同的 Assembly 类型。

为了使问题复杂化,有关这些不匹配类型的异常消息可能会令人困惑。 这些类型按其简单类型名称在异常消息中引用。 在这种情况下,常见的异常消息采用以下形式:

无法将类型为“IsolatedType”的对象转换为类型“IsolatedType”。

调试类型转换问题

如果给定一对不匹配的类型,还必须要了解:

给定两个对象 ab,在调试器中评估以下内容将很有帮助:

// In debugger look at each assembly's instance, Location, and FullName
a.GetType().Assembly
b.GetType().Assembly
// In debugger look at each AssemblyLoadContext's instance and name
System.Runtime.Loader.AssemblyLoadContext.GetLoadContext(a.GetType().Assembly)
System.Runtime.Loader.AssemblyLoadContext.GetLoadContext(b.GetType().Assembly)

解决类型转换问题

有两种设计模式可用于解决这些类型转换问题。

  1. 使用常见的共享类型。 此共享类型可以是基元运行时类型,也可以涉及在共享程序集中创建新的共享类型。 共享类型通常是在应用程序程序集中定义的 接口 。 有关详细信息,请阅读 有关如何共享依赖项的信息。

  2. 使用封送处理技术从一种类型转换为另一种类型。