一、简述

本文简要的介绍.NET Framework中System.AppDomain.AssemblyResolve事件的用法、使用注意事项,以及复杂场景下AssemblyResolve事件的污染问题和解决办法。
System.AppDomain.AssemblyResolve事件可以理解为“找程序集事件“,假设程序集A依赖了程序集B、C,而B、C不在与A相同的目录下也不在常规的常规探测路径之内,此时相应AppDomain(一般指AppDomain.Current)的AssemblyResolve事件会被触发,程序集A中可尽早响应此事件,实现加载程序集B和C到对应的AppDomain。可参考微软官方文档:Resolve assembly loads https://learn.microsoft.com/en-us/dotnet/standard/assembly/resolve-loads#how-the-assemblyresolve-event-works,该文比较详细的描述了AssemblyResolve的原理、用法和注意事项。不过该文中虽多次提及注意事项,但给出的例程中并没有很好的体现注意事项,这是官方给马虎的小伙子埋下的坑之一。

二、典型用法
场景1:让.NET软件的安装目录更整洁
将所有程序集都放到与主程序集(指.exe程序集也可能是一个.dll)相同的目录下,在软件规模稍大时显得杂乱无章。即使认定用户不会查看软件的安装目录,对于爱干净的开发者来说也不能忍。于是按功能模块建子文件夹,将相应的dll放入子文件夹内,通过在主程序集入口处立即响应AssemblyResolve事件已实现加载子文件夹中的程序集。PS:主程序集是自己的exe文件时也可通过应用程序配置解决,如有兴趣可百度关键字”assemblyBinding“。

场景2:有序的组织软件内共享程序集
有的时候我们编写的软件是大型软件的插件,不巧的是,该大型软件每年升级版本,插件适配该宿主的多个版本。插件软件有较多的程序集,其中少数依赖于宿主的API,多数与宿主无关能不妨称为软件内共享程序集,此时可将软件内共享程序集放安装目录,与宿主版本相关的程序集则放到安装目录下的子目录。让宿主启动后加载对应子目录下的"主程序集",该程序集中尽快注册AssemblyResolve事件以加载软件内共享程序集。
值得咱们这样追随的宿主平台,通常也有别的追求者,大家都用AssemblyResolve事件,林子大了,就可能出现本文主描述的AssemblyResolve污染问题。

场景3:从字节数组加载程序集
从字节数组加载程序集…貌似可以做程序集加密!?不过不用期待太多特别是看完本文之后。此处用一段摘自官方文档中的文字来描述:如果处理程序有权访问以字节数组形式存储的程序集的数据库,则它可以通过使用可采用字节数组的一种 Assembly.Load 方法重载来加载字节数组。

更多用法,欢迎评论讨论。

三、AssemblyResolve污染问题

这里解释何为AssemblyResolve污染,以及问题起因。
AssemblyResolve污染指AppDomain.AssemblyResolve事件的一个或多个响应函数没有按该事件的响应规范正确处理,影响所有依赖于该事件的功能模块运行稳定性。通常问题现象是,运行到某个具体功能时提示找不到*.Resources.dll,或*.XmlSerializers .dll,或提未能从程序集xxx中加载类型yyy。

咱们仅讨论无意中未按AssemblyResolve响应规范正确处理的情况。此时的不规范响应通常是:遇到不能处理的程序集时应简单的返回null,但自.NET Framework 4.0开始,事件参数ResolveEventArgs增加了RequestingAssembly属性,该属性刚好是一个程序集且名字看着挺像是”正要找的程序集“,于是有些开发者在遇到不能处理的程序集时返回ResolveEventArgs.RequestingAssembly。一旦返回了一个不为null的程序集对象,AssemblyResolve事件的其它响应函数变不会再被调用(本文在”探讨“一节中证实这个情况),从而引起本节描述的AssemblyResolve污染问题。

