dnx 来了:.NET 10 内置 npx 式临时工具运行器
2026/9/12 11:10:49 网站建设 项目流程

如果你做过几年 .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 tooldnx
安装方式显式 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 工具链正在变好的一种具体感受吧。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询