C#插件式开发——详解默认ALC和当前ALC的区别(AssemblyLoadContext)

默认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。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值