NuGet 如何解析包依赖项

每当安装或重新安装包(包括在还原过程中安装)时,NuGet 还会安装第一个包所依赖的任何其他包。

这些直接依赖项可能本身也具有依赖项,并可能继续延伸到任意深度。 这便形成了所谓的“依赖项关系图”,用于说明各级包之间的关系

当多个包具有相同的依赖项时,同一个包 ID 会在关系图中多次出现且可能具有不同的版本约束。 但是,一个项目中只能使用给定包的一个版本,因此 NuGet 必须选择要使用的版本。 确切流程取决于要使用的包管理格式。

利用 PackageReference 解析依赖项

当将包安装到使用 PackageReference 格式的项目中时,NuGet 将添加对相应文件中的平面包关系图的引用并提前解决冲突。 此过程称为“传递还原”。 重新安装或还原包指的是下载关系图中列出的包的过程,此过程可加快生成的速度和提高其可预测性。

还可以利用 2.8.* 等可变版本,以避免为了能使用最新版本的包而修改项目。 使用浮动版本时,我们建议启用锁定文件功能以确保可重复性。

当 NuGet 还原进程在生成之前运行时,它将首先解析内存中的依赖项,然后将生成的关系图写入名为 project.assets.json 的文件。

资产文件位于 MSBuildProjectExtensionsPath,它默认是项目的“obj”文件夹。 MSBuild 随后将读取此文件并将其转换成一组文件夹(可在其中找到潜在引用),然后将它们添加到内存中的项目树。

project.assets.json 文件是临时的,不应添加到源代码管理中。 默认情况下,此文件将在 .gitignore.tfignore 中列出。 请参阅包与源代码管理

依赖项解析规则

传递还原应用 4 条主要规则来解析依赖项:最低适用版本可变版本direct-dependency-wins等距依赖项

最低适用版本

最低适用版本规则根据包依赖项的定义还原包的最低可能版本。 此规则还适用于应用程序上或类库上未声明为可变的依赖项。

例如在下图中,1.0 beta 版本低于 1.0,因此 NuGet 选择 1.0 版本:

Choosing the lowest applicable version

在下图中,版本约束为 >= 2.1,但源上未提供版本 2.1,因此 NuGet 选取能找到的下一个最低版本,在此示例中即为 2.2:

Choosing the next lowest version available on the feed

当源上未提供应用程序指定的确切版本号(如 1.2)时,NuGet 将在尝试安装或还原包时出错并导致失败:

NuGet generates an error when an exact package version is not available

可变版本

可变依赖项版本由 * 字符指定。 例如 6.0.*。 此版本规范显示“使用最新的 6.0.x 版本”;4.* 表示“使用最新的 4.x 版本”。使用可变版本可减少对项目文件的更改,同时保持与依赖项的最新版本同步。 可变版本只能在项目级别指定。

使用可变版本时,NuGet 将解析与版本模式匹配的最高包版本,例如,请求 6.0.* 将获得以 6.0 开头的最高包版本:

Choosing version 6.0.1 when a floating version 6.0.* is requested

版本 服务器上存在的版本 解决方法 原因 说明
* 1.1.0
1.1.1
1.2.0
1.3.0-alpha 版本
1.2.0 最高稳定版本。
1.1.* 1.1.0
1.1.1
1.1.2-alpha 版本
1.2.0-alpha 版本
1.1.1 遵循指定模式的最高稳定版本。
*-* 1.1.0
1.1.1
1.1.2-alpha 版本
1.3.0-beta 版本
1.3.0-beta 版本 包含不稳定版本的最高版本。 在 Visual Studio 版本 16.6、NuGet 版本5.6、.NET Core SDK 版本 3.1.300 中提供
1.1.*-* 1.1.0
1.1.1
1.1.2-alpha 版本
1.1.2-beta 版本
1.3.0-beta 版本
1.1.2-beta 版本 遵循模式并包含不稳定版本的最高版本。 在 Visual Studio 版本 16.6、NuGet 版本5.6、.NET Core SDK 版本 3.1.300 中提供

注意

