原文:.NET 的程序集加载上下文

我们编写的 .NET 应用程序会使用到各种各样的依赖库。我们都知道 CLR 会在一些路径下帮助我们程序找到依赖,但如果我们需要手动控制程序集加载路径的话,需要了解程序集加载上下文。

如果你不了解程序集加载上下文,你可能会发现你加载了程序集却不能使用其中的类型;或者把同一个程序集加载了两次,导致使用到两个明明是一样的类型时却抛出异常提示不是同一个类型的问题。


程序集加载上下文

当你向应用程序域中加载一个程序集时,可能会加载到以下四种不同的上下文中的一种:

  1. 默认加载上下文(the Default Load Context)
  2. 加载位置加载上下文(the Load-From Context)
  3. 仅反射上下文(the Reflection-Only Context)
  4. 无上下文

你需要了解这些加载上下文,因为跨不同加载上下文加载的程序集是不能访问其中的类型的。

默认加载上下文

  • 在全局程序集缓存中发现的类型会加载到默认加载上下文中
  • 位于应用程序探测路径中的程序集会加载到默认加载上下文中,这包括了 ApplicationBasePrivateBinPath 目录中发现的程序集
  • Assembly.Load 方法的大多数重载都将程序集加载到此上下文中

ApplicationBasePrivateBinPath 这两个属性虽然允许被设置,但它们只对新生成的 AppDomain 生效,直接设置当前 AppDomain 中这两个属性的值并不会产生任何效果。

虽然我们不能直接设置这两个属性,但可以在应用程序的 App.config 文件这配置 configuration -> runtime -> assemblyBinding -> probing.privatePath 属性来设置多个应用程序执行时的依赖探测路径。

将程序集加载到默认加载上下文中时,会自动加载其依赖项。

使用默认加载上下文时,加载到其他上下文中的依赖项将不可用,并且不能将位于探测路径外部位置的程序集加载到默认加载上下文中。

加载位置上下文

当使用 Assembly.LoadFrom 方法加载程序集时,程序集会加载到加载位置上下文中。

如果程序集包含依赖,也会自动从加载位置上下文中加载依赖。另外,在加载位置上下文中加载的程序集,可以使用到默认加载上下文中的依赖;注意,反过来却不成立!

加载位置上下文的使用需要谨慎,因为它会产生一些可能让你感觉到意外的行为。以下意外的行为列表照抄自文档 Best Practices for Assembly Loading

  • 如果已加载一个具有相同标识的程序集,则即使指定了不同的路径,LoadFrom 仍返回已加载的程序集。
  • 如果用 LoadFrom 加载一个程序集,随后默认加载上下文中的一个程序集尝试按显示名称加载同一程序集,则加载尝试将失败。 对程序集进行反序列化时,可能发生这种情况。
  • 如果用 LoadFrom 加载一个程序集,并且探测路径包括一个具有相同标识但位置不同的程序集,则将发生 InvalidCastException、MissingMethodException 或其他意外行为。
  • LoadFrom 需要对指定路径的 FileIOPermissionAccess.Read 和 FileIOPermissionAccess.PathDiscovery 或 WebPermission。

无上下文

使用反射发出生成的瞬态程序集只能选择在没有下文的情况下进行加载。在没有上下文的情况下进行加载是将具有同一标识的多个程序集加载到一个应用程序域中的唯一方式。这将省去探测成本。

从字节数组加载的程序集都是在没有上下文的情况下加载的,除非程序集的标识(在应用策略后建立)与全局程序集缓存中的程序集标识匹配;在此情况下,将会从全局程序集缓存加载程序集。

