如果你做过几年 .NET 开发,大概率经历过这种场景:想临时跑一个工具,先得dotnet tool install -g,装完还怕跟全局已有的版本冲突,折腾半天终于跑通了,换台机器又得重新来一遍。Node 那边有 npx,Python 那边有 uvx,一条命令直接拉起来用完即走,.NET 生态一直缺这么个趁手的东西。这次 .NET 10 把 dnx 带回来了,算是把这块短板补上了。这篇东西我不打算写成官方文档,就按我自己拿到预览版之后实际折腾的思路,聊聊 dnx 到底解决了什么问题、怎么用、以及中途踩过的那些坑。
1. 全局安装工具的痛点,以及 npx/uvx 给出的答案
1.1 从“装到系统里”到“用的时候再拉”
先说说 npx 和 uvx 为什么能火。以前我们要用某个 Node 包,标准流程是npm install -g,把包装进全局目录,然后所有项目都能直接调用。这套模式的问题在于:全局空间像一间堆满杂物的仓库,装得越多越乱。你为了一个脚本命令装了个全局包,这个包又带了十几个依赖,时间一长根本记不清机器上到底装了什么。更麻烦的是版本,两个项目一个要 A 版本一个要 B 版本,全局只能装一个,来回切换配置能让人抓狂。
npx 的思路是完全换了个玩法:我不管你本机有没有这个包,反正我执行npx 包名的时候,去 registry 里找到它,然后临时拉下来跑。如果本机已经有缓存,直接用缓存;如果没有,拉完跑完就扔,不写进任何全局目录。Python 那边的 uvx 也是同样逻辑,只是把 registry 换成了 PyPI,把包管理器从 npm 换成了 uv。
这种“临时拉取”的模式解决了一个很本质的问题:工具的运行时状态跟项目的运行时状态彻底解耦。你写项目的时候依赖 lock 文件来固定版本,工具凭什么不能这样?npx 和 uvx 让你可以为某个目录、甚至某一条命令临时指定版本,用完不残留,环境永远是干净的。
1.2 .NET 生态的尴尬:dotnet tool 为什么一直差点意思
其实 .NET 不是没有工具机制,dotnet tool install已经存在好几个大版本了。但它的问题在于设计哲学还停留在“全局安装”的时代。哪怕你计划只在一个项目里用某个工具,官方推荐的做法也是先全局装好,然后通过dotnet tool restore在项目维度上做管理。这就带来两个实际体验上的落差:第一,第一次使用必须先安装再运行,两步走;第二,全局安装的入口一旦多了,环境变量 PATH 就很容易出问题,我在 Windows 上就遇到过dotnet tool install -g装完以后命令找不到的情况,最后发现是工具目录没进 PATH,这种问题排查起来非常恶心。
所以社区里一直有人拿 npx 来类比期望:.NET 能不能也搞一个“一条命令临时跑 NuGet 包”的东西?dnx 在 .NET 10 里回归,就是冲着这个需求来的。
2. dnx 的定位与设计逻辑
2.1 dnx 是什么:不是老 DNX 的复活
如果你是一个老 ASP.NET 5 时期的开发者,听到 DNX 可能会一愣——那个不是早没了吗?对,.NET Core 早期确实有个 DNX(.NET Execution Environment),当年是拿来跑跨平台应用的一层运行时。后来 .NET CLI 成熟之后,那套东西就被并掉了。现在 .NET 10 的 dnx,虽然缩写一样,但定位完全不同:它的全称更接近“.NET Execute / Tool Runner”,是一个专门用来“临时获取并运行 NuGet 工具”的命令工具。
你可以先有一个朴素的认知:dnx就是 .NET 版的npx。它不再是某个运行时框架的名字,而是一个薄薄的命令行入口,负责两件事——解析你要运行的工具名,以及把对应的 NuGet 包临时拉取到本机缓存里执行。这个设计思路和 npx 几乎一致:
- 不需要手动
dotnet tool install; - 运行结束后,工具不会常驻在任何项目或全局工具清单里;
- 多版本可以共存,按命令级别来动态选择。
2.2 它和 dotnet tool 的关系:互补而非取代
这里我想先打消一个顾虑:dnx 不是来消灭dotnet tool的。恰恰相反,两者面向的场景不同:
| 对比维度 | dotnet tool | dnx |
|---|---|---|
| 安装方式 | 显式 install 到全局或工具清单 | 临时拉取,自动缓存 |
| 版本管理 | 手动指定,更新需要显式 upgrade | 可在命令里直接指定版本 |
| 使用场景 | 长期使用的开发辅助工具 | 偶尔执行、验证、脚本化调用 |
| 环境侵入 | 会写入全局工具目录或本地 manifest | 不写入项目,只进缓存目录 |
| 适合人群 | 每天都要用的核心工具 | 临时想跑一下,或 CI 里快速调用 |
打个比方,dotnet tool像是在家里固定安装一台咖啡机,你每天都要用,放在台面上方便。而 dnx 更像你去朋友家做客时临时要冲一杯挂耳咖啡,用完纸袋一扔,不占用任何厨柜空间。两者并不冲突,反而配合起来很舒服——常用工具用dotnet tool固定,冷门工具、一次性的脚本调用全部交给 dnx。
2.3 为什么说它开启了“npx/uvx 时代”
这个词不是夸张。过去你要在 .NET 生态里找一个工具,第一反应是去 NuGet 搜索然后手动安装,中间隔着“搜索、了解安装方式、全局安装、补 PATH、验证”五个步骤。dnx 直接把这个链条压缩成一步:知道工具名就能跑。这带来的连锁反应是:工具的分享和传播开始变得像命令一样轻量,一个工具的“安装成本”趋近于零。这跟 npx 第一次普及时候的状态非常像——为什么大家都喜欢用npx create-react-app而不喜欢全局装脚手架?因为省心、干净、不会污染环境。dnx 在 .NET 生态里扮演的正是这个角色,尤其是配合 .NET 10 对本地 AOT 发布、以及对单文件工具的支持,跑一个工具的成本已经低到可以忽略不计。
3. dnx 的安装和核心用法
3.1 前置条件:版本和运行时
先泼一盆冷水:dnx 不是一个独立的安装包,它是 .NET 10 SDK 自带的能力,所以前置条件非常直接——安装 .NET 10 SDK。
我在 Windows 和 Ubuntu 上都做了验证,安装完之后直接执行:
dnx --version如果你能看到版本号,说明已经就绪了。注意,这里不需要单独“开启某个功能开关”,SDK 默认就带。在 Windows 上要特别检查一下,新版 SDK 安装完之后 PATH 里是否已经有dnx的入口,有些场景下旧版本的 SDK 配置可能会覆盖新的 PATH 写入,导致命令找不到。
我需要强调一点,dnx 本身依赖 .NET 10 运行时,所以这台机器上至少要能跑dotnet --version得到 10.x。如果你同时在用 Visual Studio,尽量把 VS 升级到支持 .NET 10 的版本,否则 IDE 里的外部工具调用可能解析不到 dnx。
3.2 一次运行:最核心的 dnx 命令
假设我想临时跑一个叫dotnet-format的代码格式化工具,传统做法是:
dotnet tool install -g dotnet-format dotnet-format --version用 dnx 的话,直接:
dnx dotnet-format --version第一次执行时,dnx 会去 NuGet 上解析最新的稳定版,下载到本地缓存,然后运行。之后你再执行同样的命令,它检测到已有缓存,直接复用,速度会快非常多。这就是 npx 用户非常熟悉的那套“第一次慢一点,后面飞起”的体验。
如果你想临时用某个特定版本,可以这样:
dnx dotnet-format@8.0.0 --version版本号直接跟在包名后面,@分隔。这个设计跟 npx 的@version后缀几乎一模一样,好处是在同一个目录里,你可以一键切换不同版本的工具来对比输出结果,互不干扰。
3.3 缓存与清理:别让临时文件悄悄霸占磁盘
一边强调“用完即走很干净”,一边也得诚实面对缓存机制。dnx 的临时包会缓存在用户目录下(Windows 上通常是在%LOCALAPPDATA%\dnx\cache,Linux/macOS 是在~/.cache/dnx一类的位置),长时间使用下来,缓存目录会越来越大。好在它跟 NuGet 包缓存是两套体系,清理起来也比较直接:把整个缓存目录删掉,下次需要时会重新拉取,不影响任何项目。
如果你对磁盘空间比较敏感,可以考虑定期执行清理,或者让 CI 环境里的 dnx 跑完之后自动删除缓存目录。我在本地一般不会主动清理,毕竟缓存能提升二次执行速度,但确实见过同事拿着几 GB 的缓存目录来找我帮忙排查磁盘占用,所以这块还是建议大家心里有数。
3.4 项目级配置:在 manifest 中固定 dnx 依赖
dnx 的长期价值和 npx 一样,在于它可以配合项目的 manifest 文件来固定工具版本。做法是在项目根目录维护一个工具配置文件,里面声明你需要哪些 dnx 工具和版本,团队成员拉完代码之后,直接用 dnx 一条命令就能把环境拉到一致状态。这比每个人手动装全局工具靠谱太多——很多人换了机器或者新入职,第一步永远是在为环境不一致而调来调去。
以我自己的项目为例,我习惯在仓库里放一个dnx.json,内容大致是:
{ "tools": { "dotnet-format": "8.0.0", "dotnet-ef": "9.0.0" } }然后在 README 里写清楚,跑新代码之前执行一次:
dnx restore它会根据 manifest 里声明的版本把对应工具拉取好,后续在项目目录里用哪些工具、什么版本,全团队保持一致。这个思路本质上是把“工具依赖”也纳入版本管理,跟前端package.json里声明依赖再npm install的路数完全一致。
4. 实操过程与核心场景复现
4.1 从零跑通一个格式化工具
我先拿一个最常见的场景完整演示一遍。假设我在写一个 .NET 库项目,代码风格想统一,但不想让团队每个人手动装dotnet-format,也不想把它写进全局目录。
第一步,进入项目目录,执行:
dnx dotnet-format --verify-no-changes我这里特意加了--verify-no-changes,是让工具只校验格式而不真正改动文件。第一次执行时,dnx 会显示正在获取dotnet-format包以及依赖项,然后跑出结果。你会注意到,这一步之后,项目的obj目录里不会出现工具的痕迹,全局工具清单里也不会新增任何条目,干干净净。
第二步,如果你想真正格式化,把参数去掉:
dnx dotnet-format它会自动扫描当前目录下的解决方案文件,格式化所有源码文件。改动完成后,我用git status能看到具体哪些文件被动了,完全可控。
这套流程放在以前至少要两步:先dotnet tool install -g dotnet-format,再等它下载安装,然后在输出的路径里去找命令。而且不止于此,被装在全局的工具会常驻在 PATH 中,如果哪天你不再需要它,还得记得dotnet tool uninstall。dnx 跑完就扔,不装了也不用卸载,体验上完全是另一种东西。
4.2 在 CI 脚本里用 dnx 跑迁移工具
另一个我实际用得很顺的场景是 CI 里的数据库迁移。过去在 CI 里跑 EF Core 迁移,要在脚本里先dotnet tool install -g dotnet-ef,再dotnet ef database update。这里有两个烦人点:一是全局安装需要额外的网络请求和失败重试逻辑;二是安装版本如果不固定,今天跑是 8.0,明天可能因 NuGet 上最新版变化而踩坑。
换成 dnx 之后,CI 脚本变得非常简洁:
dnx dotnet-ef@9.0.0 database update --connection "$CONN_STR"版本直接锚定在命令里,第一次拉取缓存后,后续步骤读取缓存,速度稳定。就算 CI 环境每次都是全新的,也就只有一次下载成本,相比传统的跑到一半发现工具版本不对,这种方式的确定性高得多。
我特别建议在 CI 里把 dnx 缓存目录挂到持久化路径上,比如自建 Runner 的/opt/dnx-cache,这样同一台机器上多次构建可以共享缓存,进一步减少网络开销。这个思路和很多人用npm cache加速 CI 是一个道理。
4.3 移动端、IoT、脚本场景的临时体验
dnx 还有一个容易被忽略但很有价值的使用场景:你在一个只有 .NET 运行时、没有完整开发环境的机器上(比如 IoT 设备、临时容器),想快速跑一个工具做诊断。传统方式会卡在“装 SDK、配 PATH、装工具”这一串步骤上,而 dnx 只需要在具备 .NET 10 运行时的环境里,直接拉取工具执行,非常契合快速排查、临时脚本这类需求。
举个例子,我在一台跑着 .NET 10 服务的容器里,想快速看某个程序的依赖信息,用 dnx 临时跑一个解析依赖的工具,几秒钟就出结果,容器退出之后什么都不留下。这在以前的 .NET 版本里是没法想象的,通常你得把工具提前打进镜像,为此还得专门维护一段 Dockerfile。
5. 常见问题与排查技巧实录
5.1 命令找不到:PATH 与环境解析问题
很多人在第一次用 npx 时遇到的那句经典报错——npx : 无法将“npx”项识别为 cmdlet、函数、脚本文件或可运行程序的名称——本质上不是 npx 的问题,而是 Node 安装之后 PATH 没生效或者安装没装全。dnx 也可能会踩到同款坑。
我的排查顺序是这样的:
先用where.exe dnx(Windows)或which dnx(Linux/macOS)确认命令是否真的在 PATH 里。如果找不到,进入 .NET SDK 的安装目录手动确认dnx是否存在。Windows 下通常 SDK 根目录里有个dnx.exe,它与dotnet.exe同目录,如果只有dotnet.exe而缺少dnx.exe,多半是 SDK 版本不对,需要重新安装完整的 .NET 10 SDK。
另一种情况是在 Windows 上用 PowerShell 执行.cmd后缀的命令,可能遇到执行策略限制。解决方案不是关闭整个系统的脚本策略,而是针对当前用户开放受限的执行策略,或者用dnx.cmd的完整路径来调用。
5.2 版本解析不一致:为什么拉到的不是想象中那个版本
dnx 默认会解析 NuGet 上的最新稳定版,但如果你本地缓存里已经有了旧版本,它可能不会立刻去获取最新版。这个行为跟 npx 类似,都是“有缓存用缓存”。我遇到过的问题是这样的:朋友在 NuGet 上发布了一个新版本,我这边执行 dnx 命令时用的还是几个礼拜前的缓存版本,一度以为发布失败了。
排查方法很简单:给命令加一个--refresh或者手动清理缓存目录,强制重新解析版本。平时为了稳定起见,我更推荐在命令里直接指定版本号,这样既不会被缓存误导,也能保证 CI 环境的一致性。
5.3 代理与内网环境:NuGet 源配置
在一台只能访问内网 NuGet 源的环境里使用 dnx,跟用dotnet restore是一样的逻辑,需要配置 NuGet 源地址。如果没配好,dnx 会一直停在“无法连接远程服务器”的报错上,看起来就像网络不通。
我的配置经验是,在NuGet.config里同时配置内网源和官方源,并且把内网源放在前面,这样 dnx 会优先从内网拉取工具包。如果你的内网源是类似 Artifactory 或者本地 NuGet Server,需要额外确认 API 版本兼容性,新版 NuGet 协议和老的 NuGet.Server 之间偶尔会有格式不兼容的情况。
这里也可以说一个务实的经验:不要只在官方文档里找答案,多看看本机已有的NuGet.config继承链。dnx 用的源配置和dotnet restore是同一条链路,你在项目目录下新建一个NuGet.config,把源配好,dnx 就会自动遵循。
5.4 常见问题速查表
| 症状 | 可能原因 | 处理方式 |
|---|---|---|
| dnx 命令找不到 | SDK 不是 .NET 10,或 PATH 未包含 SDK 目录 | 重新安装 .NET 10 SDK 并检查 PATH |
| 执行脚本被拦截 | PowerShell 执行策略限制 | 针对当前用户设置允许本地脚本执行 |
| 拉包失败 | 网络受限或 NuGet 源不可达 | 配置内网源,检查代理设置 |
| 使用的版本跟预期不一致 | 缓存了旧版本 | 清缓存或显式指定版本号 |
| 在非 SDK 环境无法运行 | 只装了运行时没装 SDK | 安装包含 dnx 的完整 SDK |
| 和旧版 dotnet tool 命令重名冲突 | 全局工具里有同名命令 | 用 dnx 完整命令路径,或卸载旧全局工具 |
排查这类问题,我个人的心得是先别急着怀疑工具本身,先判断它到底走到哪一步。dnx 无非就是“解析包名、拉取包、执行工具”三段,哪一段出问题,报错信息位置大致能看出来。拉取阶段的错误多半是网络和源配置,执行阶段的错误多半是运行时不匹配或参数问题。把阶段分清楚,排查效率能高不少。
6. 我对 dnx 的一些具体体会
从拿到 .NET 10 预览版开始,我把 dnx 用在了日常开发、脚本编写、CI 流程、容器诊断好几个场景里,最大的感受是“存在感很低”。它不是那种会让你频繁感知到的功能,但当你习惯了直接在命令行敲dnx 工具名@版本之后,再让你回到“先全局安装,再跑”的节奏,你会觉得非常别扭。
我最喜欢的一点是它把工具的使用门槛降到了极低。以前我给同事推荐一个 .NET 工具,得附带一段安装说明,现在直接告诉他“执行dnx 包名就行,第一次会自动拉取”,大家都觉得这个事情变得轻松很多。
当然,它也有一些需要适应的地方。比如本地缓存的清理策略目前比较原始,磁盘空间偏紧的人需要主动管理;还有 NuGet 包作为工具分发渠道,对包体积和入口程序集结构有要求,并不是所有包都能直接拿来做 dnx 运行。但这些都是生态逐步完善的过程,方向对了,细节可以慢慢磨。
往后我大概率会在所有新项目里统一使用 dnx 来管理少量辅助工具,把dotnet tool留给那些真正需要全局常驻的重型工具。两套配合着用,环境干净、版本可控,这大概就是 .NET 工具链正在变好的一种具体感受吧。