NuGet 工程手册:依赖恢复、私有源与可重建 .NET 构建
.csproj 里只有三条 PackageReference,恢复时却可能访问多个源、读取多层配置、调用凭证插件、选择几十个传递依赖,再按 Target Framework 和 Runtime Identifier 选择不同资产。开发机命中全局缓存时,这条链几乎不可见;到了干净 CI,401、NU110x、证书失败和运行时资产缺失才一起出现。
NuGet 的价值不是“把 DLL 下载下来”,而是把包标识、版本约束、依赖图、来源、认证、缓存和 MSBuild 恢复目标连接起来。架构师要让这条链可解释、可审计、可在空缓存重建,也能在升级失败时回到上一组已知输入。
restore 的来源不只在项目文件里
现代 .NET 项目用 PackageReference 声明直接依赖,但最终恢复结果还受 SDK 版本、导入的 MSBuild 属性、分层 nuget.config、包源映射、中央版本文件、锁文件、凭证插件和本地缓存共同影响。.csproj 相同而开发机与 CI 得到不同结果时,不能只比较三行包引用;要继续追踪哪个客户端执行了 restore、合并了哪些配置、访问了哪个源,以及为目标框架和 RID 选中了哪些资产。
这条证据链以 PackageReference 项目的依赖恢复和可重建构建为中心,并延伸到私有源认证、缓存、签名、漏洞审计、多目标构建与回滚。.nupkg 的生产发布、完整 MSBuild target 执行模型和制品平台服务端治理各有独立职责。旧 .NET Framework 工程若仍使用 packages.config,应先识别其安装目录、传递依赖和迁移限制,再决定是否切换到 PackageReference,不能直接套用现代恢复假设。
先确认是谁在执行 restore
先检查当前命令入口,不凭 IDE 界面猜测 SDK 与客户端版本:
dotnet --info
dotnet --list-sdks
dotnet nuget --help
dotnet restore --help项目若有 global.json,还要从仓库根目录执行 dotnet --version,确认实际选中的 SDK。没有 SDK、只有 .NET Runtime 的机器不能完成 restore/build;先从 .NET 官方下载入口安装项目批准的 SDK。
几个入口的职责不同:
dotnet CLI 随 .NET SDK 提供,适合 SDK-style 项目的 restore、build、test 和 NuGet 源管理。nuget.exe 是独立 Windows 客户端,仍服务部分旧工程、打包和管理场景,不应假设它与 dotnet 支持相同参数和凭证插件。Visual Studio 内含图形化包管理体验和自己的交互式身份上下文,但项目文件、配置层和恢复结果仍需能被无界面 CI 重现。
MSBuild 可以通过 -t:Restore 进入 NuGet restore,适用于 solution 或传统构建链;dotnet build 默认可能先触发 restore。
如果当前机器只有 Runtime 或没有 nuget.exe,不要把“命令不存在”写成 NuGet 服务故障。先明确项目所需 SDK、客户端与目标框架,再进行验证。
创建最小项目并分离恢复与构建
mkdir RestoreProbe
cd RestoreProbe
dotnet new console --name RestoreProbe
cd RestoreProbe
dotnet add package Microsoft.Extensions.Logging.Abstractions
dotnet restore --no-cache
dotnet build --no-restore
dotnet run --no-builddotnet add package 会修改项目中的包引用,restore 计算依赖图并写入 obj 下的恢复资产,build --no-restore 则证明构建没有悄悄重新解析依赖。项目模板和目标框架由实际 SDK 决定,因此团队脚本应固定模板输入或直接维护受控项目文件。
在 SDK-style 项目中,直接依赖通常写成:
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions"
Version="9.0.0" />
</ItemGroup>PackageReference 说明强调它记录项目的直接依赖;传递依赖由恢复阶段计算。不要因为项目文件里看不到某个包,就断定运行时没有它。
从 PackageReference 到 assets 文件
恢复过程可以按六步理解:
MSBuild 先求值项目、导入文件、Target Framework、RID 与条件属性。NuGet 收集直接 PackageReference 和版本约束。合并有效 nuget.config,得到源、映射、缓存和认证行为。
查询所有允许的来源,计算传递依赖图并处理冲突。将包放入 global-packages folder,生成 obj/project.assets.json 等恢复产物。build 根据目标框架与 RID 从 assets 中选择 compile、runtime、native、analyzer 或 build 资产。
查看直接和传递依赖:
dotnet list package
dotnet list package --include-transitive若当前 SDK 提示命令语法已调整,以 dotnet package list --help 或 dotnet list package --help 为准。团队文档要锁定项目 SDK,而不是让所有机器随全局 CLI 演进。
NuGet 依赖解析规则描述了 PackageReference 的图解析。排障时重点看 project.assets.json 中实际选中的版本和资产,而不是只看 IDE 的已安装列表。该文件是生成物,不应手工编辑。
nuget.config 分层:同一项目为什么访问不同源
NuGet 会累积机器级、用户级、从当前目录向上的配置以及显式指定的配置文件。离仓库越近的设置可以覆盖同名项,但不同 section 的合并行为需要按 nuget.config 参考确认。最常见的隐形差异,是开发者用户配置多了一个源或旧凭证,而 CI 只看到仓库配置。
仓库根目录建议放不含 secret 的配置,并显式清除未批准来源:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<clear />
<add key="public" value="https://api.nuget.org/v3/index.json" />
<add key="private" value="https://packages.example.invalid/v3/index.json" />
</packageSources>
</configuration>示例私有域名不可访问。列出当前有效来源时使用:
dotnet nuget list source --format Detailed当结果和预期不一致,再显式指定仓库配置验证:
dotnet restore --configfile ./nuget.config --verbosity normal--configfile 会改变配置发现范围,适合作为团队契约,但要确认凭证提供器和企业证书仍可用。不要为了“修好 CI”在命令上临时追加未批准 --source,否则审计时无法解释包来自哪里。
Package Source Mapping 防止同名包走错来源
PackageReference restore 不按 packageSources 的书写顺序决定来源。多个源都含同一 ID 时,依赖混淆和不可重复恢复风险会显著增加。Package Source Mapping 用包 ID 模式约束允许访问的源:
<packageSourceMapping>
<clear />
<packageSource key="private">
<package pattern="Company.*" />
</packageSource>
<packageSource key="public">
<package pattern="*" />
</packageSource>
</packageSourceMapping>这里 private 和 public 必须与 packageSources 的 key 完全一致。更稳妥的设计是让内部命名空间只匹配私有源,并避免一个宽泛 * 同时覆盖内部包。新增包若不匹配任何规则,restore 应明确失败,由依赖 owner 决定来源,而不是放宽规则到“什么都能下”。
另一种架构是只让客户端访问内部代理源,由服务端代理公开上游。这能收敛出口、留存和审计,但服务端高可用、同步策略与灾备属于制品平台专题;客户端仍要有 source mapping、凭证和断网行为的清晰契约。
Central Package Management:集中版本,不集中所有决策
多项目仓库可以在根目录创建 Directory.Packages.props:
<Project>
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions"
Version="9.0.0" />
</ItemGroup>
</Project>项目保留不带版本的引用:
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />Central Package Management减少版本漂移,但它不替代项目 owner。条件版本、多目标框架和 transitive pinning 都会改变最终图;尤其为库启用传递依赖固定时,可能把原本传递的包提升到发布依赖。每次集中升级仍要运行受影响项目的 restore/build/test,而不是只确认 props XML 能解析。
packages.lock.json 与 locked mode
启用锁文件:
<PropertyGroup>
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
</PropertyGroup>执行 dotnet restore 后生成 packages.lock.json。对应用和依赖图顶端的可执行项目,通常提交锁文件;对作为公共依赖的库,锁文件不能约束最终消费者解析出的完整图,应结合发布测试决定策略。
CI 使用只读锁定恢复:
dotnet restore --locked-mode
dotnet build --no-restore
dotnet test --no-buildlocked mode 的价值是:项目依赖声明变化却没有同步更新 lock 时直接失败。它不是离线包仓库,也不证明包字节来自批准来源。来源、签名、缓存和 audit 仍需独立治理。
更新流程应显式分开:开发分支修改版本,运行普通 restore 生成新 lock,审查依赖图和漏洞,再合并;日常 CI 只使用 locked mode。不要在失败的 CI 中删除 lock 让构建“自己修复”。
缓存、离线与恢复证据
PackageReference 默认使用 global-packages folder;NuGet 还维护 HTTP cache、临时目录和插件缓存。查看位置:
dotnet nuget locals all --list清理前先判断是哪一层损坏:
dotnet nuget locals http-cache --clear
dotnet nuget locals global-packages --clear
dotnet nuget locals all --clearall --clear 代价最大,会让后续构建重新下载所有包;共享 runner 上还可能与其他作业并发。优先复现具体包、具体源和具体缓存层,再清理。
缓存键至少应包含 OS、SDK、目标框架、RID、nuget.config、中央版本文件和 lock 文件摘要。缓存恢复后仍运行 dotnet restore --locked-mode,不能把缓存命中当 restore 成功。
真正离线构建需要预先批准并镜像完整包闭包、保存元数据与许可证、验证目标 TFM/RID 资产,并阻止未镜像包静默访问公网。一次在线构建后的 global cache 只是偶然快照,不是可审计离线方案。
私有源、凭证、代理与 CA
添加无凭证的源定义:
dotnet nuget add source https://packages.example.invalid/v3/index.json \
--name private \
--configfile ./nuget.config源 URL 可以进入仓库,secret 不可以。认证源说明建议优先使用源提供的 credential provider;CI 则使用短期、最小权限、可撤销的身份。不同入口的交互行为不同,dotnet restore 默认非交互,开发者首次登录可能需要:
dotnet restore --interactive环境变量凭证遵循 NuGetPackageSourceCredentials_{source-name} 命名和特定值格式,但过期变量可能以更高优先级遮蔽新配置,而且日志不一定明确说明命中了它。排查 401 时要检查凭证优先级,不能只重复登录。
代理和 CA 失败要分层处理:
用系统和企业标准工具验证源 URL、DNS、代理和 TLS 链。确认 dotnet 进程实际继承代理环境。将企业根 CA 安装进 runner 与容器所使用的信任库。
再验证 credential provider 或 token scope。
不要把代理密码提交到 nuget.config,也不要使用 disableTLSCertificateValidation 作为常规企业方案。关闭验证只会把证书配置错误变成中间人和篡改风险。
签名、审计与依赖供应链
NuGet 包签名用于验证完整性与签名身份,但能否强制、信任哪些签名者以及根证书是否可用,需要按组织策略配置。signatureValidationMode=require 与 trustedSigners 可以形成严格校验约束;启用前应在所有 OS/容器镜像验证证书链,否则会出现 NU3018、NU3028 等信任失败。
漏洞审计可在 restore 中产生 NU190x 警告,也可显式查看:
dotnet list package --vulnerable --include-transitive
dotnet list package --deprecated
dotnet list package --outdated命令名以当前 SDK 帮助为准。NuGet Audit还支持独立 auditSources,使组织只从内部代理恢复包时仍能查询批准的漏洞源。审计告警不能一律升级,也不能一律忽略:需要记录可达性、修复版本、替代包、业务暴露面、例外 owner 与到期日。
供应链评审至少同时查看包 ID、版本、来源映射、签名/哈希证据、许可证、传递依赖和构建资产。analyzer、source generator、build/buildTransitive 资产会在编译期影响项目,风险高于一个只在运行时被调用的普通库。
多目标框架、RID 与资产选择
<PropertyGroup>
<TargetFrameworks>net8.0;net9.0</TargetFrameworks>
<RuntimeIdentifiers>win-x64;linux-x64;linux-arm64</RuntimeIdentifiers>
</PropertyGroup>同一包可能为不同 TFM 提供不同 compile/runtime 资产,为 RID 提供 native 资产。restore 成功只说明图被求出,不代表每个组合都能运行。CI 应按真实支持矩阵逐格 build/test/publish,至少验证最老受支持 TFM 和每类 native RID。
遇到兼容性错误时,不要先强行选择更老版本。检查:
dotnet restore --verbosity diagnostic
dotnet list package --include-transitive再查看 obj/project.assets.json 的 targets、libraries 与日志中的 nearest framework 选择。若包只有 win-x64 资产,Linux 构建成功但运行时找不到本地库并不矛盾;测试矩阵必须覆盖执行阶段。
本地与 CI 的稳定契约
建议把仓库入口收敛为三段:
dotnet restore --locked-mode --configfile ./nuget.config
dotnet build --no-restore --configuration Release
dotnet test --no-build --configuration Release分离后,restore 失败由源、图、凭证或缓存 owner 接管,build 失败由编译与生成链排查,test 失败不再偷偷改变依赖。CI 证据应保存 SDK 信息、配置文件摘要、lock diff、目标 TFM/RID、restore 警告和测试结果,但必须脱敏源中的用户名、token、home 路径和内部包清单。
周期性增加空缓存作业,并关闭不必要公网出口。若日常作业只在热缓存成功,团队无法证明私有源灾备、凭证轮换和完整闭包仍可用。
常见失败:现象、判断、原因、修复
NU1101:找不到包
指定 ID 在源中不存在,或 source mapping 不允许访问实际源。
运行 dotnet nuget list source --format Detailed,检查有效配置、映射规则和包 ID 大小写/前缀。
仓库配置清除了需要的源、映射漏项、包尚未同步到代理,或用户配置曾掩盖问题。
修复与再验证:在批准源中补齐包或修正精确映射,执行带 --configfile 的空缓存 restore;不要临时追加公网源绕过。
NU1107 或降级冲突
两个依赖要求同一包的约束无法收敛,或出现 package downgrade。
查看 --include-transitive 输出和 assets 图,找出引入冲突的最短路径。
直接与传递版本冲突、中央版本覆盖不兼容、跨 TFM 条件不一致。
修复与再验证:优先升级引入方使约束兼容;必须直接固定时写明原因和退出条件。更新 lock 后跑所有目标框架测试。
403 或凭证反复提示
IDE 能恢复而 CLI 失败,或本机成功、CI 持续未授权。
确认源名、凭证优先级、credential provider 是否被当前客户端发现、token scope 与过期时间。
Visual Studio 与 dotnet 使用了不同 provider,上层环境变量藏着旧 secret,CI 处于非交互模式,或 token 无读取权限。
修复与再验证:更新正确身份来源,撤销旧 token;开发机用 --interactive 完成一次授权,CI 使用非交互短期凭证,再从干净身份 restore。
TLS、代理或超时
证书链不受信、连接被重置、429 或大量超时。
先验证网络和证书,再用 normal/diagnostic restore 看失败源和重试,不把所有错误归为 NuGet 服务不可用。
容器缺企业 CA、代理未传入进程、每源并发过高、代理限流或 DNS 分流错误。
修复与再验证:修复信任库和代理注入,必要时按容量调整每源请求并发;保留 TLS 验证,以空缓存恢复时间和错误率验收。
lock 不一致或“开发机没变化”
CI --locked-mode 失败,开发机普通 restore 却成功。
比较项目、中央版本文件、导入 props/targets、配置和 packages.lock.json。
依赖声明变化未提交 lock,开发机使用不同 SDK/TFM,或条件属性改变了图。
修复与再验证:在批准 SDK 和完整矩阵中重新生成并审查 lock;不能通过删除 lock 或关闭 locked mode 绕过一致性约束。
缓存损坏或 native 资产缺失
解压文件缺失、哈希异常、编译成功但运行时报本地库找不到。
区分 HTTP cache、global packages 和目标 RID,检查具体包目录与 assets 选择。
并发清缓存、下载中断、错误缓存键、包没有目标平台资产。
修复与再验证:只清理受影响层或包,重新 locked restore;在目标 RID 上执行真实测试。缺少资产时更换包或调整支持范围,而不是复制开发机二进制。
升级、迁移与回滚
升级一个包不是只改版本字符串。稳健流程是:
固定 SDK 与配置,记录旧图、审计和测试基线。在独立分支修改 PackageVersion 或 PackageReference。普通 restore 更新 lock,审查直接与传递变化。
对所有 TFM/RID 执行 build/test,并关注 analyzer、source generator 与 build assets。以 locked mode 在空缓存 CI 复验,再灰度合并。
从 packages.config 迁到 PackageReference 时,要识别 install scripts、content 变换、传统 packages 目录和旧项目类型的兼容差异。不能机械搬成 <PackageReference> 后就删除旧文件。
回滚必须同时恢复项目文件、Directory.Packages.props、lock、nuget.config 和必要的 SDK 基线。若新版本已经改变数据库、协议或部署制品,应用层回滚还要遵守对应系统的兼容策略;NuGet 只能恢复构建输入,不能逆转业务数据。
多源的便利会变成来源不确定
开发者习惯“多加几个源,总能找到包”,但 PackageReference restore 不以列表顺序提供稳定优先级。判断标准应是每个包 ID 只落到批准来源;做不到时使用 source mapping 或单一内部代理,而不是靠人的记忆辨认下载日志。
凭证轮换会被缓存掩盖
热缓存期间,过期 token 可能数天不被发现。团队应安排空缓存、短生命周期身份的定期恢复,并监控 401/403、恢复时延和源可用性。轮换验收要包含撤销旧凭证,而不只是新凭证可用。
锁文件不等于包内容托管
packages.lock.json 锁定解析结果,却不能保证上游永远可取,也不能替代来源保留、签名与灾备。关键系统需要内部代理或归档策略,并定期从灾备入口重建。成本取舍应基于 RTO、依赖数量、许可证和存储保留,而不是笼统追求“全部离线”。
中央版本会扩大一次变更的爆炸半径
CPM 让升级集中,也可能一次影响数百项目。变更系统必须能计算受影响项目,分批测试和回滚;一个根目录 props 通过 XML 校验,不能代表所有条件 TFM 都安全。
诊断日志本身可能敏感
diagnostic restore 会暴露源 URL、缓存路径、项目结构、内部包名和部分认证上下文。工单附件先脱敏,并控制留存和访问权限。不要为了排障让 CI 永久开启最高日志级别。
团队治理清单
仓库用 global.json 或等价规则声明批准的 .NET SDK 基线。restore、build、test 分离,后两步禁止隐式恢复。仓库 nuget.config 使用 <clear /> 收敛来源且不含 secret。
内部命名空间有明确 Package Source Mapping 或单一代理策略。CPM 变更能计算影响范围,并覆盖条件 TFM。应用与库分别定义 packages.lock.json 策略,CI 使用 locked mode。
缓存键包含 SDK、OS、TFM/RID 和依赖输入摘要。私有源凭证使用 provider 或短期 CI 身份,支持轮换和撤销。企业代理与 CA 在开发机、runner 和容器中分别验证。
签名、audit、许可证和传递依赖进入依赖评审。analyzer、source generator 与 build assets 按可执行供应链审查。空缓存和真实 TFM/RID 矩阵定期重建。
诊断日志脱敏,内部包名、路径和源地址按数据分级管理。升级可成组回退项目、中央版本、lock、配置和 SDK。