在没有上下文的情况下加载程序集具有以下缺点,以下摘抄自 Best Practices for Assembly Loading

  • 无法将其他程序集绑定到在没有上下文的情况下加载的程序集,除非处理 AppDomain.AssemblyResolve 事件。
  • 依赖项无法自动加载。 可以在没有上下文的情况下预加载依赖项、将依赖项预加载到默认加载上下文中或通过处理 AppDomain.AssemblyResolve 事件来加载依赖项。
  • 在没有上下文的情况下加载具有同一标识的多个程序集会导致出现类型标识问题,这些问题与将具有同一标识的多个程序集加载到多个上下文中所导致的问题类似。 请参阅避免将一个程序集加载到多个上下文中。

带来的问题

.NET 加载程序集的这种机制可能让你的程序陷入一点点坑:你可以让你的程序加载任意路径下的一个程序集(dll/exe),并且可以执行其中的代码,但你不能依赖那些路径中程序集的特定类型或接口等。

具体一点,比如你定义了一个接口 IPlugin,任意路径中的程序集可以实现这个接口,你加载这个程序集之后也可以通过 IPlugin 接口调用到程序集中的方法,因为这个接口的定义所在的程序集依然在你的探测路径中,而不是在插件程序集中。位于任意路径下的插件程序集可以访问到位于探测路径中所有程序集的所有 API,但反过来探测路径下的程序集不能访问到其他目录下插件程序集的特定类型或接口等。但是,如果这个程序集中有一些特定的类型如 WalterlvPlugin,那么你将不能依赖于这个特定的类型。

我创建了一个控制台程序,用以说明这样的加载上下文机制将带来问题。相关代码可以在我的 GitHub 仓库中找到:

其中 Program.cs 文件如下:

using System;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Threading.Tasks; namespace Walterlv.Demo.AssemblyLoading
{
class Program
{
static async Task Main(string[] args)
{
await LoadDependencyAssembliesAsync();
await RunAsync();
Console.ReadLine();
} private static async Task RunAsync()
{
try
{
await ThrowAsync();
}
catch (Exception ex)
{
Console.WriteLine(ex.Demystify());
} async Task ThrowAsync() => throw new InvalidOperationException();
} private static async Task LoadDependencyAssembliesAsync()
{
var folder = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "Dependencies");
Assembly.LoadFile(Path.Combine(folder, "Ben.Demystifier.dll"));
Assembly.LoadFile(Path.Combine(folder, "System.Collections.Immutable.dll"));
Assembly.LoadFile(Path.Combine(folder, "System.Reflection.Metadata.dll"));
}
}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40

项目文件 csproj 文件如下:

<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net48</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Ben.Demystifier" Version="0.1.4" />
</ItemGroup>
<Target Name="_ProjectMoveDependencies" AfterTargets="AfterBuild">
<ItemGroup>
<_ProjectToMoveFile Include="$(OutputPath)Ben.Demystifier.dll" />
<_ProjectToMoveFile Include="$(OutputPath)System.Collections.Immutable.dll" />
<_ProjectToMoveFile Include="$(OutputPath)System.Reflection.Metadata.dll" />
</ItemGroup>
<Move SourceFiles="@(_ProjectToMoveFile)" DestinationFolder="$(OutputPath)Dependencies" />
</Target> </Project>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

在这个程序中,我们引用了一个 NuGet 包 Ben.Demystifier。这个包具体是什么其实并不重要,我只是希望引入一个依赖而已。但是,在项目文件 csproj 中,我写了一个 Target,将这些依赖全部都移动到了 Dependencies 文件夹中。这样,我们就可以获得这样目录结构的输出:

- Walterlv.exe
- Dependencies
- Ben.Demystifier.dll
- System.Collections.Immutable.dll
- System.Reflection.Metadata.dll
  • 1
  • 2
  • 3
  • 4
  • 5

如果我们不进行其他设置,那么直接运行程序的话,应该是找不到依赖然后崩溃的。但是现在我们有 LoadDependencyAssembliesAsync 方法,里面通过 Assembly.LoadFile 加载了这三个程序集。但时机运行时依然会崩溃:

