在 Node.js 生态里待久了的人,很难不羡慕那个npx:想跑个脚手架,npx create-vite一行搞定;想临时用个工具,npx一下就跑完,用完还不污染全局环境。Python 那边这两年也有了uvx,把“按需执行工具”玩得明明白白。而 .NET 生态里,我们这么多年干得最多的还是dotnet tool install -g,装完要卸载还得专门记一下。直到 .NET 10 把新一代的dnx带出来,.NET 终于也走到了“npx/uvx 时代”这一天。
这篇文章我会按自己的实际折腾经历来写,把这个 dnx 是什么、为什么说它对标 npx/uvx、日常哪些场景最值得用、以及我在预览版里踩过哪些坑,一次说清楚。不管你是刚接触 .NET 的新人,还是已经写了很久的老开发,只要对命令行工具有点兴趣,这篇文章应该都能给你点有用的东西。
1. .NET 的 “npx 时刻” 为何来得这么晚
1.1 从 npm 的 npx 到 Python 的 uvx,按需执行到底解决了什么问题
先聊一个底层逻辑。npm 的npx出现之前,前端跑一个临时工具基本是两条路:要么全局安装,然后手动找命令;要么npm install到当前项目,再调node_modules/.bin里的可执行文件。全局装的问题很明显,你电脑上装了一堆全局包,时间一长连自己都忘了哪些还在用,升级某个包还可能牵连别的项目。装到项目里又显得重,为了跑一次临时命令改了一堆配置文件,完事还得清理。
npx解决的思路其实很直白:你在命令行输入一条命令,它会自动去本地项目依赖里找,找不到就去 registry 下载到缓存,然后直接执行,执行完这个“临时工具”就躺在缓存里,不污染当前的package.json,也不会给全局环境留下杂七杂八的依赖。Python 的uvx做的事本质上也一样,只不过底层是 PyPI 和 wheel 包那一套。你会发现这两者的共同点是:把“使用工具”这个动作变成了一次性的、无副作用的操作。
.NET 这边以前缺的就是这个体验。dotnet tool虽然也能装工具,但更多的是一种“持久化”的思路,装完就常驻,适合那种天天要用的 CLI,并不适合“我就想跑这一次”的临时场景。所以当我有一次在终端里敲dotnet tool install -g dotnet-ef装完又因为版本冲突折腾了半天的时候,脑子里冒出的想法就是:真的需要一个 .NET 版的 npx。
1.2 dotnet tool 的老毛病:全局污染、版本冲突、命令找不到
我不否认dotnet tool本身的设计,它把 .NET 程序打包成可执行命令这条路是通的,NuGet 上也有大量优秀的工具包靠它分发。但用久了你会发现几个比较磨人的问题。
一是全局工具装多了之后,版本管理全靠自觉。项目 A 需要某个工具的 3.x,项目 B 要的是 2.x,如果你用-g装全局,冲突几乎是注定的。虽然官方有dotnet tool install配合本地 manifest 的方案,也就是在项目里生成一个dotnet-tools.json,然后dotnet tool restore按项目恢复工具,但这个机制始终有点“重”——你得先手动改清单、提交文件,然后还要求团队的人都理解这套流程,否则别人 clone 项目下来根本不知道还要跑一次 restore。
二是“命令找不到”这个经典报错。Windows 上尤其常见,你明明装好了全局工具,新开一个终端却提示不是内部或外部命令。老手知道要去配环境变量、刷 PATH,新手直接卡死在这一步。究其原因,还是工具的发现路径太依赖 shell 的 PATH 机制,装的时候装到某个目录,跑的时候却不一定能在PATH里找到。
三是临时性工具缺乏“用完即走”的体验。你想用某个工具跑一遍批量改格式、生成一次报告、验证一下数据,这种场景下为了它专门装一个全局工具,后续还得考虑升级和卸载,心理负担不小。以前社区里有人写了一些近似 npx 的包装脚本,但始终没有官方一层站出来说“这个事我帮你做了”。
1.3 dnx 到底是个什么东西(我理解的定位)
.NET 10 里出现的这个dnx,从定位上看就是冲着补上这个缺口去的。拿 Node 生态类比的话,dnx就是 .NET 世界的npx;拿 Python 生态类比,它就是uvx的 .NET 版。它允许你通过一条命令直接运行某个 NuGet 包提供的工具,而不用像以前那样先执行dotnet tool install安装,用完了还要手动卸载。
这里要特别提一句,dnx这个名字在 .NET 早期其实出现过,当时是 “.NET Execution Environment” 的缩写,属于 ASP.NET 5 时代那套运行时概念,后来被统一的dotnetCLI 取代了。现在 .NET 10 把这个名字重新捡起来,但含义已经完全变了,你别把两者搞混。新的dnx更像是 “dotnet next execution” 的产物,解决的问题就是“我想用这个工具,但我不想先经历安装、配置、维护一系列流程”。
我理解它的核心价值有两点。第一,降低临时工具的使用门槛,让 .NET 生态的命令行体验向 npx/uvx 看齐;第二,为多项目、多版本的场景提供一个更干净的隔离方案,工具在缓存里按需解析版本,不会污染全局环境。你在终端里敲一条dnx <某个工具>,它会自己判断这个工具在当前上下文里存不存在,不存在就拉取到本地缓存并执行,存在就直接跑,整个过程对使用者来说非常顺滑。
2. 五分钟上手 dnx:环境、安装与第一条命令
2.1 装好 .NET 10 SDK 并验证 dnx
先交代一下我写这篇文章时的环境:我是在 .NET 10 预览阶段开始折腾的,后面也持续跟着预览版本更新,以下操作在 Windows 11 和 Ubuntu 22.04 上都跑过一遍。dnx 并不是一个独立安装的工具,它是随 .NET 10 SDK 一起发布的,所以前提条件是你机器上得有 .NET 10 SDK。
如果你是直接从官网下载的 .NET 10 SDK 安装包,装完之后理论上就自带 dnx 了。可以通过下面的命令确认版本:
dnx --version如果提示找不到命令,先确认一下 .NET 10 SDK 是否真的装好了:
dotnet --list-sdks输出里应该能看到类似10.0.100或更新版本号的 SDK。如果dotnet有但dnx没有,常见原因是你装的 SDK 版本太早,或者同时装了多个 SDK 导致命令解析到了旧版本上。这时候可以把 .NET 10 SDK 的安装目录里dnx可执行文件所在路径加到环境变量,或者干脆重新安装最新版 SDK。
提示:预览版阶段各个版本之间命令行行为可能有细微差异,我下面的示例都以我当时用的版本为准,你实际使用时如果发现输出略有不同,以官方文档为准。这不算稀罕事,任何一个大版本的新 CLI 工具都会经历这种调整期。
2.2 第一次运行:本地项目工具、远程包、指定版本、临时命令
我拿一个最小例子来说明 dnx 的用法,让你感受一下它和 dotnet tool 的区别。
假设你有一个项目,临时想用某个格式化工具扫描一下代码。以前的操作是这样:
dotnet tool install -g dotnet-format dotnet format现在用 dnx,你不需要先安装全局工具,直接跑包名就行,大致是这个姿势:
dnx dotnet-format更贴近 npx 的另一种写法是带上要运行的命令名。比如 NuGet 上有某个包叫MyCompany.CliTools,它给外部提供的命令叫mytool,在 npx 生态里你直接写npx mytool是因为 npm 做了命令名到包名的映射。dnx 在解析时会先看这个东西是不是一个已经存在的命令,如果匹配不到,它会进一步尝试把它当作包名去 NuGet 上解析,逻辑上是通的。
如果只想用某个特定版本的工具,可以在包名后用@或类似的方式指定版本:
dnx dotnet-format@8.0.0这个用法我在预览版里试过多次,它能很好地解决“两个项目需要不同大版本工具”的冲突:dnx 会按版本把工具缓存到独立目录,互不干扰,你不需要装一个卸一个,也比全局装两个版本再靠 PATH 顺序去猜方便得多。
另外,dnx 也支持运行已经存在于本地项目里的工具。这个特性用来替代dotnet tool run我觉得会更自然。你项目里如果配置了本地工具清单,以前是用dotnet tool restore先把工具恢复出来,再通过dotnet tool run <命令>执行,现在 dnx 会自己去检查这些工具是否可用,不可用就自动拉取,可用就直接执行,少了一手恢复的步骤。
2.3 dnx 执行机制简单拆解(它和 npx 一样聪明在哪)
第一次跑通 dnx 之后,我特意留意了一下它的执行过程,因为这类工具很容易做成一个“黑箱”,出了问题都不知道去哪查。dnx 的工作流程大致可以分成三步。
第一步是“找”。dnx 会在当前目录往上逐级查找项目文件,检查项目里有没有本地工具清单,同时检查它自己的全局缓存里有没有匹配版本的可执行文件。如果找到了就直接进入执行阶段。
第二步是“拉”。找不到时,它会去你配置的 NuGet 源拉取对应的包。这里我理解的是它会把 NuGet 包下载下来并解压到一个独立的缓存目录,而不是直接往全局目录塞。这个设计和 npm 的 cache 以及 uv 的 cache 很相似,好处是同一个包的不同版本能和谐共存,而且下载过一次之后,下次执行直接走缓存,速度会快不少。
第三步是“跑”。工具被还原成可执行文件后,dnx 会把当前命令行上下文里的参数透传给它,然后启动进程。对使用者来说,你敲的是一条命令,实际上 dnx 在背后帮你完成了发现、恢复、执行三个步骤。
这套机制最聪明的地方在于它把“按需解析”这件事自动化了。npx 之所以好用,是因为它把“临时跑一次”的摩擦降到了最低,dnx 现在做的也是同一件事。我实际体感是,以前遇到“这个工具我只是想试一下”的场景,现在都更愿意直接用 dnx 跑,而不是折腾 global install。
3. 三个真实场景:dnx 怎么帮你干活
3.1 场景一:一条命令处理 10 万行的 CSV 数据
先来个实际一点的。热词里有人提到 “csv net 10万数据”,我猜是想用 .NET 处理大 CSV 文件。如果是我,以前最顺手的方案是建一个临时控制台项目,写一堆 CSV 解析代码,跑完再删项目,非常折腾。有了 dnx 之后,你可以想想自己想用什么现成的工具包,然后直接用 dnx 跑起来,甚至跑完就忘掉它的存在,因为它本来就是个临时工具。
比如你想对一个 10 万行的 CSV 做简单统计、过滤列、转换格式,如果有个包叫CsvMagic.Cli提供了csvmagic命令,你可以这样:
dnx csvmagic --input bigdata.csv --operation statsdnx 会帮你把CsvMagic.Cli这个 NuGet 包拉到缓存并执行,不会再要求你先创建一个项目再引入依赖。你不关心它装在哪、升级不升级,你需要的就是“处理完这份数据”这个结果。
如果你想对处理过程有更多控制,比如想用脚本语言的方式写一段 C# 来跑,那么可以考虑用 .NET 10 里对 C# 脚本执行的支持。这块和 dnx 正好是互补关系:dnx 负责“按需提供工具”,C# 脚本负责“随便写点逻辑就跑”。对于 CSV、JSON 这类常见数据处理,组合起来的体验已经非常接近 Python 里那种“写两行代码直接跑”的轻盈感。
有一点我要特别提醒一下:处理 10 万条这种量级数据时,别一上来就在脚本里用很重的 ORM 或者反射式 CSV 库。10 万行其实很小,但如果你选择了一个把整个文件都读进内存再映射成对象的库,内存占用很容易吓你一跳。更好的做法是用横向流式读取,逐行解析,能用索引定位别用 LINQ 全表扫描。dnx 跑工具不会帮你解决算法问题,工具好不好用还得看包本身的实现。
3.2 场景二:临时调试 SMTP 邮件发送
热词里还有一条和邮件相关:“.net 10 发送邮件 ssl mailbox name not allowed. the server response was: auth”。这个报错场景我很熟,属于 SMTP 调试里比较典型的问题。简单说,这个错误一般不是你代码逻辑的问题,而是 SMTP 服务器要求:发送邮件的“发件人邮箱地址”必须和认证使用的账号完全一致。你用 A 账号做 SMTP 认证,但MailMessage.From写的是 B 邮箱,服务器就会甩出类似 “mailbox name not allowed” 的响应。
以前遇到这种报错,我得写一个小控制台程序,引用 MailKit 或者 System.Net.Mail,把账号、授权码、SSL 模式都配置一遍,才能复现和调试。现在有了 dnx,我可以直接把一个现成的邮件调试 CLI 工具拉下来跑。比如假设有一个SmtpProbe.Cli包,我就可以这样:
dnx smtpprobe --host smtp.example.com --port 465 --ssl --user test@example.com --password **** --from test@example.com --to receiver@example.com跑完一看输出,如果还是报mailbox name not allowed,那就直接去检查发件人地址和认证账号是不是同一个;如果报证书错误,那就要看 SSL 端口和证书链的问题。这种方式的优势在于,你不必为了定位一个 SMTP 配置问题专门创建一个临时项目,也不必反复改代码、编译、运行,一条 dnx 命令就把所有变量都摆在命令行参数上,排查效率高了不少。
再补一点和参数有关的内容。碰到 SMTP 这类明文密码场景,我建议别直接写在命令行里,历史记录会留下痕迹。可以临时设一个环境变量再引用,比如:
$env:SMTP_PASS='你的授权码' dnx smtpprobe --password (Get-Item Env:SMTP_PASS).Value类似地,如果你是在 CI 里跑这种探测命令,更要把密码放到 secret 环境变量里,而不是写进 yaml 文件明文。
3.3 场景三:CI 流水线里按需跑代码分析/格式化工具
第三个场景我觉得比前两个更贴近团队协作,就是 CI 里的工具使用。以前在 CI 流水线里想跑代码分析、格式检查,你得先确保 runner 上装了对应工具,或者流水线里专门写一条dotnet tool install/dotnet tool restore的步骤。前者麻烦在 runner 环境要维护,后者麻烦在每个项目都要维护工具清单文件,还得处理 restore 环节因为源不稳定导致的失败。
用 dnx 之后,流水线里可以直接把工具执行写在命令里,工具本身随命令按需解析。比如:
dnx dotnet-format --verify-no-changes如果重新构建一个干净的 runner,无需预先准备任何工具环境,只要装上 .NET 10 SDK,流水线跑到这里时 dnx 会自动把对应工具拉到缓存再执行。这有点像 npx 在某些前端 CI 流程里的用法:你不关心全局环境有什么,你只关心这次构建需要什么工具,那就在命令行里写清楚。
不过这里有个度的问题,CI 里也分“每次跑都要用的工具”和“偶尔跑一次的工具”。如果是团队里每个人每天都要用、每个 PR 都要跑的格式化工具,让它常驻项目本地工具清单可能更稳定、速度更快,因为工具已经预置好了,不用每次从远端拉取。但如果只是发布包之前偶尔做一次打包验证、做一次依赖安全检查,那用 dnx 按需执行确实更轻。
4. 常见问题排查与避坑指南
4.1 命令找不到:区分本地工具、远程包、裸命令的场景
我实际用下来,最常见的报错就是 dnx 提示找不到你输入的东西。原因通常有几种。
第一种,你输入的命令名和实际包名不一致。NuGet 包的 ID 和它导出的 CLI 命令名往往不同,比如包叫Foo.Bar.CommandLine,命令可能叫foobar。dnx 先按命令行输入去匹配已有命令,匹配不到再尝试当包名解析,所以你在不确定时,优先用包名去跑,成功率更高。
第二种,包名或版本写错了。特别是版本号,NuGet 的版本号体系比 npm 更严格,1.0.0-beta.1这种预发布版本如果不带--prerelease之类的选项,解析时很容易被跳过。我建议在不确定包是否存在时,先到 NuGet 官网或者用dotnet package search确认一下 ID 和版本,再拿到 dnx 里执行。
第三种,本地项目工具没配置好。dnx 虽然会尝试读取项目的本地工具清单,但如果清单文件格式不对或者工具的local配置缺失,它可能就直接跳过本地查找,然后去远程拉取同名工具,结果拉回来一个不是你预期的版本。遇到这种情况,检查一下dotnet-tools.json文件里的内容,确认每个工具都标了版本。
4.2 私有 NuGet 源与离线环境:配置镜像和源
dnx 要拉包,底层还是走 NuGet,所以它免不了要读取 NuGet 配置。如果你公司内部有私有源,或者身处内网环境访问外网 NuGet 不稳定,需要预先配好源。
配置方式和你平时用 NuGet 完全一样,可以在nuget.config文件里加源:
<configuration> <packageSources> <add key="nuget.org" value="https://api.nuget.org/v3/index.json" /> <add key="company" value="https://nuget.example.com/v3/index.json" /> </packageSources> </configuration>我遇到过一种情况是:本地的nuget.config把外部源清掉了,只留私有源,而私有源上又没有 dnx 想要的那个工具包,结果 dnx 一直报找不到包。排查思路很简单,用下面命令看当前生效的源:
dotnet nuget list source再看输出的源列表里有没有你需要的那个。如果工具包只在公共 NuGet 上有,而你的全局配置屏蔽了它,需要把公共源加回来,或者在执行 dnx 时指定一个单独的配置文件,把公共源和私有源都配上。
离线环境下就更直接了,先把需要的 .nupkg 包下载下来放到本地文件夹,然后把这个文件夹作为源配置进去,dnx 解析时会从本地源拿到包并执行。这种方式特别适合那些有严格安全要求的构建环境,包从外网下载一次后放进内网,所有后续执行都不碰外网。
4.3 缓存、版本与安全:dnx 的缓存机制、锁定版本、提权风险
dnx 把工具放到本地缓存后,有个问题容易被忽略:工具会更新,但缓存里的旧版本还在。如果你反复跑同一个命令名,有可能一直用的都是旧版本工具,根本感知不到新版本发布了。这和 npx 的行为其实有点像,但 npx 在很多场景下会提示有新版本,而 dnx 至少在我用的预览版里没怎么提醒。
我的建议是,对于正式流程里要用的工具,尽量锁定明确版本号,比如dnx some-tool@1.2.3。这样哪怕缓存里有新版本,也不会影响确定性。日常临时体验工具可以不锁版本,但对于要写进文档、写进 CI 脚本的命令,锁定版本和校验包来源都是负责任的做法。
安全方面也想多说两句。dnx 拉取工具包后直接执行,本质上和dotnet tool install后再运行是一样的权限模型,但因为它“按需拉取”的特点,使用者更容易忽略工具包的来源。npm 生态里早就有过恶意包通过名字仿冒来投毒的案例,.NET 这边虽然没那么猖獗,但也别掉以轻心。尽量从可信的、你认识的作者或组织发布的包里执行工具,不要为了图新鲜去跑一个来路不明的包。
注意:在需要管理员权限的目录里跑 dnx 时要格外小心。工具有时会尝试读写当前目录或临时目录,如果你用管理员终端运行,等于给了它更高权限。能不用管理员跑就尽量别用。
4.4 与现有工作流的取舍:dnx / dotnet tool / 直接 dotnet run 怎么选
最后聊一个更实际的问题:有了 dnx 之后,是不是所有工具场景都要改成 dnx?
我的判断是:不,要看场景。如果是团队每个成员每天都要用、每个项目构建都要依赖的稳定工具,放到项目本地工具清单里并用dotnet tool restore恢复,仍然是更可靠、更快的方式。工具版本跟着项目走,成员改代码时能看到工具配置的变化,代码评审的时候也能看到版本升级,这个是 dnx 那种“飞一趟就到”的模式比不了的。
反过来,如果你只是临时想跑一下某个分析脚本、验证一个 NuGet 工具好不好用、在 CI 的某个一次性步骤里调一下辅助工具,那 dnx 无疑更合适。它把“装工具”这个环节的成本压缩到近乎为零,你完全可以把注意力放在“这个命令能做什么”上,而不是“我得先装什么才能跑这个命令”。
还有一类场景是dotnet run的天下:你自己写了一段代码,放进一个项目里,想让代码编译执行。dnx 跑的是别人打包好的 NuGet 工具,dotnet run跑的是你自己的源码,两者定位完全不同,不能互相替代。所以我的经验口诀是:
- 工具是现成的、稳定的、日常用 → 项目本地工具清单或全局工具
- 工具是现成的、偶尔用、想快速试 → dnx 按需跑
- 要跑的是自己的代码 →
dotnet run,别折腾工具化
现在 .NET 生态终于有了对标 npx/uvx 的 dnx,这一步我觉得来得虽然晚,但方向是对的。预览阶段我用下来,最大的感受是很多“临时任务”终于不用再为正儿八经的安装流程买单了,命令行体验从“仪式感很重”变成了“说跑就跑”。这个变化对新手尤其友好,因为你不再需要理解全局工具、PATH、本地 manifest 那一堆概念,只要知道包名,剩下的交给 dnx。
最后再分享一个我自己的小习惯:我会在常用的几个项目里把dnx --help的输出刷一遍,然后挑几个可能用得上的命令记在笔记里。毕竟 .NET 10 正式版发布之后,工具生态大概率会快速丰富起来,到时候命令行这块的玩法只会更多。现在趁早把 dnx 用顺手,等生态成熟了你就是老手了。