通过


排查程序集引用问题

在 MSBuild 和 .NET 构建过程中,最重要的任务之一是在 ResolveAssemblyReference 任务中解析程序集引用。 本文解释了如何 ResolveAssemblyReference 工作的一些详细信息,以及在 ResolveAssemblyReference 无法解决引用时如何排查和解决可能发生的生成失败。 若要调查程序集引用失败,可能需要安装 结构化日志查看器 以查看 MSBuild 日志。 本文中的屏幕截图取自结构化日志查看器。

目的是通过.csproj文件(或其他位置)中指定的<Reference>项,获取所有引用,并将其映射为文件系统中的程序集文件路径。

编译器只能接受文件系统上的一个 .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 开关传递给编译器。

此外,还以递归方式确定所有引用的完整集合(在图论术语中实际上是可传递闭包),并为每个引用确定是否应复制到构建输出目录中。 它不会执行实际复制(稍后在实际编译步骤之后进行处理),但它准备要复制的文件的项列表。

ResolveAssemblyReferenceResolveAssemblyReferences 目标调用:

显示生成过程中调用 ResolveAssemblyReferences 时的日志查看器的屏幕截图。

如果你注意到排序, ResolveAssemblyReferences 发生在之前 Compile,当然, CopyFilesToOutputDirectory 发生在之后 Compile

注释

ResolveAssemblyReference 任务在 MSBuild 安装文件夹中的标准 .targets 文件中 Microsoft.Common.CurrentVersion.targets 调用。 您还可以在线浏览 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)"

Assemblies使用 MSBuild 项Reference的内容,在ResolveAssemblyReference为项目调用时。 所有元数据和程序集引用(包括 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 等。

当没有更多新引用被添加时,引用程序集将以迭代方式添加到列表中。 然后算法停止。

你提供给任务的直接引用称为“主要引用”。 由于传递引用而添加到集合中的间接程序集称为“Dependency”。 每个间接程序集的记录都会记录所有导致其包含的主要(“根”)项及其对应的元数据。

ResolveAssemblyReference 任务的结果

ResolveAssemblyReference 提供结果的详细日志记录:

显示结构化日志查看器中 ResolveAssemblyReference 结果的屏幕截图。

已解析的程序集分为两个类别:主引用和依赖项。 主要参考被显式指定为正在构建的项目中的参考。 通过传递方式从引用的引用中推断出依赖项。

重要

ResolveAssemblyReference 读取程序集元数据以确定给定程序集的引用。 当 C# 编译器发出程序集时,它只会添加对实际需要的程序集的引用。 因此,在将某个项目编译成程序集时,该项目可能会指定一个不必要的引用,该引用不会嵌入到程序集的构建中。 可以添加对不需要的项目的引用;它们将被忽略。

CopyLocal 项元数据

引用也可以具有 CopyLocal 元数据,也可以没有。 如果引用具有 CopyLocal = true,那么稍后由CopyFilesToOutputDirectory目标任务复制到输出目录。 在此示例中,DataFlowCopyLocal 设置为 true,而 Immutable 则未设置为 true:

显示某些引用的 CopyLocal 设置的屏幕截图。

如果CopyLocal元数据完全丢失,则默认假设其为 true。 因此 ResolveAssemblyReference ,默认情况下会尝试将依赖项复制到输出,除非它找到不这样做的原因。 ResolveAssemblyReference 记录了它选择某个引用作为 CopyLocal 或不选择的原因。

下表列举了决策的所有可能原因 CopyLocal 。 知道这些字符串,以便在生成日志中搜索它们,这非常有用。

CopyLocal 状态 Description
Undecided 复制本地状态目前尚未决定。
YesBecauseOfHeuristic 引用应该有 CopyLocal='true' ,因为没有任何理由让它为“否”。
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 所有主要引用上的元数据。 每个引用(主要或依赖项)都有一个包含所有导致该引用被加入封闭 (closure) 的主要引用(源项)的列表。

  • 如果未指定任何源项指定 Private 元数据, CopyLocal 则设置为 True (或未设置,默认为 True
  • 如果任一源项指定 Private=trueCopyLocal 则设置为 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 在第一个位置找到后会停止。

AssemblySearchPaths 是以分号分隔的列表。 它支持一组内置占位符(例如, {HintPathFromItem} 以及 {GAC})在解析过程中扩展到实际位置。

默认情况下,非 SDK 样式项目使用以下搜索路径顺序:

  1. 候选程序集文件 ({CandidateAssemblyFiles}
  2. 属性 ReferencePath$(ReferencePath)
  3. 来自 <Reference> 项目的提示路径({HintPathFromItem}
  4. 目标框架目录 ({TargetFrameworkDirectory}
  5. 来自 AssemblyFolders.config ($(AssemblyFoldersConfigFileSearchPath)) 的程序集文件夹
  6. 注册表 ({Registry:...}
  7. 旧版注册的程序集文件夹 ({AssemblyFolders}
  8. 全局程序集缓存 (GAC) ({GAC}
  9. <Reference Include="..."> 值视为真实文件名 ({RawFileName}
  10. 输出目录 ($(OutDir)

.NET SDK 设置较小的默认值 AssemblySearchPaths (默认情况下不包括 GAC/注册表/输出目录搜索):

  • {CandidateAssemblyFiles}
  • {HintPathFromItem}
  • {TargetFrameworkDirectory}
  • {RawFileName}

若要查看生成的有效值,请检查由 记录的 输入(例如,在 MSBuild 结构化日志查看器中),或使用 预处理项目。

可以通过将每个条目的相关标志设置为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。
  • 要禁用将引用的 Include 视为真实文件名,可以将AssemblySearchPath_UseRawFileName属性设置为 false。
  • 通过将 AssemblySearchPath_UseOutDir 属性设置为 false 来禁用检查应用程序的输出文件夹。

发生了冲突

常见情况是 MSBuild 发出警告,指出不同引用使用了同一程序集的不同版本。 该解决方案通常涉及将绑定重定向添加到 app.config 文件。

调查这些冲突的一种有用方法是在 MSBuild 结构化日志查看器中搜索“存在冲突”。 其中显示了有关哪些引用需要哪些版本的程序集的详细信息。