明明已经加载了这三个程序集,为什么使用其内部的类型的时候还会抛出异常呢?明明在 Visual Studio 中检查已加载的模块可以发现这些模块都已经加载完毕,但依然无法使用到里面的类型呢?

本文将介绍原因和解决办法。

解决方法

实际上 .NET 推荐的唯一解决方法是创建新的应用程序域来解决非探测路径下 dll 的依赖问题,在创建新应用程序域的时候设置此应用程序域的探测路径。

但是,我们其实有其他的方法依然在原来的应用程序域中解决依赖问题。

使用被遗弃的 API(不推荐)

AppDomain 有一个已经被遗弃的 API AppendPrivatePath,可以将一个路径加入到探测路径列表中。这样,我们不需要考虑去任意路径加载程序集的问题了,因为我们可以将任意路径设置成探测路径。

// 注意,这是一个被遗弃的 API。
AppDomain.CurrentDomain.AppendPrivatePath(folder);
  • 1
  • 2

关于此 API 为什么会被遗弃,你可以阅读微软的官方博客:Why is AppDomain.AppendPrivatePath Obsolete? - .NET Blog。因为你随时可以指定应用程序的探测路径,所以它可能让你的程序以各种不确定的方式加载程序集,于是你的程序将变得很不稳定;可能完全崩溃到你无法预知的程度。

另外,.NET Core 中已经不能使用此 API 了,这非常好!

使用 ILRepack / ILMerge 合并依赖

前面我们说过,加载位置上下文中的程序集可以依赖默认加载上下文中的程序集,而反过来却不行。通常默认加载上下文中的程序集是我们的主程序程序集和附属程序集,而加载位置上下文中加载的程序是插件程序集。

如果插件程序集依赖了一些主程序没有的依赖,那么插件可以考虑将所有的依赖合并入插件单个程序集中,避免依赖其他程序集,导致不得不去非探测路径加载程序集。

关于使用 ILRepack 合并依赖的内容,可以阅读我的另一篇博客:

首先推荐使用 ILRepack 来进行合并,如果你愿意,也可以使用 ILMerge:


参考资料


我的博客会首发于 https://blog.walterlv.com/,而 CSDN 会从其中精选发布,但是一旦发布了就很少更新。

如果在博客看到有任何不懂的内容,欢迎交流。我搭建了 dotnet 职业技术学院 欢迎大家加入。

