当前位置: 首页 > news >正文

.NET 的程序集加载上下文

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

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


本文内容

    • 程序集加载上下文
      • 默认加载上下文
      • 加载位置上下文
      • 无上下文
    • 带来的问题
    • 解决方法
      • 使用被遗弃的 API(不推荐)
      • 使用 ILRepack / ILMerge 合并依赖

程序集加载上下文

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

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

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

默认加载上下文

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

ApplicationBase 和 PrivateBinPath 这两个属性虽然允许被设置,但它们只对新生成的 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 仓库中找到:

  • walterlv.demo/Walterlv.Demo.AssemblyLoading

其中 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"));
        }
    }
}

项目文件 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>

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

- Walterlv.exe
- Dependencies
    - Ben.Demystifier.dll
    - System.Collections.Immutable.dll
    - System.Reflection.Metadata.dll

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

抛出异常

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

已加载模块

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

解决方法

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

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

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

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

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

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

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

使用 ILRepack / ILMerge 合并依赖

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

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

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

  • .NET 使用 ILRepack 合并多个程序集(替代 ILMerge),避免引入额外的依赖 - walterlv

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

  • .NET 使用 ILMerge 合并多个程序集,避免引入额外的依赖

使用 ILMerge 合并依赖


参考资料

  • Loading .NET Assemblies out of Seperate Folders - Rick Strahl’s Web Log
  • Best Practices for Assembly Loading - Microsoft Docs
  • Why is AppDomain.AppendPrivatePath Obsolete? - .NET Blog

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

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

知识共享许可协议

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

相关文章:

  • .NET 使用 ILMerge 合并多个程序集,避免引入额外的依赖
  • .NET 使用 ILRepack 合并多个程序集(替代 ILMerge),避免引入额外的依赖
  • Windows 10 解决无法完整下载安装语言包(日语输入法无法下载使用)
  • 从零开始制作 NuGet 源代码包(全面支持 .NET Core / .NET Framework / WPF 项目)
  • 如何快速创建 Visual Studio 代码片段?
  • 从 git 的历史记录中彻底删除文件或文件夹
  • 如果不用 ReSharper,那么 Visual Studio 2019 能还原 ReSharper 多少功能呢?
  • .NET/C# 如何获取当前进程的 CPU 和内存占用?如何获取全局 CPU 和内存占用?
  • 如何监视 WPF 中的所有窗口,在所有窗口中订阅事件或者附加 UI
  • 如何追踪 WPF 程序中当前获得键盘焦点的元素并显示出来
  • 使用 Visual Studio 编译时,让错误一开始发生时就停止编译(以便及早排查编译错误节省时间)
  • .NET / MSBuild 扩展编译时什么时候用 BeforeTargets / AfterTargets 什么时候用 DependsOnTargets?
  • 在项目文件 / MSBuild / NuGet 包中编写扩展编译的时候,正确使用 props 文件和 targets 文件
  • .NET Framework 的 bug?try-catch-when 中如果 when 语句抛出异常,程序将彻底崩溃
  • .NET/MSBuild 中的发布路径在哪里呢?如何在扩展编译的时候修改发布路径中的文件呢?
  • [Vue CLI 3] 配置解析之 css.extract
  • [译]Python中的类属性与实例属性的区别
  • “大数据应用场景”之隔壁老王(连载四)
  • 【EOS】Cleos基础
  • Android组件 - 收藏集 - 掘金
  • create-react-app做的留言板
  • HomeBrew常规使用教程
  • JavaScript工作原理(五):深入了解WebSockets,HTTP/2和SSE,以及如何选择
  • JavaWeb(学习笔记二)
  • Java基本数据类型之Number
  • js ES6 求数组的交集,并集,还有差集
  • SAP云平台运行环境Cloud Foundry和Neo的区别
  • Sublime text 3 3103 注册码
  • 初识MongoDB分片
  • 从零搭建Koa2 Server
  • 模仿 Go Sort 排序接口实现的自定义排序
  • 前端工程化(Gulp、Webpack)-webpack
  • 如何用Ubuntu和Xen来设置Kubernetes?
  • 世界编程语言排行榜2008年06月(ActionScript 挺进20强)
  • 智能合约Solidity教程-事件和日志(一)
  • 转载:[译] 内容加速黑科技趣谈
  • Unity3D - 异步加载游戏场景与异步加载游戏资源进度条 ...
  • 好程序员web前端教程分享CSS不同元素margin的计算 ...
  • ###C语言程序设计-----C语言学习(3)#
  • %3cli%3e连接html页面,html+canvas实现屏幕截取
  • (12)Hive调优——count distinct去重优化
  • (function(){})()的分步解析
  • (Redis使用系列) SpringBoot中Redis的RedisConfig 二
  • (独孤九剑)--文件系统
  • (附源码)ssm失物招领系统 毕业设计 182317
  • (附源码)计算机毕业设计ssm高校《大学语文》课程作业在线管理系统
  • (附源码)计算机毕业设计SSM智慧停车系统
  • (力扣记录)235. 二叉搜索树的最近公共祖先
  • (三)Pytorch快速搭建卷积神经网络模型实现手写数字识别(代码+详细注解)
  • (十六)Flask之蓝图
  • (十五)Flask覆写wsgi_app函数实现自定义中间件
  • ./和../以及/和~之间的区别
  • .MyFile@waifu.club.wis.mkp勒索病毒数据怎么处理|数据解密恢复
  • .NET MVC之AOP
  • .net MVC中使用angularJs刷新页面数据列表