问题1:提未能从程序集xxx中加载类型yyy
如果有问题的响应函数先于咱们的程序响应了AppDomain.AssemblyResolve事件,则一直轮不到咱们程序集中的AppDomain.AssemblyResolve事件,于是出现这个问题。

问题2:提示找不到*.Resources.dll,或*.XmlSerializers .dll
这类卫星程序集或附属程序集到上下文的加载,从.NET Framework 4.0开始也会触发事件,要求统一简单的返回null。摘一段官网说明:
!Important
Beginning with the .NET Framework 4, the AssemblyResolve event is raised for satellite assemblies. This change affects an event handler that was written for an earlier version of the .NET Framework, if the handler tries to resolve all assembly load requests. Event handlers that ignore assemblies they do not recognize are not affected by this change: They return null, and normal fallback mechanisms are followed.

四、解决办法
一种解决办法是喊话对应的软件开发商让修改。不过也许对应的开发商已经跑路,或的确做了修改,但用户侧仍然用了未修改前的旧版本,这种方式是不可靠的。

问题解决思路:
通过反射拿到承载AppDomain.AssemblyResolve事件的Delegate,逐一检查Delegate中各ResolveEventHandler是否正常,不正常者关小黑屋后改造后再置入AppDomain.AssemblyResolve事件的Delegate。

附例程:

 1         class AssemblyResolveHook
2 {
3 ResolveEventHandler _handler;
4 const string c_no_this_assembly = "NoThisAssembly, Version=1.0.0.0, Culture=zh-CN, PublicKeyToken=null";
5
6 AssemblyResolveHook(ResolveEventHandler handler)
7 {
8 _handler= handler;
9 }
10
11 Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args)
12 {
13 if (_handler== null) {
14 return null;
15 }
16
17 // 这里是“小黑屋”改造过程
18 // 此处是对症下药的较温和的无害的处理方式
19 var asm = args.RequestingAssembly;
20 var asm2 = _handler(sender, args);
21 if (asm2 != null && asm != null && asm.FullName == asm2.FullName) {
22 asm2 = null;
23 }
24
25 return asm2;
26 }
27
28 /// <summary>
29 /// 使调用本方法之前的所有CurrentDomain_AssemblyResolve事件响应无害化
30 /// </summary>
31 public static void Handsup()
32 {
33 try {
34 var domain = AppDomain.CurrentDomain;
35 var far = domain.GetFieldValue("_AssemblyResolve") as ResolveEventHandler;
36 if (far != null) {
37 var invocationList = far.GetInvocationList();
38 var num = invocationList.Length;
39 for (int i = 0; i < num; i++) {
40 var handler = (ResolveEventHandler)invocationList[i];
41
42 // 测试一下这个handler有没有问题?
43 // 方法是给一个不可能找到的程序集名称,看是否返回了程序集,如果是,给关小黑屋
44 var asm = handler(domain, new ResolveEventArgs(c_no_this_assembly, System.Reflection.Assembly.GetExecutingAssembly()));
45 if (asm != null) {
46 handler = new ResolveEventHandler(new AssemblyResolveHook(handler).CurrentDomain_AssemblyResolve);
47 }
48
49 far = i == 0 ? handler : (ResolveEventHandler)Delegate.Combine(far, handler);
50 }
51
52 domain.SetFieldValue("_AssemblyResolve", far);
53 }
54 }
55 catch (System.Exception ex) {
56 System.Diagnostics.Debug.WriteLine(ex.ToString());
57 }
58 }
59 }

AssemblyResolveHook

几点解释:
问:例程中的GetFieldValue/SetFieldValue,没有这样的方法?
答:这是扩展方法,能看到这里的你,也不会在乎反射的这几行代码怎么写

问:AssemblyResolveHook.Handsup 调用之后,新添加的 AssemblyResolve 事件响应有问题怎么办?
答:一般AssemblyResolve事件会在第一时间响应,故可延迟调用AssemblyResolveHook.Handsup

