写在前面
上一篇讲的是 NuGet 的消费侧:包怎么装、怎么还原、packages.config 到 Central Package Management 演进了什么。这一篇换个方向,讲生产侧——你写了一个类库,怎么把它做成一个规范的 .nupkg,怎么定版本号,包里那些配置属性都是干什么的,最后怎么把它发布到 nuget.org,或者搭一个自己的私有 NuGet 服务器。
打包从来不只是"压缩一个文件夹"。 一个 NuGet 包同时是程序集的载体、依赖声明的清单、MSBuild 行为的载体和身份与许可的凭证。把这些面都配对了,使用者
dotnet add package一行命令就能正确地用上你的库;配错了,下游就要用 issue 和反射来还债。
本文所有命令与包结构均在 .NET SDK 10.0.403 下实际运行验证,工具生态与政策信息核对自 2026-11 的官方文档(nuget.org 的 API Key 政策在 2026 年下半年有过重要变更,网上不少教程已经过时,文中会专门标注)。
一、从类库到第一个 .nupkg
1.1 最短路径
打包的起点是一个普通的类库项目。除 5.1 节明确使用 PowerShell 外,多行命令采用 Bash 的 \ 续行语法;PowerShell 用户请合并成一行执行。<...> 是需要替换的占位符,不要连尖括号一起输入:
| |
.NET 10 SDK 生成的 Jiwei.DemoLib.csproj 只有三行有效配置:
| |
现在直接打包:
| |
命令成功,bin/Release/ 下出现 Jiwei.DemoLib.1.0.0.nupkg。注意两点:
- 什么元数据都没配,打包照样成功。包 ID 默认取程序集名(
PackageId默认值为$(AssemblyName)),版本默认1.0.0。NuGet 在本地打包时不强制 Description、Authors 之类的元数据——但它们会以占位值进入包里,发布到 nuget.org 之前应当补齐(见第二节)。 - .NET 8 SDK 起,
TargetFramework为net8.0及以上的项目dotnet pack默认就走Release配置(PackRelease属性默认true),本文仍显式传-c Release,让意图一目了然。
1.2 包里面到底装了什么
.nupkg 本质是一个改了扩展名的 zip 文件。把刚才的包解开看:
| |
[Content_Types].xml、_rels/、package/ 这三样是 OPC(Open Packaging Conventions)打包格式的固定部件——与 .docx 同源——平时不需要关心,官方文档也不逐项解释它们,知道"包 = zip + 约定目录 + 清单"即可。真正要理解的是 lib/ 和 .nuspec:
lib/{TFM}/是程序集的家。每个目标框架一个子目录,还原时 NuGet 按"与消费项目 TFM 最匹配"的原则挑一个。官方明确建议:永远不要把程序集直接放在lib/根目录。.nuspec是包的清单:ID、版本、作者、描述、依赖组都记在这里。上面这个包的清单长这样:
| |
这是补齐元数据后重新打包的结果(见 2.1),可以看到 <dependencies> 里按 TFM 分好了组——这个演示库没有任何包依赖,所以组是空的;一旦引用了别的包,每个 TFM 组里会自动出现对应的 <dependency> 条目。
1.3 csproj 属性与 .nuspec:写哪个
.nuspec 有两种生产方式:
- SDK-style 项目(现在的默认):不写
.nuspec,把一切写在 csproj 属性里。dotnet pack会在打包时自动生成.nuspec并塞进包内,官方文档原话是"If you are creating a package usingdotnet.exe packormsbuild pack target, we recommend that you include all the properties that are usually in the.nuspecfile in the project file instead"。 - 手写
.nuspec仍有两个场合:一是维护packages.config时代的非 SDK-style 老项目,用nuget.exe pack打包时以.nuspec为输入;二是 .NET 10 新增的能力——dotnet pack foo.nuspec可以直接吃一个.nuspec文件、不需要工程文件、不跑 MSBuild(还配了--version参数覆盖清单里的版本)。对全新项目,优先 csproj 属性。
二、包的元数据:csproj 里的配置属性
2.1 常用属性一览
下面这张表覆盖日常包元数据的九成场景(权威出处是 MSBuild pack targets 文档):
| 属性 | 作用 | 未设置时的默认 |
|---|---|---|
PackageId | 包 ID,全局唯一身份 | $(AssemblyName) |
Version | 包版本(SemVer 2.0.0) | 见第四节 |
Authors | 作者,分号分隔 | 占位值(文档两处描述不一致,建议显式设置) |
Title | 展示名(nuget.org 标题) | $(PackageId) |
Description | 描述,搜索与列表页的主要文字 | 占位文本 |
PackageTags | 标签,空格分隔,参与搜索 | 空 |
PackageProjectUrl | 项目主页 | 空 |
RepositoryUrl / RepositoryType | 源码仓库地址 / 类型(git) | 空 |
PackageReleaseNotes | 版本变更说明 | 空 |
Copyright | 版权声明 | 空 |
PackageRequireLicenseAcceptance | 安装前是否要求接受许可 | false |
DevelopmentDependency | 标记为开发期依赖,不流入下游 | false |
两个容易混淆的点:
Company不进包。nuspec 元数据里没有 company 元素,Company只影响程序集的AssemblyCompany特性(AssemblyInfo 一侧),别指望它出现在 nuget.org 页面上。Owners和Summary在 MSBuild 打包路线里不可用,它们只存在于手写.nuspec的世界。
2.2 图标、readme、许可证三件套
nuget.org 的包页面主要由这三样撑起门面。三个文件的共同套路:属性声明文件名 + None Include 把文件打进包根目录:
| |
各自的规则:
| 项 | 要求与版本门槛 | 弃用提示 |
|---|---|---|
图标 PackageIcon | JPEG/PNG,≤ 1 MB,推荐 128×128 分辨率(推荐值,非硬性) | 旧的 PackageIconUrl 已弃用;NuGet 5.3+ 只配 PackageIconUrl 时 pack 会报 NU5048 警告 |
自述 PackageReadmeFile | 仅 Markdown(.md);NuGet 5.10.0 preview 2 / .NET SDK 5.0.300 起支持 | 缺失时 NuGet 会输出"缺少自述文件"的建议提示(本文 1.1 节实测见到),nuget.org 页面没有 readme 区域 |
许可 PackageLicenseExpression | SPDX 表达式(如 MIT、BSD-2-Clause OR MIT),nuget.org 只接受 OSI/FSF 批准的许可证;非标准许可用 PackageLicenseFile 打包一个 .txt/.md 文件 | 旧的 PackageLicenseUrl 已弃用,会产生 NU5125 警告 |
许可证配置的三者(表达式 / 许可文件 / URL)同时只能设一个。license expression 打出来的包里,nuspec 会自动补一个指向 https://licenses.nuget.org/{表达式} 的 licenseUrl——1.2 节的实测清单里可以看到——这让旧客户端也能跳到许可证页面。
2.3 仓库信息、SourceLink 与确定性构建
RepositoryUrl/RepositoryType 之外,.NET 8 起 Source Link 发生了质的变化,值得单独说:
.NET 8 起,Source Link 内置于 .NET SDK 且默认启用(覆盖 GitHub、Azure Repos、GitLab、Bitbucket)。.NET 8+ 的项目不需要再引用
Microsoft.SourceLink.GitHub包、也不需要设置 SourceLink 相关属性;反而显式引用包会覆盖 SDK 内置版本。
Source Link 的产出是:PDB 里嵌入"源码 → 仓库 URL + commit"的映射,调试你的库时 VS 能自动按 commit 拉到对应源码。配套的两个属性:
PublishRepositoryUrl:Source Link 默认不把仓库 URL 写进包清单,设为true时随包发布(分支信息同理受其控制);ContinuousIntegrationBuild:在 CI 上设为true时启用官方构建专属行为(典型的是源码路径规范化为相对路径),与Deterministic配合产出确定性构建——官方库建议清单(library guidance)对此的建议级别是 CONSIDER。
顺手补一个与"包质量"直接相关的属性:
| |
它把 XML 文档注释生成为 .xml 并随 DLL 一起放进 lib/,使用者在 IDE 里能看到你的 IntelliSense 提示。代价是所有 public 成员缺注释都会冒 CS1591 警告,通常配 <NoWarn>$(NoWarn);CS1591</NoWarn> 或老老实实补注释。
三、依赖、多目标与包内高级布局
3.1 多目标:TargetFrameworks 与包内布局
把演示库改成双目标:
| |
重新打包,一个包里出现两套产物,nuspec 也按 TFM 分成两个依赖组(均为实测结果):
| |
| |
SDK-style 项目的好处就在这:pack target 知道布局约定,自动把程序集放进正确的 lib/ 子目录、按 TFM 生成依赖组。多目标实跑时踩到两个真实坑,都值得记下:
- netstandard2.0 目标默认用 C# 7.3,
<Nullable>enable</Nullable>直接报错"无效的 nullable 值"——要么<LangVersion>latest</LangVersion>,要么接受老语言版本; - 消费侧的坑:如果 nuget.config 用
<clear />只留私有源,构建 netstandard2.0 目标会因还原不到NETStandard.Library而失败(NU1101)——它本身不进包(SDK 隐式引用按开发期资产处理),但构建机需要能从某个源还原它。
另外两条布局纪律:声明在 dependencies group 里的 TFM 与 lib/、ref/ 目录里的 TFM 不完全匹配时,pack 报 NU5128 警告;TargetFramework(单数)存在时 TargetFrameworks(复数)被忽略——两者只能活一个。
3.2 依赖的资产控制:谁该进包,进包后给谁用
包的依赖声明不只是"要哪些包",还有"拿来干什么"。PackageReference 的三个资产属性在打包语境下最常见的用法:
| |
PrivateAssets="all":这个依赖不流入包——分析器、构建工具类引用的标准配置;ExcludeAssets="runtime":编译要用、运行时不要(少见,如仅编译期常量包);IncludeAssets:反向收窄,如IncludeAssets="analyzers"只要分析器资产。
与它配合的是包自己一侧的 DevelopmentDependency=true(见 2.1 表):它将包标记为开发期依赖,避免被列入其他包的依赖清单;在 PackageReference(NuGet 4.8+)下,还会排除该包的编译资产,不能把它用于需要向消费者暴露 lib/ API 的普通类库。分析器、源生成器、内部构建工具可按用途采用这个标记。
3.3 build/ 目录:把 MSBuild 行为装进包
lib/ 装程序集,build/ 装 MSBuild 文件。约定是三个目录:
| 目录 | 注入时机 | 适用 |
|---|---|---|
build/ | NuGet 2.5+,直接消费者 | 常规 props/targets |
buildMultiTargeting/ | 外层构建(多目标一次编译多个 TFM) | 需要感知"外层"的脚本 |
buildTransitive/ | NuGet 5.0+,沿依赖图传给所有下游 | 想让行为穿透传递依赖 |
目录里放 <包ID>.props 和 <包ID>.targets,也可以按 build/{TFM}/ 分框架。消费端 restore 后这些文件会被自动 import——packages.config 时代 .props 加在项目文件顶部、.targets 加在底部;PackageReference 项目则由 restore 生成的 *.nuget.g.props/.targets 引入,效果等价。官方文档同时给了两条红线:props/targets 里不要改 TargetFramework、PackageReference 这类会影响 restore 的属性/项;只有 build 文件而没有 lib//ref/ 产物的包,新工具链会提示 NU5127。
想看现实例子:Microsoft.NET.Test.Sdk 怎么把 VSTest 目标注入你的测试项目、coverlet.msbuild 怎么在测试运行后挂接覆盖率统计——把这两个包解开看它们的 build/ 目录便知。这也是"包不只是 DLL"最直观的证据。
3.4 analyzers/ 与 contentFiles/:两个约定目录
analyzers/dotnet/cs/:Roslyn 分析器与源生成器的家。路径约定是analyzers/{framework}/{language}/,dotnet是目前唯一有效的 framework 值,语言段为cs/vb/fs。放进去的 DLL 在安装时自动挂为分析器,Roslyn 官方 cookbook 明确说源生成器"用与分析器相同的方式打包"。分析器/生成器包的完整姿势:产物进analyzers/、DevelopmentDependency=true、不往lib/放东西。contentFiles/:想往消费项目里放源码/配置文件时用。ContentTargetFolders默认content;contentFiles,实际落位contentFiles/{language}/{TFM}/,条目上可配buildAction(默认 Compile)与copyToOutput(默认 false)。这是packages.config时代"content 文件 + 变换"的现代替代品,日常类库很少用到,知道入口即可。
原生库怎么打包(runtimes/{RID}/native/ 目录、DllImportResolver 按平台解析)本文不展开,见《C# 原生互操作与设备 SDK(八):C++ ABI 与跨平台封装》与官方 Native files in .NET packages 指南。
四、版本号规范:SemVer 2.0.0
4.1 三段语义
NuGet 采用 Semantic Versioning 2.0.0(NuGet 4.3.0+ / VS 2017 15.3+ 全面支持,更早客户端对 SemVer 2.0.0 特有版本不可见)。三段各自管什么,官方文档的建议直白到不用翻译:
| 段 | 何时递增 |
|---|---|
| MAJOR | 破坏性变更 |
| MINOR | 新增功能,向后兼容 |
| PATCH | 向后兼容的缺陷修复 |
-Suffix(可选) | 先行版标记 |
对库作者真正有分量的是第一行的连带责任:公共 API 一旦破坏,就必须升 MAJOR——哪怕你觉得"没人会用那个重载"。
4.2 先行版:构成规则与优先级
先行版跟在补丁号后面,- 加点分隔的标识符,如 1.0.0-beta.2。规范约束:标识符只能用 ASCII 字母数字和连字符、不得为空、纯数字标识符不得有前导零(beta.01 非法,beta.1 合法)。
比较先行版优先级的规则(SemVer 规范 §11,NuGet 遵循同一套):
- 纯数字标识符按数值比较,带字母的按 ASCII 字典序;
- 数字标识符优先级低于字母数字标识符;
- 前缀全部相同时,标识符段数多的优先级高;
- 任何先行版都低于对应的正式版。
规范的示例链一步到位:
| |
注意 beta.2 < beta.11 是数值比较(不是字典序的 “11” < “2”),这也是"beta.1 之后应该发 beta.2 还是 beta.10"这类争论的标准答案。还原侧的对应行为:默认不解析先行版——依赖图里没有任何包显式要求先行版时,NuGet 只考虑稳定版;dotnet add package --prerelease 或版本写 1.0.0-alpha* 才会把先行版拉进来。
4.3 构建元数据:可以带,但会被剥掉
SemVer 允许 1.0.0+git.abc123 这样的构建元数据,NuGet 的处理规则值得记两条:
- 比较与匹配时剥离:
1.0.7+r3456在 NuGet 眼里就是1.0.7。同版本推两次即使后缀不同,对源来说也是同一个版本(不可覆盖,见 5.5)。 - 文件名与 URL 使用规范化版本(剥掉
+后缀):打包产物是Jiwei.DemoLib.1.0.0.nupkg而不是1.0.0+meta.nupkg——这是 NuGet 的版本规范化规则,并非 URL 禁止使用+。
带点分隔先行版(1.0.0-alpha.1)或构建元数据的版本被官方称为"SemVer 2.0.0 特有版本":nuget.org 上的这类包对旧客户端不可见(需要 NuGet 4.3.0+ / .NET SDK 2.0.0+);自建 V2(OData)源默认会把 SemVer 2.0.0 特有版本整体过滤掉——生产环境遇到"包明明推上去了却搜不到",先查版本形态和源协议。
4.4 csproj 里的版本属性:Version / VersionPrefix / VersionSuffix
三个属性与 dotnet pack --version-suffix 的组合行为,官方给过一张明确的表:
| csproj 里设置了 | 打出的包版本 |
|---|---|
| 都不设 | 1.0.0 |
Version | $(Version) |
仅 VersionPrefix | $(VersionPrefix) |
仅 VersionSuffix | 1.0.0-$(VersionSuffix) |
VersionPrefix + VersionSuffix | $(VersionPrefix)-$(VersionSuffix) |
另有 PackageVersion 作为最终出口(默认取 Version,显式设置会盖掉 Prefix/Suffix 的拼装结果)。两个实践要点(均经 .NET 10 SDK 实测):
- 日常开发写
VersionPrefix,不要写死Version。官方文档原话:“If you want to use--version-suffix, specify VersionPrefix and not Version in the project file”——csproj 里已有Version时,--version-suffix会被忽略。 dotnet pack --version-suffix rc.1会覆盖 csproj 里的VersionSuffix:实测 csproj 配beta.1、命令行传rc.1,产物是1.0.0-rc.1.nupkg。命令行传入的是 MSBuild 全局属性,优先级高于项目文件内赋值——CI 里按构建号注入先行版后缀正是靠这一机制。
4.5 让版本跟着 Git 走:MinVer 与 Nerdbank.GitVersioning
手工维护三段版本容易忘,主流做法是让版本从 Git 信息推导。两个活跃的现成方案(均为 2026-11 核实的最新稳定版):
| 工具 | 最新稳定版 | 机制与定位 |
|---|---|---|
| MinVer | 8.0.0 | 极简主义:版本完全由 Git tag 决定,无配置文件;tag 之间的提交自动推导出先行版 |
| Nerdbank.GitVersioning | 3.10.94 | 一个 version.json 声明主版本,公共版本从 Git 高度(提交高度)生成;仓库在 dotnet 组织下、有 .NET Foundation 支持 |
两者的共同卖点:本地构建与 CI 构建产出一致的版本号,不用在 csproj 里来回改数字。选型上一句话:tag 驱动选 MinVer,版本文件驱动选 NB.GV。
4.6 先行版命名习惯
-alpha(功能未完成,实验性)、-beta(功能集完整、已知缺陷尚存)、-rc(候选发布,没有重大问题就转正)是被广泛认可的命名习惯——官方文档明确说明:技术上任何字符串都合法,NuGet 只按"有没有后缀"判定先行版、不做更多解释,alpha/beta/rc 只是"package developers generally follow recognized naming conventions"。放心用社区看得懂的名字,但别指望后缀本身携带机器语义。
五、发布到 NuGet 服务器
5.1 发布前:先在本地源把闭环走通
推到公网之前,把"打包 → 发布 → 消费"闭环在本地文件夹源上走一遍,是成本最低的验证。完整流程(Windows PowerShell + .NET 10 SDK;在新建的临时工作目录执行,独立于前面各节):
| |
三个实测细节:--source 直接给相对路径(./local-feed)时被拒(“指定的源无效”),换成绝对路径或配置好的源名即可;push 到空目录产生的是扁平布局(nupkg 直接落在根上,见 6.2 的布局讨论);闭环终点是 dotnet run 输出 Hello, NuGet!——验证"包能用"永远以消费端跑通为准,不以 push 成功为准。
再配两个体检命令:dotnet package search <包ID> --exact-match --source <源>(.NET 8.0.2xx SDK 起)列出包在某个源上的稳定版本(先行版另加 --prerelease;--exact-match 会忽略分页参数),验证"推上去没有";dotnet nuget why <项目或解决方案> <包ID>(.NET 8.0.4xx 起)解释依赖图里谁带进了这个包。
5.2 nuget.org:API Key 新政与 push
发布到 nuget.org 需要账号和 API Key。这里有时效性很强的新政:2026-08-17 起,新建 API Key 最长有效期从可选的一年收紧为 30 天;此前创建的旧 Key 已于 2026-11-01 全部过期。官方给的理由是供应链安全(缩短凭据泄露的窗口期),官方推荐迁移到 5.3 节的 Trusted Publishing。
Key 的创建路径:nuget.org 右上角用户名 → API Keys → Create。作用域用 glob 模式圈定包 ID(如 Jiwei.*),Key 只在创建时显示一次,过期前 10 天会收到提醒邮件。
推送命令的完整形态:
| |
常用参数:--skip-duplicate 批量推送时把 409(同版本已存在)降级为警告继续推后面的包;-t/--timeout 指定超时秒数(默认 300 秒);--no-symbols 不推符号包。CI 里从 NuGet 7.6(.NET SDK 10.0.300 起)可以不写 --api-key,改设 NUGET_API_KEY 环境变量。另有一个 .NET 10 新参数 --allow-insecure-connections:允许推到纯 HTTP 源——它存在的意义恰恰是提醒你默认不允许这么干(自建源请上 HTTPS,或至少在私有网络内)。
5.3 CI 发布:Trusted Publishing,而不是存一个长期 Key
GitHub Actions 场景下,当前官方推荐 Trusted Publishing(2025-09 上线):工作流用 OIDC 向 nuget.org 换一个有效期 1 小时的临时 API Key,仓库里不落任何长期凭据。要点:
| |
nuget.org 侧要在账户的 Trusted Publishing 页登记仓库与 workflow 名。API Key 路线(5.2 节)在 CI 上依然可用——配上 NUGET_API_KEY 环境变量 + 30 天轮换——但官方态度明确:新项目优先 Trusted Publishing。
5.4 符号包:snupkg 与嵌入式 PDB
让使用者能调试进你的库,有两条路:
路线一:符号包(snupkg)。 打包时加两个属性:
| |
产物多出一个 Jiwei.DemoLib.1.0.0-beta.1.snupkg——解包实测里面只有 .pdb(加 nuspec 和 OPC 部件)。注意 SymbolPackageFormat 不设时默认是遗留格式 symbols.nupkg(整包带符号),现在应当显式指定 snupkg;dotnet pack --include-symbols 只是 IncludeSymbols 的命令行形式,同样需要配格式属性。推送时:nuget.exe 的语义是同目录下主包与 snupkg 一起推(先主包后符号包);dotnet CLI 的 -n/--no-symbols 定义反向印证了默认行为——符号包在场就随主包推送,-ss/--symbol-source、-sk/--symbol-api-key 可指定独立符号服务器。nuget.org 的符号服务器端点是 https://symbols.nuget.org/download/symbols,消费者把它加进 VS 的符号源即可;只接受 portable PDB,Windows PDB 与原生项目符号不收。
路线二:PDB 嵌入程序集。 <DebugType>embedded</DebugType> 把 PDB 直接塞进 DLL,使用者无需配置符号源。代价是二进制变大——不管使用者是否需要调试你的源码。Source Link 官方对发布到 nuget.org 的库的建议是路线一:同时发布 snupkg。
5.5 发布之后:校验、签名与不可变性
push 成功不是终点。nuget.org 的处理流水线:校验 + 索引通常 15 分钟内完成(超 1 小时未上架再联系支持),期间包出现在账户的 Unlisted Packages 里,完成后收到邮件。所有上传到 nuget.org 的包会被自动仓库签名(repository signature)——这也是不可变性的来源:
- 同 ID + 同版本永不可覆盖:重复 push 得到 409;
- 包内容(含 nuspec)一旦签名即不可变,改一个字都不行;
- 不支持永久删除:现行政策下作者只能 unlist(从搜索与列表消失,精确版本仍可还原,保住下游构建)或 deprecate(标记弃用并推荐替代)。“发布后 72 小时内可删"是早已过时的说法,别信旧教程。
所以每次 push 前值得自问:这个包推上去就是 forever 的。顺带的硬性上限:单包 ≤ 250 MB。
六、搭建私有 NuGet 服务器
6.1 为什么需要私有源
四类典型诉求:内部分发(公司公共库不外发)、离线/受控环境(产线、内网无法直连 nuget.org)、缓存加速(一次下载全员复用)、供应链治理(只允许经过审查的包版本进入构建)。2026 年的治理趋势也体现在工具上:dotnet nuget add source 文档开篇就提醒"Use package sources that you trust”,背后就是依赖混淆(dependency confusion——公开源上有人注册了与你私有包同名的 ID,配合 <clear /> 用错源配置就会被钓鱼)这类风险;NuGet 的 Package Source Mapping(按包 ID 前缀固定源)和 .NET SDK 11(11.0.100)起 dotnet nuget update source 新增的 --min-publish-age-hours(自动选版时排除尚未"满龄"的版本,防"刚发布的投毒版本")都是为此服务的。本文主线是"搭一个能用的私有源",治理深潜留作以后。
6.2 第一档:本地文件夹源与网络共享
零依赖方案:一个目录就是源。push 侧是文件复制(NuGet 识别出本地/UNC 路径就直接复制,不走 HTTP);消费侧把目录配成源即可。它有两种布局:
| 布局 | 形态 | 怎么产生 |
|---|---|---|
| 扁平 | 所有 nupkg 堆在根目录 | dotnet nuget push 推空目录(本文实测)、直接拷贝 |
| 分层 | <包ID>/<版本>/*.nupkg | nuget add / nuget init(nuget.exe 专属命令) |
官方对规模的提示很直接:旧版 NuGet 只支持单文件夹布局,其性能"远低于分层结构"。当前语义下 dotnet nuget push 会尊重源里已有的布局——源里已经有分层结构就沿用分层;空源第一次 push 落成扁平。想要分层起步,用 nuget.exe 的 nuget init 初始化,或从全局包缓存(%userprofile%\.nuget\packages,天然分层)拷一个包目录进去。目录可以放在网络共享(UNC 路径直接当源用),这就是最简"团队私有源"。
6.3 第二档:BaGetter——一个容器跑起来的 V3 服务器
要 HTTP 服务、搜索、网页界面、符号与镜像,最轻的活跃方案是 BaGetter。先交代沿革:曾经最流行的轻量服务器 BaGet(loic-sharma/BaGet)自 2024 年中起实质停滞(仓库未归档但无维护动向);社区续作 BaGetter(bagetter/BaGetter)从其 fork 而来持续演进,微软官方文档的托管清单里也已单列 BaGetter。另一些老教程的主角 NuGet.Server 见 6.4 末尾。
Docker 一行起步(官方快速开始命令):
| |
镜像 bagetter/bagetter,容器内端口 8080(示例映射宿主 5000;快速起步用 latest,生产部署建议固定到具体版本号)。常用配置走环境变量(__ 是层级分隔符):
| |
上述 HTTP 示例仅用于本机验证,生产部署应配置 HTTPS;Compose 的卷同时保存数据库与包文件,避免重建容器后丢失。V3 端点是标准的 http://localhost:5000/v3/index.json,所以 dotnet CLI、Visual Studio、MSBuild 全部即插即用。推送与消费:
| |
几个值得知道的能力边界(官方文档核实):支持 snupkg 符号服务;Mirror 开启后兼任 nuget.org 缓存代理(构建加速 + 离线场景);默认包大小上限 8 GiB;删除行为默认 unlist,可配 PackageDeletionBehavior: HardDelete 真删——私有源自建源没有 nuget.org 的不可变性教条,这是它"可控"的一面。
6.4 第三档:重型自托管与托管服务
BaGetter 之上还有一整个光谱。2026-11 时点的关键事实(选型时最容易踩的过时认知已加粗):
| 方案 | NuGet 能力 | 2026-11 现状要点 |
|---|---|---|
| ProGet(Inedo) | V3、符号、连接器代理上游 | Free 版可用且允许商用:不限用户不限 feed;但无 feed 级安全、无 AD/LDAP、无个人 API Key;付费版 $2,395/年起 |
| Nexus Repository(Sonatype) | V2 + V3,proxy/hosted/group | 免费版(2025-02 起从 OSS 更名 Community Edition)即含 NuGet v2/v3;符号服务器是 3.95.0(2026 年)新增 |
| Artifactory(JFrog) | V3、符号(7.36+) | 免费线(JCR/OSS/CE)都不含 NuGet——OSS 只管 Maven/Gradle 系,NuGet 属商业版能力 |
| Azure Artifacts | V3、上游源 | 计费锚点是存储:每组织 2 GiB 免费,超出约 $2/GiB(“前 5 用户免费"是 DevOps Basic 用户许可的话术,不是 Artifacts 本体) |
| GitHub Packages | NuGet registry | 公开包免费;GitHub Free 私有包 500 MB 存储 / 1 GB 月流量;个人访问令牌认证只支持 classic PAT(发布用 write:packages;Actions 另可用 GITHUB_TOKEN) |
| GitLab / Gitea 内置 registry | V2+V3 | GitLab 12.8 引入(13.3 进 Free,14.1 支持符号);Gitea 1.17 起支持(V2+V3+符号) |
| Sleet(emgarten) | 静态 V3 feed | 无服务器进程:sleet push 写包 + 对象存储(S3/Azure Blob/MinIO)直接提供索引;活跃维护(当前 7.2.0) |
NuGet.Server 的谢幕值得单独一句:这个 .NET Foundation 时代的 ASP.NET/IIS 方案只支持 V2 协议,最终版本停在 3.4.2(2022);其 GitHub 仓库已于 2026-05-29 归档为只读——虽然微软文档页尚未加注弃用标记,但新项目没有任何理由再选它。
6.5 私有源的认证与配置管理
私有源有了,团队怎么用得整齐、用得安全:
配置的层级与归属。 nuget.config 分三级:机器级、用户级、解决方案/项目级(从盘根逐级下探),离当前目录越近优先级越高;<clear /> 掐断继承。官方最佳实践是把 nuget.config 放解决方案仓库根目录、随代码提交——源的清单、Package Source Mapping 规则、 <clear /> 一起进版本控制,每人每台 CI 拿到的都是同一套源配置。
凭据的存放。 dotnet nuget add source --username --password 存凭据时,加密只在 Windows 上可用、且只能在加密时的同一机器同一用户下解密——Linux/macOS 与 CI 上没有加密选项,要么 --store-password-in-clear-text,要么用环境变量宏,要么上凭据提供程序(authenticated feeds 文档给的安全性推荐顺序:credential provider > 加密 > 环境变量 > 明文)。GitHub Packages 是典型例子,官方命令直接含明文开关:
| |
在 GitHub Actions 里则优先用自带的 GITHUB_TOKEN(配 write:packages 权限),连 PAT 都不用发。源配置的日常维护用 dotnet nuget list source / update source / remove source(三者都支持 --configfile 指定目标文件)。
七、避坑与决策清单
全文踩过、见过的高频坑,收拢成一张表:
| 坑 | 症状 | 解法 |
|---|---|---|
--version-suffix 不生效 | 包版本没有后缀 | csproj 里改用 VersionPrefix,删掉 Version(有 Version 时后缀被忽略) |
| netstandard2.0 目标构建失败 | “无效的 nullable 值:C# 7.3” | 该 TFM 默认语言版本老,显式 <LangVersion>latest</LangVersion> |
私有源 <clear /> 后还原失败 | NU1101 找不到 NETStandard.Library | 构建期依赖也需要源:保留 nuget.org 或自建镜像/上游 |
dotnet nuget push 本地源被拒 | “指定的源无效” | 相对路径在部分 shell 下不识别,用绝对路径或配置源名 |
只配了 PackageIconUrl/PackageLicenseUrl | NU5048 / NU5125 警告 | 换 PackageIcon + 文件打包 / license expression 或 file |
--include-symbols 打出老式符号包 | 产出 .symbols.nupkg 而非 .snupkg | 再加 -p:SymbolPackageFormat=snupkg |
| SemVer 2.0.0 特有版本"消失” | 点分隔先行版/带 + 的包在源上搜不到 | 旧客户端或 V2 源默认过滤;客户端升 4.3+/SDK 2.0+,源用 V3 |
| 同版本想覆盖重发 | 409 冲突 | nuget.org 不可覆盖不可删,只能升版本;自建源可配 HardDelete |
发布路径的决策树:
- 个人开源库 → nuget.org + Trusted Publishing(CI 零长期凭据)+ snupkg;
- 公司内部分发,起步阶段 → BaGetter 单容器(半天上线),镜像 nuget.org 顺手解决缓存;
- 公司规模化(权限、审计、多制品类型) → ProGet 或 Nexus(预算有限选前者 Free 版起步)、Azure Artifacts(已在 Azure 生态);别指望 Artifactory 免费版;
- 离线/强管控环境 → 文件夹源或 Sleet 静态 feed + 镜像同步,配合 Package Source Mapping 与最小发布年龄策略。
八、结语
从 dotnet new classlib 到一个别人 dotnet add package 就能用的包,中间的每一步——元数据三件套、SemVer 版本纪律、依赖与资产控制、发布闭环验证——都是在替下游的使用者省时间。而"发布"这个动作本身在 2026 年的两极化也很有意思:nuget.org 一侧在收紧(30 天 API Key、不可删除、签名不可变),私有源一侧在 democratize(一个容器甚至一个目录就是一个源)。工具一直在变,这篇文章里的大部分命令细节都会有保质期,但那两条主线不会变:对下游负责的包才值得发布,验证过的发布才算发布。
参考资料
打包与元数据
- MSBuild pack targets 参考(包元数据属性权威表)
- .nuspec 参考(nuspec 元素与依赖组约定)
- dotnet pack 命令参考(VersionPrefix/Suffix 组合表、.NET 10 直接打包 nuspec)
- 支持多目标框架打包
- 包内 MSBuild props 与 targets(build/ 目录约定)
- 分析器打包约定(analyzers/ 目录)
- Roslyn 源生成器 cookbook(源生成器打包)
版本规范
- Semantic Versioning 2.0.0 规范
- NuGet 包版本概念(SemVer 2.0.0 支持范围与规范化)
- 先行版包(命名习惯与还原行为)
- MinVer / Nerdbank.GitVersioning
符号与 SourceLink
发布与 nuget.org
- dotnet nuget push 命令参考
- 发布到 nuget.org(API Key、校验时长、上传要求)
- Scoped API Keys(作用域与过期策略)
- Trusted Publishing(OIDC 临时凭据工作流)
- nuget.org 删除政策(unlist 与不可变性)
- NuGet 供应链安全与 API Key 时长收紧公告
私有源
- 自托管 NuGet 源总览
- 本地文件夹源(布局与性能)
- BaGetter 文档(Docker、配置、镜像)
- ProGet 版本与许可功能对比 / Inedo 定价
- Nexus Repository NuGet 文档(V2/V3 与符号服务器)
- Artifactory 自托管版本功能矩阵(免费线不含 NuGet)
- Azure Artifacts 入门(2 GiB/组织免费额度) / Azure DevOps 定价
- GitHub Packages 的 NuGet registry / Packages 计费
- Sleet:静态 NuGet feed 生成器
- NuGet 配置文件行为(层级、凭据加密边界) / 带认证源的凭据最佳实践