.Net Framework中的AppDomain.AssemblyResolve事件的常見用法、問題,以及解決辦法
- 2022 年 10 月 9 日
- 筆記
- .NET Framework
一、簡述
本文簡要的介紹.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 //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 }
幾點解釋:
問:常式中的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 }
如果哪天微軟對上述程式碼稍加修改,本關小黑屋方法就可以退休了。
(全文完,本文最早由yangzhj發表於部落格園,轉載需註明出處)
