瞭解程序集如何在C#.NET中加載
咱們一直在處理庫和NuGet軟件包。不論是好是壞,高級.NET開發人員都須要瞭解.NET運行時如何加載程序集。git
這些庫依賴於其餘流行的庫,而且有不少共享的依賴項。有了足夠大的依賴關係網絡,您最終將陷入衝突或困境。處理此類問題的最佳方法是瞭解該機制在內部的工做方式。github
在本文中,您將看到.NET進程如何以及什麼時候加載引用的程序集。web
您將瞭解加載了哪一個庫版本,當有多個可用版本時會發生什麼,以及爲何有時因爲版本衝突而出現問題。算法
您將看到如何調試這些類型的問題,查看程序集綁定日誌(融合日誌)以及一些解決衝突的方法。c#
程序集,模塊和引用
讓咱們從圍繞.NET流程的一些基本術語開始。緩存
一個裝配在.NET是一個DLL或EXE文件。Visual Studio解決方案中的每一個項目都被編譯爲一個程序集。安全
每一個程序集能夠包含多個模塊,可是實際上,咱們幾乎老是在一個程序集中有一個模塊,該模塊的名稱與該程序集相同。網絡
在Visual Studio中啓動進程或單擊F5時,將執行啓動項目程序集。除了.NET Framework或.NET Core程序集以外,它將是第一個加載的程序集。app
以後,該過程將根據須要在運行時加載其餘程序集。僅當須要調用該程序集的方法或使用該程序集的類型時,它纔會延遲加載程序集。dom
這裏是爲一個簡單的「 Hello World」 .NET Framework項目加載的模塊(出於咱們全部的意圖和目的,模塊和程序集都是相同的)。MyStartup.dll是此處的啓動項目:
當您從另外一個項目引用一個項目時,在構建時,被引用項目的DLL或EXE被複制到啓動項目的Bin文件夾中。
一般是Bin \ Debug或Bin \ Release。在運行時,當您第一次使用引用的項目中的類型時,CLR在應用程序目錄中查找具備與指望的名稱和版本相同的DLL文件。而後將程序集加載到流程中。這也稱爲綁定到裝配件。
這是一個例子:
假設咱們有一個名爲MyStartup的簡單控制檯應用程序,它引用了另外一個名爲Lib1的項目。MyStartup使用Lib1程序集中的某些類。
在MyStartup中:
class Program { static void Main(string[] args) { int a = int.Parse(Console.ReadLine()); int b = int.Parse(Console.ReadLine()); Console.WriteLine("A + B = " + Add(a, b)); } private static int Add(int a, int b) { var calculator = new Lib1.Calculator(); return calculator.Sum(a, b); } }
在Lib1中:
public class Calculator { public int Sum(int a, int b) { return a + b; } }
輸入Main
方法時,還沒有加載Lib1程序集。可是,在輸入Add
方法時,CLR嘗試解析Calculator
類型,找出它在引用的程序集Lib1中,而後嘗試加載該程序集。
.NET中的程序集綁定
當CLR須要加載程序集時,邏輯實際上比在Bin文件夾中查找要複雜一些。這是執行的實際邏輯(有關詳細說明,請參見Microsoft文檔[1]):
1.根據配置文件(app.config或web.config)肯定須要加載的程序集的版本。該配置文件的名稱爲(在生成以後) [executable name].exe.config
或web.config
。綁定重定向在這裏發揮了做用(稍後會詳細介紹)。2.查看程序集是否已加載。若是加載了其餘版本,則將拋出FileLoadException,除非它是一個能夠同時加載多個版本的強命名程序集。3.若是它是強名稱程序集,請檢查全局程序集緩存[2](GAC)。GAC是機器上共享多個應用程序部件的地方。若是須要的話,程序集會緩存。它只能存儲強命名程序集。它能夠存儲同一程序集的不一樣版本。您可使用gacutil.exe[3]本身將其安裝到GAC 。4.若是它是一個強名稱的程序集,而且配置文件包含<codeBase>
節點,那麼它將檢查那裏的程序集位置。若是該<codeBase>
節點存在而且找不到程序集,FileNotFoundException
則將引起a。5.根據啓發式算法檢查程序集DLL或EXE。此過程稱爲「探測」。算法以下:1.檢查文件夾[application base] / [assembly name].dll
。應用程序庫是應用程序可執行文件所在的位置。一般,您的Bin \ Debug或Bin \ Release文件夾。2.檢查一下 [application base] / [assembly name] / [assembly name].dll
3.若是爲引用的程序集指定了區域性信息,則僅檢查如下目錄: [application base] / [culture] / [assembly name].dll
[application base] / [culture] / [assembly name] / [assembly name].dll
4.若是該<probing>
節點存在於配置文件中,則它將在該privatePath
節點的屬性指定的文件夾中查找程序集。
他們爲何要使全部事情變得如此困難,對嗎?
實際上,這種邏輯很是有助於咱們發展,而不會使事情變得困難。它的存在是爲了實現一些重要目標:
•爲了確保您引用的是特定的程序集和版本,則將加載該確切版本。不然,將引起異常。並且,若是您知道本身在作什麼,則能夠在配置文件中指定覆蓋規則(綁定重定向)。•爲了靈活地在您要加載的程序集中進行。例如,若是要根據不一樣的區域性(語言)加載不一樣的程序集,則能夠輕鬆地作到這一點。或者,若是您要根據客戶配置加載不一樣的程序集,那也能夠。•爲了安全起見,咱們使用了全稱的程序集。他們確保您不能「僞造」程序集。例如,若是某個進程但願加載Lib1 v4.5,那麼您將沒法加載具備相同名稱和版本的惡意軟件程序集。加載時會引起異常。這就是爲何在計算機上全部進程都共享的GAC只接受強名稱程序集的緣由。
在大多數應用程序中,您無需記住程序集加載和探測的複雜邏輯。您無需瞭解或考慮GAC,全名程序集或操做配置文件。
您幾乎根本不須要考慮庫的版本,由於可能的衝突經過稱爲「綁定重定向」的機制自動解決了。
綁定重定向
若是有一件事對於瞭解這筆交易很是重要,那就是綁定重定向。可以告訴運行時它將實際加載哪一個版本,而無論其引用的版本如何。
這是一個示例:您的流程有兩個項目(模塊):項目A和項目B。項目A引用log4net.dll v1.1,項目B引用log4net.dll v1.2。兩個log4net DLL文件都複製到輸出文件夾,可是隻能有一個log4net.dll文件。
假設複製到輸出文件夾的文件是log4net.dll v1.2。假設到達的第一個代碼是Project A中的代碼,該代碼引用了log4net v1.1。運行時將在輸出文件夾中查找,找到不一樣版本的log4net,並失敗FileLoadException
。
還有另外一種可能。假設首先執行了項目B中的代碼,而且在嘗試使用log4net時,它成功加載了log4net.dll v1.2。片刻以後,Project A中的代碼將嘗試使用log4net v1.1,請參見該程序集已經加載了其餘版本,並拋出FileLoadException
。
若是您知道哪一個log4net版本將在輸出文件夾中,在這種狀況下能夠作的就是告訴運行時應該使用哪一個版本。只需app.config
在該runtime
部分的文件中添加如下幾行:
<?xml version="1.0" encoding="utf-8" ?> <configuration> ... <runtime> ... <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1"> <dependentAssembly> <assemblyIdentity name="log4net" publicKeyToken="669e0ddf0bb1aa2a" culture="neutral" /> <bindingRedirect oldVersion="0.0.0.0-5.0.0.0" newVersion="1.2.0" /> </dependentAssembly> </assemblyBinding> </runtime> ... </configuration>
這意味着,只要運行時想綁定到版本範圍爲0.0.0.0
to的程序集log4net 5.0.0.0
,它就會嘗試綁定到version 1.2.0
。
實際上,您沒必要手動添加這些重定向,由於它們是自動添加的。若是轉到啓動項目的「屬性」,則會看到如下設置:
默認狀況下選中此選項。它會自動檢測版本衝突並在.config
文件中生成綁定重定向。
當問題開始發生時
乍一看,綁定重定向可能看起來像是對全部問題的答案,但事實並不是如此。使用綁定重定向時,基本上使用的庫版本與預期不一樣。若是刪除方法怎麼辦?或方法的簽名已更改?在這種狀況下,調用該方法時,程序將因運行時錯誤而失敗。畢竟,建立版本是有緣由的。
若是確實存在此類問題,則有解決方法。查看個人文章:如何解決.NET引用和NuGet軟件包版本衝突[4]。
故障排除
當您有一個FileLoadException
或相似的東西時,我建議作的第一件事是查看Visual Studio中的「模塊」窗口。在這裏,您將看到全部已加載的模塊,並肯定您要加載的程序集是否已加載,使用哪一個版本以及從哪一個路徑加載。
除此以外,您還能夠查看程序集綁定日誌,也稱爲融合日誌。這些日誌將顯示在程序集綁定嘗試過程當中到底發生了什麼。您將看到運行時查找的程序集版本,運行時查找的文件夾以及故障點。
有幾種查看融合日誌的方法。首先,您必須啓用它們,由於默認狀況下它們是禁用的。您能夠經過將HKLM\Software\Microsoft\Fusion\ForceLog
值設置爲1並將HKLM\Software\Microsoft\Fusion\LogPath
值設置爲來在註冊表中手動啓用它們C:\FusionLogs
。日誌將自動出現。或者,您可使用Fusion Log Viewer,該軟件應以方式安裝在PC上fuslogvw.exe
。我建議使用「一切窗口」搜索之[5]類的程序來查找它。確保以管理員權限運行融合日誌查看器,以便可以啓用和禁用日誌。最近更流行的一種更現代的工具是Fusion ++[6]。
邊注
也許您不須要,可是我之前討厭不得不處理這類問題。例如一個邏輯上的問題,讓我構建一些東西,甚至解決一個生產錯誤,但其餘問題都好說,惟獨這個……。
在這件事上別無選擇,我不得不艱難地學習程序集綁定的內部工做。我發現,就像其餘全部內容同樣,一旦您理解了某些內容,它就會變得不那麼可怕,甚至變得再也不那麼有趣了。
所以,我但願本文對您有意義,並會在我走過的道路上爲您提供快速幫助。
References
[1]
Microsoft文檔: https://docs.microsoft.com/en-us/dotnet/framework/deployment/how-the-runtime-locates-assemblies[2]
全局程序集緩存: https://docs.microsoft.com/en-us/dotnet/framework/app-domains/gac[3]
gacutil.exe: https://docs.microsoft.com/en-us/dotnet/framework/tools/gacutil-exe-gac-tool[4]
如何解決.NET引用和NuGet軟件包版本衝突: https://michaelscodingspot.com/how-to-resolve-net-reference-and-nuget-package-version-conflicts/[5]
一切窗口」搜索之: https://www.voidtools.com/[6]
Fusion ++: https://github.com/awaescher/Fusion/