.Net Framework中的AppDomain.AssemblyResolve事件的常见用法、问题,以及解决办法

时间:2022-10-09 21:08:40

一、简述

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污染问题

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发表于博客园,转载需注明出处)