本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。欢迎转载、使用、重新发布,但务必保留文章署名吕毅(包含链接:https://walterlv.blog.csdn.net/),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。如有任何疑问,请与我联系

发布了382 篇原创文章 · 获赞 232 · 访问量 47万+

.NET 的程序集加载上下文的更多相关文章

  1. .NET Core 3.0 可回收程序集加载上下文

    一.前世今生 .NET诞生以来,程序集的动态加载和卸载都是一个Hack的技术,之前的NetFx都是使用AppDomain的方式去加载程序集,然而AppDomain并没有提供直接卸载一个程序集的API, ...

  2. CLR中的程序集加载

    CLR中的程序集加载 本次来讨论一下基于.net平台的CLR中的程序集加载的机制: [注:由于.net已经开源,可利用vs2015查看c#源码的具体实现] 在运行时,JIT编译器利用程序集的TypeR ...

  3. 【C#进阶系列】23 程序集加载和反射

    程序集加载 程序集加载,CLR使用System.Reflection.Assembly.Load静态方法,当然这个方法我们自己也可以显式调用. 还有一个Assembly.LoadFrom方法加载指定路 ...

  4. 重温CLR(十七)程序集加载和反射

    本章主要讨论在编译时对一个类型一无所知的情况下,如何在运行时发现类型的信息.创建类型的实例以及访问类型的成员.可利用本章讲述的内容创建动态可扩展应用程序. 反射使用的典型场景一般是由一家公司创建宿主应 ...

  5. clr via c# 程序集加载和反射(2)

    查看,clr via c# 程序集加载和反射(1) 8,发现类型的成员: 字段,构造器,方法,属性,事件,嵌套类型都可以作为类型成员.其包含在抽象类MemberInfo中,封装了所有类型都有的一组属性 ...

  6. clr via c# 程序集加载和反射集(一)

    1,程序集加载---弱的程序集可以加载强签名的程序集,但是不可相反.否则引用会报错!(但是,反射是没问题的) //获取当前类的Assembly Assembly.GetEntryAssembly() ...

  7. .net 程序集加载,版本不匹配的解决方法

    经常有些时候,A.dll引用的是Microsoft.EntityFrameworkCore.dll version=1.0.0.0 publicKeyToken="adb9793829dda ...

  8. 应用程序域 System.AppDomain,动态加载程序集

    一.概述 使用.NET建立的可执行程序 *.exe,并没有直接承载到进程当中,而是承载到应用程序域(AppDomain)当中.在一个进程中可以包含多个应用程序域,一个应用程序域可以装载一个可执行程序( ...

  9. 非常郁闷的 .NET中程序集的动态加载

    记载这篇文章的原因是我自己遇到了动态加载程序集的问题,而困扰了一天之久. 最终看到了这篇博客:http://www.cnblogs.com/brucebi/archive/2013/05/22/Ass ...

随机推荐

  1. python 虚拟环境指定python版本

    virtualenv --no-site-packages -p python3.7 testenv source testenv/bin/activate deactivate 参考:https:/ ...

  2. ES6 - 开篇

    一些关于es6简单的介绍与了解.初始认知有限,循序完善. ES6: 又叫ES2015,是2015年推出的JavaScript新版本. 相应的,后边推出的ES7.8.9.10等都依次是上一版本发出后一年 ...

  3. [Shell]多姿势反弹shell

    客户端监听本地: nc -nvlp 4444 从原生的 shell 环境切换到 linux 的交互式 bash 环境: python -c 'import pty; pty.spawn("/ ...

  4. <每日 1 OJ> -24. The Simple Problem

    题目描述 Solo上了大学,对数学很感兴趣,有一天他面对数分三,一个Sequence(数列)摆在了他面前,这可难住他了……序列如下:S(a,k,n)=a+(k+a)+(2k+a)+…+(nk+a),题 ...

  5. 学习HSDB

    HSDB则是在SA(Serviceability Agent)基础上包装起来的一个调试器,而SA是个非常便于探索HotSpot VM内部实现的API. Stack Memory窗口的内容有三栏: 左起 ...

  6. Mesa: GeoReplicated, Near RealTime, Scalable Data Warehousing

    Mesa的定义并没有反映出他的特点,因为分布式,副本,高可用,他都是依赖google的其他基础设施完成的 他最大的特点是,和传统数仓比,可以做到near real-time的返回聚合的查询结果 算入实 ...

  7. 【C++】C++ 左值、右值、右值引用详解

    左值.右值 在C++11中所有的值必属于左值.右值两者之一,右值又可以细分为纯右值.将亡值.在C++11中可以取地址的.有名字的就是左值,反之,不能取地址的.没有名字的就是右值(将亡值或纯右值).举个 ...

  8. 爬虫中Selenium和PhantomJS

    Selenium Selenium是一个Web的自动化测试工具,最初是为网站自动化测试而开发的,类型像我们玩游戏用的按键精灵,可以按指定的命令自动操作,不同是Selenium 可以直接运行在浏览器上, ...

  9. 登陆服务器提示“You need to run "nvm install N/A" to install it before using it.”

    一.登陆服务器提示“You need to run "nvm install N/A" to install it before using it.” 二.执行命令: nvm ls ...

  10. Android Studio运行Hello World程序

    老的神舟本本装上了深度LINUX了...应该是基于ubuntu的,安装软件用的apt-get而不是yum 想重装学下android原生开发,官网下载了android studio, 发现不用FQ也能下 ...