问:怎么知道字段名是 _AssemblyResolve?
答:反正用VS2022社区版,光标放代码的AppDomain上按F12,就能看到答案。其它版本VS应该也行

问:被关小黑屋了,如果对方要注销 AssemblyResolve 事件的响应咋办?
答:太多问题了…

五、探讨

本问题提及AssemblyResolve事件的某响应函数一旦返回了非null程序集,就不会再调用后续的响应函数,可以光标放代码的AppDomain上按F12,找到对应的 .NET Framwork 代码证实。
如下:

 1 [SecurityCritical]
2 private RuntimeAssembly OnAssemblyResolveEvent(RuntimeAssembly assembly, string assemblyFullName)
3 {
4 ResolveEventHandler assemblyResolve = _AssemblyResolve;
5 if (assemblyResolve == null) {
6 return null;
7 }
8
9 Delegate[] invocationList = assemblyResolve.GetInvocationList();
10 int num = invocationList.Length;
11 for (int i = 0; i < num; i++) {
12 Assembly asm = ((ResolveEventHandler)invocationList[i])(this, new ResolveEventArgs(assemblyFullName, assembly));
13 RuntimeAssembly runtimeAssembly = GetRuntimeAssembly(asm);
14 if (runtimeAssembly != null) {
15 return runtimeAssembly;
16 }
17 }
18
19 return null;
20 }

OnAssemblyResolveEvent

如果哪天微软对上述代码稍加修改,本关小黑屋方法就可以退休了。

(全文完,本文最早由yangzhj发表于博客园,转载需注明出处)

.Net Framework中的AppDomain.AssemblyResolve事件的常见用法、问题,以及解决办法的更多相关文章

  1. bootstrap table 生成的表格里动态添加HTML元素按钮,JS中添加点击事件,点击没反应---解决办法

    bootstraptable中onExpandRow属性---js  方法添加的 html代码,然后给这代码里面的 元素 添加 事件,却获取不该元素.(称之为未来元素),由于是未来的 所以现在没有这个 ...

  2. HTML中的select下拉框内容显示不全的解决办法

    HTML中的select下拉框内容显示不全的解决办法 今天,我遇到这样一个问题:查询栏中的下拉框中的内容过长,导致部分被覆盖了. 查询了一些资料,有的说用函数控制,有的说用事件控制,有的看不懂,有的实 ...

  3. 【转】Android中引入第三方Jar包的方法(java.lang.NoClassDefFoundError解决办法)

    原文网址:http://www.blogjava.net/anchor110/articles/355699.html 1.在工程下新建lib文件夹,将需要的第三方包拷贝进来.2.将引用的第三方包,添 ...

  4. Windows Server 2008 R2中IIS7.5配置完网站权限不足问题的解决办法:

    Windows Server 2008 R2中IIS7.5配置完网站权限不足问题的解决办法:常见问题:HTTP 错误 500.0 - Internal Server Error无法显示页面,因为发生内 ...

  5. 【转】Android Fragment中使用SurfaceView切换时闪一下黑屏的解决办法

    重构了下之前自己的一个新闻客户端,全部使用了Fragment来进行页面切换,只有一个入口Activity作为程序的启动Activity,其中有一个界面需要调用摄像头识别二维码, 于是就会用到Surfa ...

  6. idea中Entity实体中报错:cannot resolve column/table/...解决办法。

    idea中Entity实体中报错:cannot resolve column/table/...解决办法. 若idea中Entity实体中报错: cannot resolve column.... c ...

  7. .Net“/”应用程序中的服务器错误 超过了最大请求长度 错误解决办法

    错误如下: 错误提示: 说明: 执行当前 Web 请求期间,出现未处理的异常.请检查堆栈跟踪信息,以了解有关该错误以及代码中导致错误的出处的详细信息. 异常详细信息: System.Web.HttpE ...

  8. jQuery事件委托之Safari失效的解决办法--摘抄

    什么是事件委托 事件委托是Jquery中一种事件绑定的方式,不同于常见的事件绑定方式将事件绑定在目标元素上,而是将事件绑定在父级元素上通过事件冒泡来执行绑定函数. //常见的事件绑定(Jquery) ...

  9. VC6.0 MFC中WebBrowser控件禁止新窗口弹出的解决办法

    http://blog.csdn.net/gnorth/article/details/7258293 分类: WebBrowser MFC 禁止新窗口2012-02-14 15:25 1787人阅读 ...

