
开头直接切入NuGet打包这事儿命令看着不多但实际用起来坑不少。很多.NET开发者第一次发包都会在“nuget打包常用命令”这类关键词里翻到一堆零散的指令贴过来一跑要么版本号对不上要么包打进私服后同事引用不了。这篇文章我就把打包这条链路从头到尾梳理一遍从命令背后的原理到工程文件该怎么配再到发布后怎么验证一次性讲透。先交代一下适用人群如果你在维护一个会被多个项目引用的类库或者要把公司内部的通用组件发布到私有源又或者准备把开源项目发到NuGet.org这篇文章值得你从头看完。如果你是第一次接触打包我保证你用最普通的命令行就能完成整个流程不依赖任何IDE图形界面。1. 打包的本质一个.nupkg文件里到底装了什么很多人在打包时会有一个误解以为打包就是把项目“发布”一下。实际上NuGet打包做的是另外一件事——把编译产物dll、pdb、xml等连同包的元数据名称、版本、依赖、作者、图标压缩成一个后缀为.nupkg的文件。这个文件本质上就是一个zip压缩包只是NuGet客户端能识别它的内部结构。我第一次带新人时最喜欢让他们做一个实验随便找一个.nupkg文件把后缀改成.zip然后解压里面大概长这样MyLib.nuspec lib/netstandard2.0/MyLib.dll lib/netstandard2.0/MyLib.pdb lib/netstandard2.0/MyLib.xmlMyLib.nuspec是一个XML清单文件记录了这个包的“身份证信息”——包名、版本号、作者、依赖项列表、文件清单。lib目录下则按目标框架Target Framework分门别类地放着编译产物。1.1 NuGet对包内目录的约定NuGet对.nupkg内部有几个约定俗成的目录搞清楚这些后面排查很多问题会容易得多。目录路径作用说明lib/{tfm}/编译时和运行时程序集最常见的目录tfm指目标框架如netstandard2.0、net6.0ref/{tfm}/仅编译期引用的程序集可以在不加载实现的情况下提供编译支持用得较少build/{tfm}/MSBuild的props/targets文件需要在编译期注入属性和目标时使用runtimes/{rid}/平台相关的原生运行库比如win-x64下的native dllcontentFiles/随包注入项目的内容文件较老的用法新项目不太推荐tools/安装包时执行的脚本或工具部分场景会用需要注意安全审查NuGet会把项目引用的包中lib目录下最匹配目标框架的dll取出来加入编译和运行时搜索路径。所以如果你发现引用了包但编译时找不到类型大概率就是包里的lib目录框架与你项目不匹配。1.2 dotnet pack和nuget pack两条打包路线怎么选这是命令行操作前必须先定的路线。简单说dotnet pack现代SDK风格项目即带Project SdkMicrosoft.NET.Sdk的csproj的首选跨平台无需额外安装构建和打包一体是当前的主流。nuget packNuGet.exe命令行工具提供的命令适用于老式非SDK风格项目或者当你需要直接操作.nuspec文件做精细控制时。NuGet.exe需要单独下载在Windows下使用体验最好。IDE里那个“打包”按钮底层触发的其实就是MSBuild的Pack目标这条链路和dotnet pack是同一条所以你在命令行里做的事情和IDE里做的没有本质区别只是命令行能加的参数更多、更容易自动化。2. 打包之前工程文件里必须做对的事命令本身不难难的是工程文件没配好。一个csproj里如果缺少必要的Package*属性打出来的包在NuGet页面上会很难看甚至缺依赖、缺图标、缺文档直接影响使用者体验。2.1 SDK风格项目里的核心打包属性以下这些属性是打包时最常用的建议在csproj的PropertyGroup里配好PropertyGroup PackageIdMyCompany.CoolLib/PackageId Version1.2.0/Version Authors张三/Authors Description一个用于演示NuGet打包流程的示例类库/Description PackageTagsutils;demo;nuget/PackageTags PackageProjectUrlhttps://github.com/yourname/coolib/PackageProjectUrl RepositoryUrlhttps://github.com/yourname/coolib.git/RepositoryUrl PackageLicenseExpressionMIT/PackageLicenseExpression PackageIconicon.png/PackageIcon PackageReadmeFileREADME.md/PackageReadmeFile /PropertyGroup这些属性会在打包时自动写入.nuspec文件。PackageId决定了包显示在NuGet站点上的名字不设置的话会取AssemblyName。Version就是包的版本号它和程序集的AssemblyVersion是两回事这一点特别容易踩坑我后面专门讲。2.2 版本号怎么在不翻车的情况下统一版本号是最容易埋雷的地方。很多项目在csproj里只设置了Version1.2.0/Version但程序集的AssemblyVersion可能还停留在1.0.0.0。这种做法短期内没毛病可一旦某个依赖了该程序集的强名称引用运行时就可能报FileLoadException或者“无法加载文件或程序集”的错误因为程序集的实际版本和引用版本对不上。我的建议是在仓库根目录放一个Directory.Build.props把版本号统一管理起来Project PropertyGroup Version1.2.0/Version AssemblyVersion1.2.0.0/AssemblyVersion FileVersion1.2.0.0/FileVersion /PropertyGroup /Project这样仓库内所有项目都会自动继承这个版本号不需要在每个csproj里重复写。SDK风格项目默认会根据Version生成AssemblyInfo但如果你遇到版本对不上的问题最直接的办法是手动指定AssemblyVersion再在输出dll上右键查看文件属性确认。注意Version、AssemblyVersion、FileVersion三者语义不同。Version决定NuGet包版本号AssemblyVersion是CLR加载程序集时校验的版本FileVersion是Windows文件属性里显示的版本。三者不要求一致但还是建议保持同步能少很多莫名其妙的问题。2.3 什么时候才需要手写nuspec现代SDK风格项目完全可以不写.nuspecdotnet pack会自动根据csproj内容生成。但有两种情况手写.nuspec更合适老式非SDK风格项目比如传统的packages.config项目csproj里没有SDK风格的那些打包属性此时用nuget pack xxx.csproj不如直接维护一个.nuspec文件清晰。需要精细控制包内文件比如想把多个项目的dll合并到一个包里或者需要在包根目录放一些特殊文件手动写.nuspec的控制力就体现出来了。一个简化版的.nuspec长这样?xml version1.0 encodingutf-8? package metadata idMyCompany.CoolLib/id version1.2.0/version authors张三/authors description一个示例包/description dependencies group targetFramework.NETStandard2.0 dependency idNewtonsoft.Json version13.0.1 / /group /dependencies /metadata files file srcbin/Release/netstandard2.0/MyCompany.CoolLib.dll targetlib/netstandard2.0 / /files /package如果项目不复杂我仍然推荐用SDK风格项目加dotnet pack省心得多。3. 常用打包命令逐个拆解先给一份我实际工作中用得最多的命令速查表然后逐个解释关键参数。表格前面有dotnet pack后面有nuget pack两条路线分开看。3.1 dotnet pack 常用参数清单命令示例作用dotnet pack MyLib.csproj -c Release -o ./artifacts以Release配置打包输出到artifacts目录dotnet pack -p:PackageVersion2.0.0临时指定包版本号不修改csprojdotnet pack --include-symbols --include-source生成包含源码和符号的包dotnet pack --no-build跳过编译直接基于上次build产物打包dotnet pack -v m输出最简日志适合在CI里看解释几个容易忽略的-o / --output指定输出目录。不指定的话默认输出到项目根目录下的bin/Release/里会在目录里混入一堆其他文件所以建议每次都指定。-p / --property可以临时覆盖MSBuild属性。典型场景是CI打包时想临时改版本号不动csproj文件。--no-build在已经编译过的情况下可以跳过编译直接打包能省几秒到几十秒。但如果代码改了却忘了重新编译打出来的就是旧产物我自己就吃过这个亏。--include-symbols生成符号包。如果不配合--symbol-package-format使用默认可能是.symbols.nupkg老格式推荐用-p:SymbolPackageFormatsnupkg生成.snupkg。3.2 nuget pack 常用参数清单命令示例作用nuget pack MyLib.csproj -Properties ConfigurationRelease -OutputDirectory ./artifacts指定配置和输出目录nuget pack MyLib.csproj -Version 2.0.0打包时直接指定版本号nuget pack MyLib.nuspec直接根据nuspec文件打包nuget pack MyLib.csproj -IncludeReferencedProjects把引用的项目也一起打进来nuget pack MyLib.csproj -Symbols -SymbolPackageFormat snupkg生成snupkg符号包nuget pack和dotnet pack最直观的差异是nuget pack更贴近“操作.nuspec文件”的思维它允许你直接在命令行指定版本号并且对老项目的兼容性更好。但如果你已经用SDK风格项目了我还是建议优先用dotnet pack不需要额外下载NuGet.exe。3.3 从csproj打包和从nuspec打包到底有什么区别从csproj打包时NuGet/MSBuild会“读取项目文件 → 生成nuspec → 执行打包”这一过程会动态收集依赖、文件、属性。从nuspec打包时命令行只认你写好的.nuspec文件不会帮你动态补全依赖和文件列表。这意味着如果你从nuspec打包但nuspec里的文件路径不对或者依赖版本写错那打出来的包就是错的而且没有任何警告。所以我的经验是能用csproj打包就不手写nuspec必须用nuspec时务必先在本地解压成品包检查一遍。3.4 一条常用命令组合本地一键打包并验证我一般在本地会反复敲这几条命令形成一个简单的“打包闭环”dotnet clean MyLib.csproj -c Release dotnet pack MyLib.csproj -c Release -o ./artifacts -p:PackageVersion1.2.0 unzip -l ./artifacts/MyLib.1.2.0.nupkg先清洁再打包最后看一眼nupkg里的文件列表。unzip -l在Windows也可以换成tar -tfWin10以上自带或者直接用NuGet Package Explorer打开查看。这一步能避免很多“以为自己打了包但内容不对”的情况。4. 我在实际打包中踩过的五个坑命令背得再熟不踩几个坑很难真正理解NuGet打包。下面这些是我和团队在实际使用中真实遇到的问题每一个都带症状、排查思路和解决办法。4.1 坑一AssemblyVersion和NuGet包版本对不上运行时加载异常症状包打好了也能引用了项目编译也通过但程序运行到某个方法时突然抛出System.IO.FileLoadException提示“无法加载文件或程序集……所检索到的程序集版本与所引用的程序集版本不匹配”。排查链路我当时第一反应是包损坏或引用错误但干净环境重装包后问题依旧。后来打开bin目录下的dll右键看属性再切到“详细信息”选项卡发现文件版本是1.0.0.0而我们引用的包版本是1.2.0CLR发现引用的程序集版本和实际加载的不一致直接拒绝加载。解决办法在Directory.Build.props里把三个版本号全部固定住然后用dotnet pack -p:PackageVersion1.2.0统一打包。这样NuGet包版本、程序集版本、文件版本保持一致CLR加载时就不会再挑刺。4.2 坑二包内混入垃圾文件导致体积异常大或加载冲突症状有次同事发我一个包只有几个类但nupkg体积有几十MB。解压一看里面除了dll还有大量*.pdb、*.xml之外的临时文件甚至包含测试工程生成的文件。排查链路这种情况多半是csproj里写了过于“贪婪”的Content或None包含规则比如ItemGroup Content Include**/* / /ItemGroup一旦这样写打包时所有匹配的文件都会被当成内容包进去。SDK风格项目默认打包的是编译输出不会把obj、bin下的中间产物打进去但如果你手动Include了乱七八糟的文件就会把垃圾也带进包里。解决办法检查csproj里的ItemGroup把不需要的内容排除掉或者给对应的文件加上PackfalseItemGroup Content Includetest/** Packfalse / None Include*.user Packfalse / /ItemGroup打包后在本地用unzip -l看文件列表确认只有需要的内容。4.3 坑三依赖项没有包含在包里用户一装就报缺少程序集症状包发布后同事在另一个项目里dotnet add package MyLib结果编译报错说什么类型找不到或者运行时提示缺少Newtonsoft.Json。排查链路NuGet包的依赖不是“把第三方dll嵌入你的包”而是通过nuspec里的dependencies节点告诉NuGet“我依赖谁”。SDK风格项目打包时会把.csproj里的PackageReference自动转换到nuspec的依赖组里。如果你解决依赖的方式是“直接引用dll文件”而不是“PackageReference”那这个依赖就不会出现在包信息里用户自然装不上。解决办法所有运行时依赖的第三方包全部改为PackageReference形式引用。如果只是编译期使用不需要传给使用方可以加上PrivateAssetsallPackageReference IncludeNewtonsoft.Json Version13.0.1 / PackageReference IncludeInternal.Tool Version1.0.0 PrivateAssetsall /打包后可以打开nupkg里的nuspec文件检查dependencies节点是否符合预期。4.4 坑四图标和README没有正确进包NuGet页面一片空白症状包推到NuGet.org之后页面上的图标不显示README也不显示看起来非常不专业。排查链路PackageIcon和PackageReadmeFile虽然设置了属性但如果图标文件和README文件本身没有被包含在包里NuGet.org就找不到文件来展示。SDK风格项目里这些文件默认不一定作为打包内容包含。解决办法在csproj里显式把它们标记为打包内容ItemGroup None Includeicon.png Packtrue PackagePath\ / None IncludeREADME.md Packtrue PackagePath\ / /ItemGroup同时记得设置PropertyGroup PackageIconicon.png/PackageIcon PackageReadmeFileREADME.md/PackageReadmeFile /PropertyGroup这样打包后icon.png和README.md会出现在包根目录NuGet.org才能正确读取。4.5 坑五push到私有源时被NuGet.Config里的其他源干扰症状公司内部搭了私有NuGet源但执行dotnet nuget push时提示401 Unauthorized或者莫名其妙把包传到了nuget.org。排查链路dotnet nuget push如果不带-s参数会读取NuGet.Config里配置的默认源。而很多机器上的NuGet.Config里配置了多个源甚至默认源是nuget.orgAPI Key不匹配就导致认证失败。解决办法推送时始终显式指定源地址和API Key不要依赖默认配置dotnet nuget push ./artifacts/MyLib.1.2.0.nupkg \ -k 你的APIKey \ -s https://api.nuget.org/v3/index.json推送私有源时同理把-s换成私有源地址。另外一个小习惯推送到私有源之前先确认同一个版本号没有被推过。NuGet源是不允许重复版本号的一旦推上去再想覆盖就得先删除或unlist旧版本这对使用者来说非常烦。5. 发布和验证包推上去之后必须做的一次“买家视角”检查打包只是第一步发布和验证才是确定这个包“能用”的最终关口。很多人打完包直接推到线上结果使用者装了一堆问题然后回来说“你的包是坏的”。与其这样不如自己在发布前多花两分钟做一次完整验证。5.1 推到NuGet.org的命令与前置条件推到NuGet.org需要先在网站注册账号然后在个人账户里创建API Key。推送命令dotnet nuget push ./artifacts/MyLib.1.2.0.nupkg \ -k 你的APIKey \ -s https://api.nuget.org/v3/index.json注意这里的源是https://api.nuget.org/v3/index.json这是NuGet服务的V3 API地址dotnet nuget push默认会走这个地址但显式写出来更保险。5.2 推到私有源或本地目录公司内部通常会用本地目录、局域网共享、或Artifactory等工具搭建私有源。最轻量的方式之一是本地目录源dotnet nuget push ./artifacts/MyLib.1.2.0.nupkg -s D:\nuget-feed然后在Visual Studio的包管理器中添加这个本地目录为包源就能像使用nuget.org一样安装这个包。这个方法特别适合跨团队快速共享内部组件不用搭复杂的服务端。5.3 发布后的验证清单新建一个干净的项目来测试安装这是最直接有效的验证方式dotnet new console -n TestPackage cd TestPackage dotnet add package MyLib --version 1.2.0 -s D:\nuget-feed dotnet run这一步能测出依赖是否完整、目标框架是否兼容、API是否能正常调用。检查包缓存NuGet在设计上会在本地缓存下载过的包。如果你改了包又用同一个版本号重新推本地可能还留着旧版缓存导致你验证时装的还是旧包。这是非常容易被忽视的问题遇到“明明改了却不生效”的怪事先清一下缓存dotnet nuget locals all --clear用NuGet Package Explorer查看如果是在Windows上推荐装一个NuGet Package Explorer它能图形化显示包内的文件、依赖、元数据检查起来非常直观。命令行党也可以用unzip -l或tar -tf查看包结构。6. 进阶把打包做进日常开发流当你不满足于“手动在命令行敲命令打包”就可以考虑把打包过程自动化并为使用者提供更好的调试体验。6.1 SourceLink和符号包让使用者在调试时能进入你的包源码如果你的包是开源的强烈建议启用SourceLink。这样使用者在调试时按F11就能直接跳进你的源码而不是对着反编译代码干瞪眼。启用方式是在csproj里添加PropertyGroup PublishRepositoryUrltrue/PublishRepositoryUrl EmbedUntrackedSourcestrue/EmbedUntrackedSources DebugTypeportable/DebugType /PropertyGroup ItemGroup PackageReference IncludeMicrosoft.SourceLink.GitHub Version8.0.0 PrivateAssetsall / /ItemGroup然后打包并推送符号包dotnet pack MyLib.csproj -c Release -o ./artifacts -p:SymbolPackageFormatsnupkg dotnet nuget push ./artifacts/MyLib.1.2.0.snupkg \ -k 你的APIKey \ -s https://api.nuget.org/v3/index.json注意符号包的版本号要和主包一致否则NuGet无法关联。6.2 多目标框架打包一个包兼容多个.NET版本当你维护的类库需要同时被.NET Framework 4.7.2、.NET 6、.NET 8项目引用时可以用多目标框架打包PropertyGroup TargetFrameworksnetstandard2.0;net6.0;net8.0/TargetFrameworks /PropertyGroup打包后lib目录下会生成lib/netstandard2.0/MyLib.dll lib/net6.0/MyLib.dll lib/net8.0/MyLib.dllNuGet在安装时会根据使用者项目的目标框架选择最合适的dll谁的版本与目标框架最接近就用谁的。这里的基本原则是使用者目标框架NuGet选择net8.0优先匹配net8.0其次net6.0再其次netstandard2.0net7.0优先匹配net6.0不匹配net8.0因为版本更高其次netstandard2.0.NET Framework 4.8优先匹配netstandard2.0多目标框架不是越多越好每多一个目标框架你就要多维护一份API兼容性测试。比较稳健的起步组合是netstandard2.0加当前主力版本比如net8.0。6.3 CI里自动打包把命令写进流水线手动打包适合本地验证正式发布建议交给CI/CD。这里以GitHub Actions为例核心就两步- name: Pack run: dotnet pack ./src/MyLib/MyLib.csproj -c Release -o artifacts -p:Version${GITHUB_REF_NAME} - name: Push run: dotnet nuget push artifacts/*.nupkg -k ${{ secrets.NUGET_API_KEY }} -s https://api.nuget.org/v3/index.json这里有几个细节值得注意版本号来自Git标签GITHUB_REF_NAME就是当前tag名比如v1.2.0但要注意NuGet版本号不允许v前缀最好在推送前做一次字符串处理。Secrets管理API Key一定不要硬编码在yml里用仓库的Secrets配置。幂等性同一tag重复跑流水线时第二次推送会失败因为包版本已存在。可以在Push步骤前加一个判断或者接受失败并视为“已发布”。个人经验就算没有完整CI也至少把dotnet pack和dotnet nuget push脚本化保存下来不要每次手工敲一堆长命令。最后再分享一个小技巧。我本地每次打包前习惯先跑一次dotnet nuget locals all --clear把自己的全局包缓存清干净确保后续验证安装时是从当前推送的源里拉的最新包而不是被本地缓存误导。很多“改了没生效”的诡异问题最后都发现是缓存惹的祸。如果你也在为NuGet包的“旧版本残留”头疼不妨先试试这个动作再回头排查命令和配置。