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 版本:
在下图中,版本约束为 >= 2.1,但源上未提供版本 2.1,因此 NuGet 选取能找到的下一个最低版本,在此示例中即为 2.2:
当源上未提供应用程序指定的确切版本号(如 1.2)时,NuGet 将在尝试安装或还原包时出错并导致失败:
可变版本
可变依赖项版本由 * 字符指定。 例如 6.0.*
。 此版本规范显示“使用最新的 6.0.x 版本”;4.*
表示“使用最新的 4.x 版本”。使用可变版本可减少对项目文件的更改,同时保持与依赖项的最新版本同步。
可变版本只能在项目级别指定。
使用可变版本时,NuGet 将解析与版本模式匹配的最高包版本,例如,请求 6.0.*
将获得以 6.0 开头的最高包版本:
版本 | 服务器上存在的版本 | 解决方法 | 原因 | 备注 |
---|---|---|---|---|
* | 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 上的依赖项是对图中应用程序的直接依赖,因此将使用该版本:
警告
“直接依赖项优先”规则可导致包版本降级,可能破坏关系图中的其他依赖项。 降级包后,NuGet 会添加发出警告以提醒用户。
这条规则还能提高大型依赖图的效率。 当同一子图中的近依赖项的版本高于远依赖项时,NuGet 将忽略该依赖项,并且 NuGet 还会忽略关系图上该分支上的所有剩余依赖项。
例如在下图中,由于使用了包 C 2.0.0,因此 NuGet 忽略了子图中引用早先版包 C 的所有分支:
有了此规则,NuGet 会尝试遵循包作者的意图。 在下图中,包 A 的作者已从包 C 2.0.0 显式降级到包 C 1.0.0。
应用程序所有者可以选择将包 C 升级到高于 2.0.0 的版本,这样就不会进一步降级包 C 的版本。在这种情况下,不会引发任何警告。
等距依赖项
当应用程序在关系图中的不同子图中引用不同的包版本时,NuGet 将使用满足所有版本要求的最低版本(与最低适用版本和可变版本规则一样)。 例如在下图中,2.0 版本的包 B 满足另一个 >=1.0.0 约束,因此使用此包:
请注意,包不需要与要应用的等距依赖项规则保持相同的距离。 在下图中,在包 C 子图中选择包 D 2.0.0,在包 A 的子图中选择包 D 3.0.0。在应用程序子图中,没有对包 D 的直接依赖项,因此应用了最低适用的版本规则,并且选择了版本 3.0.0。
某些情况下无法满足所有版本要求。 如下所示,如果包 A 明确要求包 B 1.0.0,而包 C 要求包 B >=2.0.0,NuGet 则无法解析依赖项并显示错误。
在此情况下,顶层使用者(应用程序或包)应在包 B 上添加其自己的直接依赖项,以便应用直接依赖项优先规则。
带有 PackageReference 的版本范围和预发布版本
包同时具有稳定版本和预发布版本并不罕见。
解析依赖项关系图时,NuGet 决定是否基于单个规则考虑包的预发布版本:If the project or any packages within the graph request a prerelease version of a package, then include both prerelease or stable versions, otherwise consider stable versions only.
在实践中,根据最低适用规则,这意味着:
版本范围 | 可用版本 | 所选版本 |
---|---|---|
[1.0.0, 2.0.0) | 1.2.0-beta.1、1.2.0、 | 1.2.0 |
[1.0.0, 2.0.0-0) | 1.2.0-beta.1、1.2.0、 | 1.2.0-beta.1 |
[1.0.0, 2.0.0) | 1.2.0-beta.1、2.0.0-beta.3 | 无,引发 NU1103。 |
[1.0.0, 2.0.0-rc) | 1.2.0-beta.1、2.0.0-beta.3 | 1.2.0-beta.1 |
利用 packages.config 解析依赖项
利用 packages.config
,项目依赖项 将作为简单列表写入 packages.config
。 这些包的所有依赖项也将写入同一列表。 安装包时,NuGet 可能还会修改 .csproj
文件、app.config
、web.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
进程会随着依赖项关系图的规模增大而愈加复杂。 每次安装新包都需要遍历整个关系图,并且可能引发版本冲突。 发生冲突时,安装将停止,此时项目处于不确定状态,很可能导致项目文件本身发生更改。 使用其他包管理格式时则不会出现此问题。
带有 packages.config 的版本范围和预发布版本
packages.config 解析不允许在图表中混合使用稳定和预发布依赖项。
如果依赖项以类似 [1.0.0, 2.0.0)
的范围表示,则图表中不允许预发布版本。
管理依赖项资产
使用 PackageReference 格式时,可以控制依赖项中的哪些资产可流入顶层项目。 有关详细信息,请参阅 PackageReference。
当顶层项目本身是一个包时,还需要使用其依赖项在 .nuspec
文件中列出的 include
和 exclude
属性来控制此流。 请参阅 .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 页面,我们很乐意听取您的体验反馈。