随机推荐

  1. linux 安装redis及问题收集

    contos 7 下安装redis教程可参照https://www.cnblogs.com/hxun/p/11075755.html值得注意的是在第6步方法一(所以建议使用方法二),如果直接使用xft ...

  2. 远见而明察近观若明火|Centos7.6环境基于Prometheus和Grafana结合钉钉机器人打造全时监控(预警)Docker容器服务系统

    原文转载自「刘悦的技术博客」https://v3u.cn/a_id_181 我们知道,奉行长期主义的网络公司,势必应在软件开发流程管理体系上具备规范意识,即代码提交有CR(CodeReview),功能 ...

  3. 来看看这位年轻的 eBay 小伙是如何成为 Committer

    介绍一下我自己 目前就职于eBay中国,专注于微服务中间件,分布式架构等领域,同时也是狂热的开源爱好者. 如何成为一个commiter 过去几个月,我一直持续在为 Apache DolphinSche ...

  4. Excelize 2.3.1 发布,Go 语言 Excel 文档基础库,支持加密表格文档

    Excelize 是 Go 语言编写的用于操作 Office Excel 文档基础库,基于 ECMA-376,ISO/IEC 29500 国际标准.可以使用它来读取.写入由 Microsoft Exc ...

  5. Java对象已死吗 深入理解Java虚拟机笔记

    1.引用计数器法 给每个对象设置一个计数器,每当有一个引用就给计数器的值+1,引用时小时就减一,当计数器值为0是就可以回收掉了. 主流虚拟机都没有使用这种算法,循环依赖问题 2.可达性分析: 思路是通 ...

  6. Python小游戏——外星人入侵(保姆级教程)第一章 01创建Pygame窗口 02创建设置类Setting()

    系列文章目录 第一章:武装飞船 01:创建Pygame窗口以及响应用户输入 02:创建设置类Setting() 一.前期准备 1.语言版本 Python3.9.0 2.编译器 Pycharm2022 ...

  7. UVA1306 The K-League(最大流)

    题面 有 n n n 支队伍进行比赛,每支队伍需要打的比赛数目相同. 每场比赛恰好一支队伍胜,另一支败. 给出每支队伍目前胜的场数 w i w_i wi​ 和败的场数(没用),以及每两个队伍还剩下的比 ...

  8. [NOI2021] 密码箱 (平衡树,连分数,Stern-Brocot 树,矩阵)

    题面 记忆犹新 题解 f f f 函数值给得非常明显,一看就给人一种熟悉感--这不是连分数吗? 众所周知,连分数有个递推公式,即 p i = a i p i − 1 + p i − 2 q i = a ...

  9. Taurus.MVC 微服务框架 入门开发教程:项目部署:6、微服务应用程序Docker部署实现多开。

    系列目录: 本系列分为项目集成.项目部署.架构演进三个方向,后续会根据情况调整文章目录. 开源地址:https://github.com/cyq1162/Taurus.MVC 本系列第一篇:Tauru ...

  10. Spring5中JdbcTemplate

    JdbcTemplate是什么 JdbcTemplate 类提供了很多便利的方法解决诸如把数据库数据转变成基本数据类型或对象,执行写好的或可调用的数据库操作语句,提供自定义的数据错误处理. 在spri ...