疑難排解組件參考

MSBuild 和 .NET 建置程序中最重要的工作之一是解析組件參考,這發生在 ResolveAssemblyReference 工作中。 本文介紹了 ResolveAssemblyReference 運作方式的一些詳細資訊,以及如何疑難排解當 ResolveAssemblyReference 無法解析參考時可能發生的建置失敗。 若要調查組件參考失敗,您可能需要安裝結構化記錄檢視器來檢視 MSBuild 記錄。 本文中的螢幕擷取畫面取自結構化記錄檢視器。

ResolveAssemblyReference 的目的是透過 <Reference> 項目來取得 .csproj 檔案 (或其他地方) 中指定的所有參考,並將其對應到檔案系統中的組件檔案路徑。

編譯器只能接受檔案系統上的 .dll 路徑做為參考,因此 ResolveAssemblyReference 會將專案檔中出現的字串 (例如 mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089) 轉換為路徑 (例如 C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.6.1\mscorlib.dll),然後透過 /r 參數傳遞給編譯器。

另外,ResolveAssemblyReference 遞歸地確定所有 .dll.exe 參考的完整集合 (實際上是圖表理論術語中的遞移閉包),並針對每個參考確定是否應將其複製到組建輸出目錄。 它不會執行實際的複製 (稍後在實際編譯步驟之後處理),但它會準備要複製的檔案項目清單。

ResolveAssemblyReferenceResolveAssemblyReferences 目標叫用:

記錄檢視器的螢幕擷取畫面,其中顯示在建置程序中呼叫 ResolveAssemblyReferences。

如果您注意到順序,ResolveAssemblyReferences 發生在 Compile 之前,當然,CopyFilesToOutputDirectory 發生在 Compile 之後。

注意

會叫用 MSBuild 安裝資料夾中標準.targets 檔案Microsoft.Common.CurrentVersion.targets中的 ResolveAssemblyReference 工作。 您也可以在 https://github.com/dotnet/msbuild/blob/a936b97e30679dcea4d99c362efa6f732c9d3587/src/Tasks/Microsoft.Common.CurrentVersion.targets#L1991-L2140 線上瀏覽 .NET SDK MSBuild 目標。 此連結準確顯示了 .targets 檔案中叫用 ResolveAssemblyReference 工作的位置。

ResolveAssemblyReference 輸入

ResolveAssemblyReference 全面記錄其輸入:

顯示 ResolveAssemblyReference 工作之輸入參數的螢幕擷取畫面。

Parameters 節點是所有工作的標準節點,但另外 ResolveAssemblyReference 也會在 [輸入] 下記錄自己的一組資訊 (與 Parameters 下的基本相同,但結構不同)。

最重要的輸入包括 AssembliesAssemblyFiles

    <ResolveAssemblyReference
        Assemblies="@(Reference)"
        AssemblyFiles="@(_ResolvedProjectReferencePaths);@(_ExplicitReference)"

當為專案叫用 ResolveAssemblyReference 時,Assemblies 會使用 Reference MSBuild 項目的內容。 所有中繼資料和組件參考 (包括 NuGet 參考) 都應包含在此項目中。 每個參考都附加了一組豐富的中繼資料:

顯示組件參考中繼資料的螢幕擷取畫面。

AssemblyFiles 來自 ResolveProjectReference 目標的名為 _ResolvedProjectReferencePaths 的輸出項目。 ResolveProjectReferenceResolveAssemblyReference 之前執行,並將 <ProjectReference> 項目轉換為磁碟上已建置之組件的路徑。 因此 AssemblyFiles 將包含目前專案之所有被參考專案所建置的組件:

顯示 AssemblyFiles 的螢幕擷取畫面。

另一個有用的輸入是布林值 FindDependencies 參數,它從 _FindDependencies 屬性取得其值:

FindDependencies="$(_FindDependencies)"

您可以在組建中將此屬性設定為 false 以關閉分析遞移相依組件。

ResolveAssemblyReference 演算法

