默认ALC(默认ALC)
当一个应用程序启动时,CLR将一个特殊的ALC分配给静态的Assembly LoadContext.Default属性。
加载启动程序集及其静态引用的依赖项和 .NET 运行时 BCL 程序集都是在默认 ALC 中完成的。
默认 ALC 首先在默认探测路径(default probing paths)中查找以自动解析程序集。默认探测路径 通常是应用程序的 .deps.json 和 .runtimeconfig.json 文件中指示的位置。
如果 ALC 在其默认探测路径中找不到程序集,则会触发其 Resolving 事件。你可以通过处理此事件去从其他位置加载程序集,您也可以将应用程序的依赖项部署到其他位置,例如子文件夹、共享文件夹,甚至作为主机程序集中的二进制资源:
AssemblyLoadContext.Default.Resolving += (loadContext, assemblyName) =>
{
// Try to locate assemblyName, returning an Assembly object or null.
// Typically you’d call LoadFromAssemblyPath after finding the file.
// ...
};
当自定义 ALC 无法解析时(换句话说,当其 Load 方法返回 null 时)并且默认 ALC 无法解析程序集时,默认 ALC 中的 Resolving 事件也会触发。
你也可以不通过解析事件(Resolving event)将程序集加载到默认ALC。然而,在继续之前,你应该首先确定你是否可以通过使用一个单独的ALC或使用我们在下一节中描述的方法(使用执行和上下文ALC)来更好地解决问题。对默认 ALC 进行硬编码会使您的代码变得脆弱,因为它不能作为一个整体被隔离(例如,通过单元测试框架或 LINQPad)。
如果您仍想继续,最好调用解析方法(即 LoadFromAssemblyName)而不是加载方法(例如 LoadFromAssemblyPath)——尤其是当您的程序集被静态引用时。这是因为程序集可能已经加载,在这种情况下 LoadFromAssemblyName 将返回已加载的程序集,而 LoadFromAssemblyPath 将抛出异常。
(使用LoadFromAssemblyPath,你也有可能从一个与ALC的默认解析机制不一致的地方加载程序集的风险。)
如果程序集位于 ALC 不会自动找到它的地方,您仍然可以按照此过程并额外处理 ALC 的 Resolving 事件。
注意,在调用 LoadFromAssemblyName 时,无需提供全名;简单的名称就可以了(即使程序集被强命名也是有效的):
AssemblyLoadContext.Default.LoadFromAssemblyName ("System.Xml");
但是,如果您在名称中包含公钥令牌,它必须与加载的内容相匹配。
默认探测(Default probing)
默认探测路径通常包含以下几个:
- AppName.deps.json中指定的路径(其中AppName是你的应用程序的主程序集的名称)。如果这个文件不存在,就会使用应用程序的根目录来代替。
- 包含.NET运行时系统程序集的文件夹(如果你的应用程序的部署模式是依赖框架的)。
MSBuild 自动生成一个名为 AppName.deps.json 的文件,它描述了在哪里可以找到它的所有依赖项。
其中包括放置在应用程序根目录下的与平台无关的程序集,以及放置在 win 或 unix 等子文件夹下的 runtimes\ 子目录中的特定于平台的程序集。
在生成的 .deps.json 文件中指定的路径是相对于应用程序根目录的——或者您在 AppName.runtimeconfig.json or AppName.runtimeconfig.dev.json 配置文件的 additionalProbingPaths 部分中指定的任何其他文件夹(后者仅用于开发环境)。
当前ALC(“Current” ALC)
在上一节中,我们告诫不要将程序集显式加载到默认 ALC 中。相反,您通常想要的是加载/解析到“当前”ALC。
在大多数情况下,“当前” ALC是包含当前正在执行程序集的ALC:
var executingAssem = Assembly.GetExecutingAssembly();
var alc = AssemblyLoadContext.GetLoadContext (executingAssem);
Assembly assem = alc.LoadFromAssemblyName (...); // to resolve by name
// OR: = alc.LoadFromAssemblyPath (...); // to load by path
获取ALC有一种更灵活,更明确的方法:
var myAssem = typeof (SomeTypeInMyAssembly).Assembly;
var alc = AssemblyLoadContext.GetLoadContext (myAssem);
...
有时,无法推断出 "当前 "的ALC。例如,假设您负责编写 .NET 二进制序列化程序,在这个程序中你写入它序列化的类型的全名(包括它们的程序集名称),然后这些名称必须在反序列化期间解析。问题是,应该使用哪种 ALC来解析?而依赖当前执行程序集的问题是,它将返回任何包含反序列化器的程序集,而不是调用反序列化器的程序集。
最好的解决办法明确是哪个ALC:
public object Deserialize (Stream stream, AssemblyLoadContext alc) {
...
}
明确的方式可以最大限度地提高灵活性,最大限度地减少犯错的机会:
var assem = typeof (SomeTypeThatIWillBeDeserializing).Assembly;
var alc = AssemblyLoadContext.GetLoadContext (assem);
var object = Deserialize (someStream, alc);
Assembly.Load and 上下文 ALCs
下列展示将一个Assembly加载到当前执行的ALC:
var executingAssem = Assembly.GetExecutingAssembly();
var alc = AssemblyLoadContext.GetLoadContext (executingAssem);
Assembly assem = alc.LoadFromAssemblyName (...);
微软在Assembly类中定义了以下方法:
public static Assembly Load (string assemblyString)
以及一个接受AssemblyName作为参数的重载版本:
public static Assembly Load (AssemblyName assemblyRef);
(不要将这些方法与传统的 Load(byte[]) 方法混淆,后者的行为方式完全不同,将在下面介绍)
与 LoadFromAssemblyName 一样,您可以选择指定程序集的简单名称、部分名称或全名:
Assembly a = Assembly.Load ("System.Private.Xml");
这会将 System.Private.Xml 程序集加载到正在执行代码程序集的任何 ALC 中。
在这种情况下,我们指定了一个简单的名字。以下参数也是有效的,且在.NET中都会有相同的结果
"System.Private.Xml, PublicKeyToken=cc7b13ffcd2ddd51"
"System.Private.Xml, Version=4.0.1.0"
"System.Private.Xml, Version=4.0.1.0, PublicKeyToken=cc7b13ffcd2ddd51"
如果要指定一个公钥令牌,那么它必须与加载的程序集相匹配。
这两个方法都是严格意义上的解析,所以不能指定一个文件路径。(如果你在AssemblyName对象中填入CodeBase属性,会被忽略掉)。
自定义Assembly.Load
如果你想自定义一个Assembly.Load,可以参考下面的模板:
[MethodImpl(MethodImplOptions.NoInlining)]
Assembly Load (string name)
{
Assembly callingAssembly = Assembly.GetCallingAssembly();
var callingAlc = AssemblyLoadContext.GetLoadContext (callingAssembly);
return callingAlc.LoadFromAssemblyName (new AssemblyName (name));
}
EnterContextualReflection
当Assembly.Load通过中间人(如反序列化器或单元测试运行器)被调用时,Assembly.Load使用调用者程序集的ALC上下文的策略将会失败。
如果中间人被定义在一个不同的程序集中,那么中间人的加载上下文被使用,而不是调用者的加载上下文。
为了让Assembly.Load在这种情况下仍能工作,微软为AssemblyLoadContext增加了一个方法,叫做EnterContextualReflection。它会为AssemblyLoadContext.CurrentContextualReflectionContext分配一个ALC。虽然这是一个静态属性,但它的值存储在一个 AsyncLocal 变量中,因此它可以在不同的线程上保存不同的值(但在整个异步操作中仍会保留)。
如果此属性非空,Assembly.Load 会自动优先使用它而不是调用方的ALC。
Method1();
var myALC = new AssemblyLoadContext ("test");
using (myALC.EnterContextualReflection()) {
Console.WriteLine (
AssemblyLoadContext.CurrentContextualReflectionContext.Name); // test
Method2();
}
// Once disposed, EnterContextualReflection() no longer has an effect.
Method3();
void Method1() => Assembly.Load ("..."); // Will use calling ALC
void Method2() => Assembly.Load ("..."); // Will use myALC
void Method3() => Assembly.Load ("..."); // Will use calling ALC
我们之前演示了如何编写和Assembly.Load功能类似的方法。下面给出更准确的版本,它考虑了上下文中的反射上下文(contextual reflection context)的场景:
[MethodImpl(MethodImplOptions.NoInlining)]
Assembly Load (string name)
{
var alc = AssemblyLoadContext.CurrentContextualReflectionContext
?? AssemblyLoadContext.GetLoadContext (Assembly.GetCallingAssembly());
return alc.LoadFromAssemblyName (new AssemblyName (name));
}
尽管上下文中的反射上下文背景下(contextual reflection context)在允许遗留代码运行方面很有用,但更健壮的解决方案(如我们之前所述)是修改调用 Assembly.Load 的代码,使其在调用者传递进来的ALC上调用LoadFromAssemblyName。