可变版本解决方法不考虑是否列出包。 如果全局包文件夹中的包满足条件,浮动版本解析将在本地解决。

直接依赖项优先

当应用程序的包图包含同一子图中包的不同版本,并且其中一个版本是该子图中的直接依赖项时,将为该子图选择该版本,并忽略其余版本。 通过此行为,应用程序能够替代依赖项关系图中的任何特定包版本。

在下面的示例中,应用程序直接依赖于版本约束为 >=2.0.0 的包 B。 应用程序还依赖于版本约束为 >=1.0.0 的包 A,此包依赖于包 B。 在关系图中,由于包 B 2.0.0 上的依赖项是对图中应用程序的直接依赖,因此将使用该版本:

Application using the Direct dependency wins rule

警告

“直接依赖项优先”规则可导致包版本降级,可能破坏关系图中的其他依赖项。 降级包后,NuGet 会添加发出警告以提醒用户

这条规则还能提高大型依赖图的效率。 当同一子图中的近依赖项的版本高于远依赖项时,NuGet 将忽略该依赖项,并且 NuGet 还会忽略关系图上该分支上的所有剩余依赖项。

例如在下图中,由于使用了包 C 2.0.0,因此 NuGet 忽略了子图中引用早先版包 C 的所有分支:

When NuGet ignores a package in the graph, it ignores that entire branch

有了此规则,NuGet 会尝试遵循包作者的意图。 在下图中,包 A 的作者已从包 C 2.0.0 显式降级到包 C 1.0.0。

When a package author explicitly downgrades, NuGet honors that.

应用程序所有者可以选择将包 C 升级到高于 2.0.0 的版本,这样就不会进一步降级包 C 的版本。在这种情况下,不会引发任何警告。

When an application honor adds a direct dependency for a downgraded package, NuGet honors that.

等距依赖项

当应用程序在关系图中的不同子图中引用不同的包版本时,NuGet 将使用满足所有版本要求的最低版本(与最低适用版本可变版本规则一样)。 例如在下图中,2.0 版本的包 B 满足另一个 >=1.0.0 约束,因此使用此包:

Resolving cousin dependencies using the lower version that satisfies all constraints

请注意,包不需要与要应用的等距依赖项规则保持相同的距离。 在下图中,在包 C 子图中选择包 D 2.0.0,在包 A 的子图中选择包 D 3.0.0。在应用程序子图中,没有对包 D 的直接依赖项,因此应用了最低适用的版本规则,并且选择了版本 3.0.0。

Resolving cousin dependencies using the lower version that satisfies all constraints at different distances

某些情况下无法满足所有版本要求。 如下所示,如果包 A 明确要求包 B 1.0.0,而包 C 要求包 B >=2.0.0,NuGet 则无法解析依赖项并显示错误。

Unresolvable dependencies due to an exact version requirement

在此情况下,顶层使用者(应用程序或包)应在包 B 上添加其自己的直接依赖项,以便应用直接依赖项优先规则。

利用 packages.config 解析依赖项

利用 packages.config,项目依赖项 将作为简单列表写入 packages.config。 这些包的所有依赖项也将写入同一列表。 安装包时,NuGet 可能还会修改 .csproj 文件、app.configweb.config 和其他单独文件。

利用 packages.config,NuGet 可尝试解决在安装每个单独包期间出现的依赖项冲突。 也就是说,如果正在安装包 A 并且其依赖于包 B,同时包 B 已作为其他项的依赖项在 packages.config 中列出,则 NuGet 将比较所请求的包 B 版本,并尝试找到满足所有版本约束的版本。 具体而言,NuGet 将选择可满足依赖项的较低 major.minor 版本

默认情况下,NuGet 2.8 会查找最低的修补程序版本(请参阅 NuGet 2.8 发行说明)。 可以通过 NuGet.Config 中的 DependencyVersion 属性和命令行上的 -DependencyVersion 开关控制此设置。