ResolveAssemblyReference 工作的簡化演算法如下:

  1. 記錄輸入。
  2. 檢查 MSBUILDLOGVERBOSERARSEARCHRESULTS 環境變數。 將此變數設定為任何值,以取得更詳細的記錄。
  3. 初始化參考物件的資料表。
  4. obj 目錄 (若有) 讀取快取檔案。
  5. 計算相依項閉包。
  6. 建置輸出資料表。
  7. 將快取檔案寫入 obj 目錄。
  8. 記錄結果。

此演算法取得組件的輸入清單 (來自中繼資料和專案參考),擷耳其處理之每個組件的參考清單 (透過讀取中繼資料) 並建立所有被參考組件的完整集合 (遞移閉包),並從各種位置解析它們 (包括 GAC、AssemblyFoldersEx 等)。

被參考組件會反覆新增至清單,直到不再加入新的參考為止。 然後演算法會停止。

您提供給工作的直接參考稱為主要參考。 因為遞移參考而新增至集合的間接組件稱為相依項。 每個間接組件的記錄都會追蹤導致其包含的所有主要 (「根」) 項目及其對應的中繼資料。

ResolveAssemblyReference 工作的結果

ResolveAssemblyReference 提供結果的詳細記錄:

顯示結構化記錄檢視器中 ResolveAssemblyReference 結果的螢幕擷取畫面。

已解析的組件分為兩個類別:主要參考和相依項。 主要參考已明確指定為所建置專案的參考。 相依項是從參考的參考遞移推斷出來的。

重要

ResolveAssemblyReference 讀取組件中繼資料以確定所指定組件的參考。 當 C# 編譯器發出組件時,它只會新增實際需要的組件參考。 因此,當您編譯特定專案時,專案可能會指定不需要的參考,而該參考不會被製作到組件中。 可以新增不需要的參考至專案,其會被忽略。

CopyLocal 項目中繼資料

參考也可以有或沒有 CopyLocal 中繼資料。 如果參考具有 CopyLocal = true,則稍後 CopyFilesToOutputDirectory 目標會將其複製到輸出目錄。 在此範例中,DataFlowCopyLocal 設為 true,而 Immutable 則沒有:

顯示某些參考 CopyLocal 設定的螢幕擷取畫面。

如果 CopyLocal 中繼資料完全遺失,則預設為 true。 因此,ResolveAssemblyReference 預設會嘗試將相依項複製到輸出,除非它找到不這樣做的原因。 ResolveAssemblyReference 會記錄其選擇特定參考為 CopyLocal 或不選擇的原因。

下表列舉了 CopyLocal 決策的所有可能原因。 了解這些字串有助於在組建記錄中搜尋它們。

CopyLocal 狀態 描述
Undecided 複製本機狀態目前尚未決定。
YesBecauseOfHeuristic 參考應該具有 CopyLocal='true',因為無論出於何種原因它都不是 'no'。
YesBecauseReferenceItemHadMetadata 參考應該具有 CopyLocal='true',因為其來源項目具有 Private='true'
NoBecauseFrameworkFile 參考應該具有 CopyLocal='false',因為它是架構檔案。
NoBecausePrerequisite 參考應該具有 CopyLocal='false',因為它是必要條件檔案。
NoBecauseReferenceItemHadMetadata 參考應該具有 CopyLocal='false',因為 Private 屬性在專案中設定為 'false'。
NoBecauseReferenceResolvedFromGAC 參考應該具有 CopyLocal='false',因為它解析自 GAC。
NoBecauseReferenceFoundInGAC 舊版行為,CopyLocal='false' 在 GAC 中找到組件時 (即使在其他地方解析時也一樣)。
NoBecauseConflictVictim 參考應該具有 CopyLocal='false',因為它遺失了同名組件檔案之間的衝突。
NoBecauseUnresolved 參考未解析。 無法將其複製到 bin 目錄,因為找不到它。
NoBecauseEmbedded 參考已內嵌。 不應將其複製到 bin 目錄,因為它不會在執行階段載入。
NoBecauseParentReferencesFoundInGAC 屬性 copyLocalDependenciesWhenParentReferenceInGac 設定為 false,並且在 GAC 中找到了所有父來源項目。
NoBecauseBadImage 提供的組件檔案不應複製,因為它是不正確的映像,可能未受管理,可能根本不是組件。

私人項目中繼資料

