原文鏈接:傳送門。
某一天我正在寫一些反射代碼,目的是遍歷所有的程序集來查找一個特定的接口,然后在Startup中調用其上的一個方法。看起來這個功能似乎很簡單,但是在現實中,卻沒有一個清晰的,簡單的,適合各種情形的方式來獲取一個程序集。這篇文章獲取對某些人來說非常的枯燥,但是如果我能夠幫助哪怕一個人來解決此類問題,那么這篇文章也是值得的。
說真的,由於有多種獲取程序集的方法,我將不會說”使用這個方法”。很有可能,對於你的特定的工程來說,也許只有一種方式可以工作,所以依賴其他的方式是毫無意義的。讓我們簡單的對所有的方式做個測試,然后看看哪一個方法是最合理的。
使用AppDomain.GetAssemblies
你可能會遇到的第一個選項是AppDomain.GetAssemblies。它(看似)加載了在AppDomain中的所有程序集,基本上可以說加載了你項目使用到的每一個程序集。但是卻存在着大量的警告。在.NET中程序集是延遲加載到AppDomain中的,它不可能一次性加載所有的程序集,而是等你調用了一個程序集中的方法/類的時候,它才會將它加載進來--也就是即時加載。這是合理的,因為如果你從不使用一個程序集的話,是沒有理由加載它的。
但問題是在你調用AppDomain.GetAssemblies()的那個時間點上,如果你並沒有調用某個特定的程序集的方法,它便不會被加載。現在如果你要為了Startup方法得到所有的程序集,很有可能你還沒有調用到那個程序集,這就意味着它還沒有加載到AppDomain,所以便獲取不到這個接口方法。
用代碼來說:
AppDomain.CurrentDomain.GetAssemblies(); // Does not return SomeAssembly as it hasn't been called yet. SomeAssembly.SomeClass.SomeMethod(); AppDomain.CurrentDomain.GetAssemblies(); // Will now return SomeAssembly.
雖然這看起來是一個很有吸引力的選項,但是要知道,對於這個方法來說,時機是一切。
使用AssemblyLoad事件
因為你不能確保當你調用CurrentDomain.GetAssemblies()時所有程序集都被加載了,而實際上當AppDomain加載另一個程序集的時候有一個事件會運行。基本上說,當一個程序集被延遲加載的時候,你可以被通知到。它看起來像是這樣:
AppDomain.CurrentDomain.AssemblyLoad += (sender, args) => { var assembly = args.LoadedAssembly; };
如果你只是想當程序集加載的時候檢查下一些東西,那這或許是一個不錯的解決方案,但是這個過程在某個特定的點並不是必然會發生(在你的.NET Core app的Startup.cs類中並不會發生)。
這個方法的另一個問題是到你添加你的事件處理器的那個時刻,並不能保證程序集還沒有被加載(事實上它們很可能已經加載過了)。所以呢?你需要付出雙份的努力,首先添加你的事件處理器,之后迅速的檢查AppDomain.CurrentDomain.GetAssemblies,找到已經被加載了的東西。
這是一個完美的解決方案,但是如果你習慣於使用延遲加載的程序集來做事的話,這就不能正常工作了。
使用 GetReferencedAssemblies()
排名中的下一個是GetReferencedAssemblies()。本質上你可以通過一個程序集,比如你的入口點程序集,一般來說便是你的web項目,來得到所有引用的程序集。
其代碼本身看起來像是這樣:
Assembly.GetEntryAssembly().GetReferencedAssemblies();
再一次,看起來像是在玩把戲,但是這個方法有另一個很大的問題。在許多項目中會有一個“模式分離”的概念,比如 Web Project>>Service Project>>Data Project。Web Project本身並不直接引用Data Project。而當你調用“GetReferencedAssemblies”時其意味着直接引用。因此如果你期望在程序集列表中得到Data Project,你將會失望不已。
所以,再一次的,在一些情況下可以正常工作,但並不是一個普遍的解決方案。
循環GetReferencedAssemblies()
使用GetReferencedAssemblies()的另一個選擇是創建一個方法來遍歷所有的程序集。類似於這樣:
public static List GetAssemblies() { var returnAssemblies = new List(); var loadedAssemblies = new HashSet(); var assembliesToCheck = new Queue(); assembliesToCheck.Enqueue(Assembly.GetEntryAssembly()); while(assembliesToCheck.Any()) { var assemblyToCheck = assembliesToCheck.Dequeue(); foreach(var reference in assemblyToCheck.GetReferencedAssemblies()) { if(!loadedAssemblies.Contains(reference.FullName)) { var assembly = Assembly.Load(reference); assembliesToCheck.Enqueue(assembly); loadedAssemblies.Add(reference.FullName); returnAssemblies.Add(assembly); } } } return returnAssemblies; }
這個方法的邊界處理得有點粗糙,但是它的確可以工作並且意味着在Startup方法中,你可以立即看到所有的程序集。
關於這個方法你可能會被卡住的一次是如果你正在動態加載程序集,並且它們實際上並不會被任何項目所引用。對於這種情況,你需要下一個方法。
目錄DLL加載
一個很粗糙的獲取所有解決方案dll的方式是將它們加載出你的bin文件夾。看起來像是這樣:
public static Assembly[] GetSolutionAssemblies() { var assemblies = Directory.GetFiles(AppDomain.CurrentDomain.BaseDirectory, "*.dll") .Select(x => Assembly.Load(AssemblyName.GetAssemblyName(x))); return assemblies.ToArray(); }
它可以正常工作但的確是一個粗糙的解決方案。但是使用這個方式的一個最大的好處是一個dll只需要簡單的放置在需要加載的目錄中就可以。因此如果你出於任何原因動態的加載DLLs,對於你來說,這很可能是唯一的方法(除過在AppDomain中監聽AssemblyLoad)。
這是做這件事情的看起來像惡作劇的方式之一。但是很可能你已經被阻擋到角落之中而這正是解決問題的唯一方式。
僅僅得到“我的”程序集
使用這些方法中的任何一種,您會很快發現你正在將Nuget下的每個程序集加載到你的項目中,包括Nuget包、.NET核心庫甚至運行時特定的dll。在.NET世界中,程序集就是程序集。沒有“是的,但這是我的程序集”並且它們很特別的概念。
過濾的唯一方法就是檢查名字。您可以將其作為白名單來執行,因此如果解決方案中的所有項目都以“MySolution”開頭。因而你可以像這樣來進行過濾:
Assembly.GetEntryAssembly().GetReferencedAssemblies().Where(x => x.Name.StartsWith("MySolution."))
或者你可以選擇一個黑名單選項,這個選項並不真正限制你的程序集,但至少可以減少你正在加載/檢查/處理的程序集的數量。就像這樣:
Assembly.GetEntryAssembly().GetReferencedAssemblies() .Where(x => !x.Name.StartsWith("Microsoft.") && !x.Name.StartsWith("System."))
黑名單可能看起來很蠢,但在某些情況下,如果您正在構建一個實際上不知道最終解決方案名稱的庫,那么這是減少您試圖加載的內容的唯一方法。