原文:.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. 灵活的MyBatis

    一.前言 将数据存储到数据库是开发中很重要的一环.曾经有程序员说自己做过最牛逼的事情就是增删改查.确实我们做了很多页面,后太代码写了很多,可是最终都离不开数据库的增删改查.Java有一套自己的JPA标 ...

  2. [技术博客]React Native——HTML页面代码高亮&数学公式解析

    问题起源 原有博文显示时代码无法高亮,白底黑字的视觉效果不好. 原有博文中无法解析数学公式,导致页面会直接显示数学公式源码. 为了解决这两个问题,尝试了一些方法,最终利用开源类库实现了页面美化. (失 ...

  3. tornado多进程模式不同进程写不同日志

    #coding: utf- ''' Author: Time: Target: ''' import logging import logging.handlers import os import ...

  4. 如何单独编译Linux内核源码中的驱动为可加载模块?

    答: 分为两步: 1. 配置某个驱动为模块(如: CONFIG_RTC_XXX=m) 2. 指定路径并编译, 如编译drivers/rtc中的驱动 make SUBDIRS=drivers/rtc m ...

  5. python对不同类型文件(doc,txt,pdf)的字符查找

    python对不同类型文件的字符查找 TXT文件: def txt_handler(self, f_name, find_str): """ 处理txt文件 :param ...

  6. [Golang] Gin框架学习笔记

    0x0 Gin简介 1.Gin 是什么? Gin 是一个用 Go (Golang) 编写的 HTTP web 框架. 它是一个类似于 martini 但拥有更好性能的 API 框架, 由于 httpr ...

  7. linux python 安装 pymssql

    其实也不是很完整的. 我主要在dockers中的alpine linux 下进行开发. 这里主要说的就是如何在alpine下安装pymssql 多级依赖 pymssq 依赖 Cython , Cyth ...

  8. Centos7无法播放mp4视频(待验证)

    新安装Centos7后,发现无法正常播放本地mp4视频 可以尝试安装 yum -y install ffmpeg 安装之后,需要重启电脑才能生效 浏览器安装年flash,只能播放部分视频,也有可能是s ...

  9. UE4 常用数学

    转自:https://dawnarc.com/2016/07/mathlinear-algebra%E5%90%91%E9%87%8F%E7%A7%AF%E5%A4%96%E7%A7%AF%E5%8F ...

  10. Java调用动态链接库so文件(传参以及处理返回值问题)

    刚来到公司,屁股还没坐稳,老板把我叫到办公室,就让我做一个小程序.我瞬间懵逼了.对小程序一窍不通,还好通过学习小程序视频,两天的时间就做了一个云开发的小程序,但是领导不想核心的代码被别人看到,给了我一 ...