確定 CopyLocal 非常重要的一環所有主要參考的 Private 中繼資料。 每個參考 (主要或相依) 都有包含所有主要參考 (來源項目) 的清單,這些參考造成該參考加入至閉包。

  • 如果沒有任何來源項目指定 Private 中繼資料,則 CopyLocal 設定為 True (或不設定,預設為 True)
  • 如果任何來源項目指定 Private=true,則 CopyLocal 設定為 True
  • 如果沒有一個來源組件指定 Private=true 並且至少有一個來源組件指定 Private=false,則 CopyLocal 設定為 False

哪個參考將 Private 設定為 false?

最後一點通常是 CopyLocal 設定為 false 的原因:This reference is not "CopyLocal" because at least one source item had "Private" set to "false" and no source items had "Private" set to "true".

MSBuild 不會告訴我們哪個參考將 Private 設為 false,但結構化記錄檢視器將 Private 中繼資料新增至上面指定的項目:

顯示結構化記錄檢視器中 Private 設定為 false 的螢幕擷取畫面。

這簡化了調查並準確地告訴您哪個參考導致使用 CopyLocal=false 設定有問題的相依項。

全域組件快取

全域組件快取 (GAC) 在決定是否將參考複製到輸出時扮演重要角色。 這很可惜,因為 GAC 的內容是機器特定的,這會導致可重現的組建發生問題 (因為不同機器上的行為會有所不同,取決於機器狀態,例如 GAC)。

最近對 ResolveAssemblyReference 進行了修復以緩解這種情況。 您可以透過 ResolveAssemblyReference 的這兩個新輸入來控制行為:

    CopyLocalDependenciesWhenParentReferenceInGac="$(CopyLocalDependenciesWhenParentReferenceInGac)"
    DoNotCopyLocalIfInGac="$(DoNotCopyLocalIfInGac)"

AssemblySearchPaths

有兩種方法可以自訂嘗試尋找組件時 ResolveAssemblyReference 搜尋的路徑清單。 要完全自訂清單,可以提前設定屬性 AssemblySearchPaths。 順序很重要;如果組件位於兩個位置,則 ResolveAssemblyReference 會在第一個位置找到組件後停止。

依預設,ResolveAssemblyReference 會搜尋十個位置 (如果使用 .NET SDK,則為四個),並且可以透過將相關標幟設為 false 來停用每個位置:

  • 透過將 AssemblySearchPath_UseCandidateAssemblyFiles 屬性設為 false,可以停用從目前專案搜尋檔案。
  • 透過將 AssemblySearchPath_UseReferencePath 屬性設為 false,可以停用搜尋參考路徑屬性 (從 .user 檔案)。
  • 透過將 AssemblySearchPath_UseHintPathFromItem 屬性設為 false,可以停用使用項目的提示路徑。
  • 透過將 AssemblySearchPath_UseTargetFrameworkDirectory 屬性設定為 false,可以停用將目錄與 MSBuild 的目標執行階段一起使用。
  • 透過將 AssemblySearchPath_UseAssemblyFoldersConfigFileSearchPath 屬性設為 false,可以停用從 AssemblyFolders.config 搜尋組件資料夾。
  • 透過將 AssemblySearchPath_UseRegistry 屬性設為 false 可以停用搜尋登錄。
  • 透過將 AssemblySearchPath_UseAssemblyFolders 屬性設為 false,可以停用搜尋舊版已登錄組件資料夾。
  • 透過將 AssemblySearchPath_UseGAC 屬性設為 false 可以停用查閱 GAC。
  • 透過將 AssemblySearchPath_UseRawFileName 屬性設為 false,可以停用將參考的 Include 視為實際檔案名稱。
  • 透過將 AssemblySearchPath_UseOutDir 屬性設為 false,可以停用檢查應用程式的輸出資料夾。

發生衝突

常見的情況是 MSBuild 發出警告,指出不同的參考使用同一組件的不同版本。 解決方案通常涉及將繫結重新導向新增至 app.config 檔案。

調查這些衝突的一個有用方法是在 MSBuild 結構化記錄檢視器中搜尋「發生衝突」。 它會顯示相關詳細資訊,指出哪個參考需要有問題組件的哪個版本。