用于解析依赖项的 packages.config 进程会随着依赖项关系图的规模增大而愈加复杂。 每次安装新包都需要遍历整个关系图,并且可能引发版本冲突。 发生冲突时,安装将停止,此时项目处于不确定状态,很可能导致项目文件本身发生更改。 使用其他包管理格式时则不会出现此问题。

管理依赖项资产

使用 PackageReference 格式时,可以控制依赖项中的哪些资产可流入顶层项目。 有关详细信息,请参阅 PackageReference

当顶层项目本身是一个包时,还需要使用其依赖项在 .nuspec 文件中列出的 includeexclude 属性来控制此流。 请参阅 .nuspec 引用 - 依赖项

排除引用

对于有些方案,同一项目中可能多次引用具有相同名称的程序集,并因此生成设计时和生成时错误。 例如,某个项目既包含自定义版本的 C.dll,又引用同样包含 C.dll 的包 C。 同时,该项目还依赖于同样依赖于包 C 和 C.dll 的包 B。 在此情况下,NuGet 无法确定要使用哪一个 C.dll,但你也不能直接删除包 C 上的项目依赖项,因为包 B 也依赖于此依赖项。

要解决此问题,必须直接引用所需的 C.dll(或使用其他引用正确对象的包),然后在包 C 上添加不包括其所有资产的依赖项。 此方法如下所示,具体取决于当前使用的包管理格式:

  • PackageReference:在依赖项中添加 ExcludeAssets="All"

    <PackageReference Include="PackageC" Version="1.0.0" ExcludeAssets="All" />
    
  • packages.config:从 .csproj 文件中删除对 PackageC 的引用,以便它仅引用所需版本的 C.dll

在安装包期间更新依赖项

如果依赖项版本已满足,则在其他程序包安装期间不会更新依赖项。 例如,假设包 A 依赖于包 B 并且指定版本号 1.0。 源存储库包含程序包 B 的版本 1.0、1.1 和 1.2。如果将 A 安装在已包含 B 版本 1.0 的项目中,则 B 1.0 将保持使用状态,因为它满足版本限制。 但是,如果包 A 请求 1.1 或更高版本的 B,则会安装 B 1.2。

解决包不兼容错误

在包还原操作期间,可能会看到错误“一个或多个包不兼容...”或包与项目的目标框架“不兼容”。

如果项目中引用的一个或多个包未指示其支持包的目标框架,则会出现此错误;即,包的 lib 文件夹中不包含与此项目兼容的目标框架的适用 DLL。 (请参阅目标框架获取列表。)

例如,如果项目面向 netstandard1.6,并且你尝试安装仅在 lib\net20\lib\net45 文件夹中包含 DLL 的包,则将看到类似如下针对包或其依赖项的消息:

Restoring packages for myproject.csproj...
Package ContosoUtilities 2.1.2.3 is not compatible with netstandard1.6 (.NETStandard,Version=v1.6). Package ContosoUtilities 2.1.2.3 supports:
  - net20 (.NETFramework,Version=v2.0)
  - net45 (.NETFramework,Version=v4.5)
Package ContosoCore 0.86.0 is not compatible with netstandard1.6 (.NETStandard,Version=v1.6). Package ContosoCore 0.86.0 supports:
  - 11 (11,Version=v0.0)
  - net20 (.NETFramework,Version=v2.0)
  - sl3 (Silverlight,Version=v3.0)
  - sl4 (Silverlight,Version=v4.0)
One or more packages are incompatible with .NETStandard,Version=v1.6.
Package restore failed. Rolling back package changes for 'MyProject'.

要解决不兼容问题,请执行下列操作之一:

  • 将项目重定向到要使用的包所支持的框架。
  • 联系包创建者并与其协作添加对所选框架的支持。 nuget.org 中每个列出包的页面均针对此目的提供了“联系所有者”链接

提示

替代解决方案:NuGetSolver 是由 Microsoft DevLabs 开发的 Visual Studio 扩展,旨在帮助解决依赖项冲突问题。 它可以自动执行识别和解决这些问题的过程。 有关更多详细信息,请访问 Visual Studio Marketplace 上的 NuGetSolver 页面,我们很乐意听取您的体验反馈。