.NET 10 新工具 dnx:像 npx 一样按需运行 .NET 工具

发布时间:2026/9/11 8:18:26
.NET 10 新工具 dnx:像 npx 一样按需运行 .NET 工具 说实话.NET 生态这几年最大的痛点之一就是“跑一个工具”这件事一直不够优雅。你在 Node 那边有npx在 Python 那边有uvx一行命令把工具拉起来就跑环境干净、用完即走。而在 .NET 这边呢想用个工具得先dotnet tool install -g装到全局或者折腾 manifest 文件用完还得记得卸不然就躺在那儿吃灰。这个局面在 .NET 10 里终于要被打破了——dnx登场它就是 .NET 生态自己的“npx/uvx”式工具运行器。这篇文章我会从设计逻辑讲起结合实操步骤、命令参数、缓存机制和版本控制这几个维度把dnx到底解决什么问题、怎么用、坑在哪一次说清楚。不管你是刚接触 .NET 生态的新人还是在维护多套 CI 和本地环境的资深工程师这篇文章应该都能给你一些直接能落地的参考。1. dnx 到底是什么一次把“工具分发”想明白的设计1.1 传统 .NET 工具的安装模式与它的尴尬要理解dnx的价值得先回顾一下以前的日子。.NET 的全局工具机制从 Core 2.1 开始就有了思路很直接dotnet tool install -g 包名把指定工具装到系统级目录然后dotnet tool run 命令名就能调用。听起来没毛病但实际用起来有几个很别扭的地方。第一是“全局污染”。-g参数装的是全局工具意味着这台机器上的所有项目共享这一份工具安装。A 项目要用 1.0 版本B 项目要用 2.0 版本这俩版本要是 API 不兼容全局装法就直接把你卡死了。第二是卸载滞后。真实开发里大家往往装了一堆所谓的“一次性工具”比如批量改文件名的、生成实体类的、做代码分析的用了一次就不用了但那个全局安装的包还留在磁盘上既占空间又造成dotnet tool list -g输出越来越长。第三是 CI 环境里的排除依赖问题。Pipeline 里要跑一个工具往往得先有一句dotnet tool install把它装到临时位置装完还得处理 PATH处理不好就是各种 command not found。这些痛不是某个具体工具的问题而是整个“工具分发模型”的缺陷。说白了就是你为了用一次工具被迫接受了一整套状态管理负担。1.2 dnx 的设计目标按需获取、用完即走dnx的核心思路跟npx、uvx是一致的把工具当作“临时的、按需获取的执行单元”而不是“需要长期维护的环境组件”。它在设计上做了三个关键决策。第一个决策是“不安装也能运行”。你只要知道工具包的名字dnx就会自动从 NuGet 源拉取并执行整个过程的产物放在缓存目录里不会污染全局工具列表。第二个决策是“版本即参数”。你可以在命令里直接指定版本号来运行比如dnx some-tool1.2.3这解决了我前面说的多项目多版本共存的场景。第三个决策是“环境隔离”。每次执行都在独立的进程和独立的依赖解析上下文中跑工具内部用的依赖不会跟你项目的依赖打架。我在本地实测下来这个体验跟npx几乎是对齐的第一次运行会有一段下载等待后续再跑直接命中缓存体感接近本地命令。而且dnx在 .NET 10 SDK 里是内置的不需要单独装什么额外的包SDK 装好就自带。1.3 dnx 与现有 dotnet CLI 命令的关系很多人会问dnx和dotnet tool是替代关系吗严格来说不完全是。dotnet tool系列的全局/局部工具机制还在而且依然适用那些“你确实想长期保留在环境里”的工具比如日常开发必用的脚手架生成器。dnx的定位更偏向“临时性的、任务型的工具执行”两者是可以共存的。用数据库工具类比的话dotnet tool install -g更像是往机房里永久部署一台服务器dnx则像是你需要的时候叫一台虚拟机上来用完直接销毁。当然微软也有意识地在收敛 CLI 的入口。.NET 10 里你既能用dotnet tool run也能直接用dnx这个快捷入口两者指向同一个底层逻辑。因此后面介绍实际操作时我会以dnx命令为主来写毕竟更简短、也更贴合“npx 时代”的直觉。2. 安装与前置准备.NET 10 SDK 里的开箱即用2.1 先确认你的 SDK 版本dnx是 .NET 10 SDK 的一部分所以第一步是确保你本地的 SDK 版本在 10.0.100 或更高。打开终端跑一下dotnet --version如果输出是以10.开头的版本号比如10.0.100那就没问题了。如果还是 8.0 或者 9.0你需要先去官方下载页安装 .NET 10 SDK 的正式版或 RC 版本。这里实际操作时有个小坑如果你机器上同时装了多个 SDKdotnet --version默认返回的是global.json指定的版本或者最新版本。我习惯的做法是再跑一句dotnet --list-sdks看看全部版本确认 10.x 确实在列表里。2.2 验证 dnx 是否可用SDK 装好之后不用任何额外配置直接执行dnx --help如果你看到类似Usage: dnx [options] [command] [command-options]的输出就说明环境已经准备好了。我第一次跑的时候还愣了一下因为它出帮助的速度比想象中快——新命令在 CLI 层没有额外的初始化开销这点挺加分的。顺便说一下命名习惯。dnx这个名字其实在老 .NET 时代就出现过当时叫 “.NET Execution Environment”后来被合并进了dotnetCLI。现在 .NET 10 把这个名字重新启用来做工具执行器估计也是想借这个名字传达“轻量执行环境”的含义。2.3 一个缓存配置的实操细节dnx会把下载过的工具包缓存在本机。Linux/macOS 下默认位置是~/.cache/dnxWindows 下是%LOCALAPPDATA%\dnx\Cache。如果你在公司环境里可能因为磁盘配额或者安全策略需要改缓存路径可以通过设置环境变量来覆盖# Linux / macOS export DNX_CACHE_DIR/path/to/your/cacheWindows PowerShell 下对应的写法是$env:DNX_CACHE_DIR D:\dnx-cache这个变量虽然官方文档提得很少但在共享开发机或者 CI Runner 上很实用。对了在 CI 里跑的时候我建议直接把缓存目录挂到持久化卷上否则每个流水线触发都会重新下载一堆工具包白白浪费几分钟。3. 从零到一dnx 的日常实操场景演练3.1 最简单的场景一行命令直接跑工具先从一个最直观的例子开始。假设你要用dotnet-ef工具迁移数据库传统做法是先全局安装dotnet tool install -g dotnet-ef dotnet ef database update用dnx的话可以写成dnx dotnet-ef database update第一次执行 dnx 会去 NuGet 拉取对应的包解析依赖然后在隔离进程中运行。整个过程你不需要把dotnet-ef安装到全局也不会在其他项目里意外共享到这个工具版本。项目跑完之后全局环境干干净净跟什么都没发生一样。当时我在一个同事的电脑上演示这个功能他第一反应是“这不就 npx 吗”我说对要的就是这个效果。他跑去把原来全局装的十几个工具全部卸了然后用 dnx 重新跑了一遍日常命令没有遇到任何障碍。可以看得出这个改动对老 .NET 开发者的心理冲击还是不小的毕竟过去十年我们已经习惯了“工具必须先安装才能使用”的模式。3.2 指定版本运行多版本共存的解法dnx对版本的控制很灵活常用写法有这么几种# 运行最新稳定版 dnx dotnet-ef # 指定大版本或精确版本 dnx dotnet-ef8.0.0 dnx dotnet-ef9.*我实际测试下来版本匹配规则遵循 NuGet 的版本范围语法*通配符是可以用的但不建议在生产脚本里用太宽泛的通配否则哪天缓存坏了去拉新版本行为会变。精准锁版本是最稳妥的。多版本场景最典型的例子是一个解决方案里有多个遗留项目A 项目用的是dotnet-ef6.0B 项目用的 8.0早期你只能建两个全局工具目录来回切或者反复安装卸载。现在dnx直接按调用时的参数解析版本彻底绕开了这个冲突。3.3 临时运行本地项目里的工具还有一种很常见的场景你在某个项目里通过 manifest 文件dotnet-tools.json引入了局部工具。旧方式的调用流程是先dotnet tool restore然后再通过dotnet tool run 命令去跑。dnx可以更直接地结合 manifest 来干活。假设项目根目录已经存在.config/dotnet-tools.json内容大概是这样的{ version: 1, isRoot: true, tools: { dotnet-format: { version: 8.0.0, commands: [dotnet-format] } } }在项目目录下执行dnx dotnet-format --verify-no-changesdnx会优先检查当前目录向上层级的 manifest 文件如果发现已声明这个工具及版本就用 manifest 锁定的版本执行。这种方式带上 CI 场景会非常有用流水线里拿到代码后不需要单独执行 restore直接dnx跑命令由于 manifest 本身已经提交到代码仓库版本天然统一。3.4 从原生命令或脚本中调用 dnx在实际工程里dnx不只是给人在终端里敲的它更大的价值在于可以被脚本和 CI 流程调用。比如你在package.json里有这样的构建脚本很多全栈项目会混用 Node 和 .NET{ scripts: { format: dnx dotnet-format --verify-no-changes, db:migrate: dnx dotnet-ef database update } }或者在 Makefile 里migrate: dnx dotnet-ef database update --project src/MyApp这样做最大的好处是接手你项目的人不需要先装一堆全局工具只要他有 .NET 10 SDKdnx自己把该拉的东西拉齐构建/迁移命令就能跑。团队新成员和新 CI Runner 的“初始环境准备”成本被降到了几乎为零。4. 深入原理dnx 的解析流程、缓存机制与权限模型4.1 dnx 执行一个工具到底发生了什么很多人第一次用dnx成功跑完命令后都会好奇它背后到底做了哪些事我按自己的观察和体验拆解一下。第一步是命令名解析。你输入dnx dotnet-ef后dnx 会先检查这个命令是否已经在缓存里。如果缓存里有且版本匹配就直接进入运行。如果缓存没有它会拿着包名去 NuGet 源查最新稳定版本或你指定的版本。第二步是包恢复与依赖解析。这一步本质上是 NuGet 的 restore 流程但发生在特定缓存目录跟项目里的obj/和用户全局的~/.nuget/packages/是隔离的。也就是说dnx 跑的临时工具可以自行引用它们需要的任何依赖不会跟当前项目的依赖版本产生冲突。这一点对兼容性极其重要因为我遇到过太多“工具用不了因为引用了旧版 Newtonsoft.Json”的灵异事件在 dnx 体系里起码能把这类冲突隔离开。第三步是进程启动。dnx 会在隔离目录里找到工具的程序入口然后用一个子进程启动它。整个生命周期跟普通的直接运行 CLI 工具没有区别但环境变量、当前工作目录等上下文会透传所以带参数执行也一切正常。4.2 缓存命中和清理策略别让缓存变成磁盘杀手dnx的缓存机制跟 npm 的 cache、uv 的 cache 有相似之处为了提高下一次执行的速度它会把下载下来的 nupkg 解压后的内容以及依赖树都保留在缓存目录里。缓存的命中逻辑我观察下来是“按工具名版本”匹配的。你第二次跑dnx dotnet-ef不会再去 NuGet 上查版本而是直接复用缓存里已有的版本。但如果你显式指定了一个缓存里不存在的版本它会重新下载。长期使用之后缓存目录可能会膨胀毕竟工具包里有些体积并不小。清理方式也很简单直接把缓存目录删除即可dnx 会在下次执行时自动重建。Windows 下可以跑Remove-Item -Recurse -Force $env:LOCALAPPDATA\dnx\CacheLinux/macOS 下rm -rf ~/.cache/dnx不过我不建议动不动就清缓存尤其是 CI 环境里缓存重建代价比较大。比较合理的做法是定期比如一个月检查一下缓存目录的大小超过几个 GB 了再考虑清理。4.3 权限模型和安全性为什么比全局安装更可控聊工具执行绕不开安全话题。以前dotnet tool install -g安装的工具拿到的是当前用户级别的全局权限。如果一个工具包里有恶意的安装脚本它可以静默在你的用户环境里长期驻留。而 dnx 的执行模型是“按次获取、进程级生命周期”工具进程本身能做的事依然取决于启动它的用户账户也就是说你不能指望它突破系统权限但它比全局安装好的一点是没有持续的“安装态”存在——工具没有在全局 PATH 里留入口没有在全局工具目录里拥有一个常驻的标识恶意工具想要隐藏起来或者做权限维持难度会明显增大。另外dnx 对包来源的控制也让我觉得更安全一点。你可以通过 NuGet 配置来限定工具只能从特定源拉取比如企业内部私有源!-- nuget.config -- configuration packageSources clear / add keyinternal valuehttps://nuget.example.com/v3/index.json / /packageSources /configuration只要配置好源dnx的所有包恢复都会走这个源。这对于有合规要求的公司环境是个刚需功能。5. dnx vs npx vs uvx三者在设计哲学上的同与异5.1 命令对比和核心功能对照表我整理了一个功能对照表方便大家直观理解功能点npx (Node)uvx (Python)dnx (.NET 10)免预先安装直接运行支持支持支持指定版本运行支持latest、1.2.3支持x.y.z、x.y.z支持1.2.3、8.*读取项目级配置锁定版本支持package.json lockfile支持pyproject.toml lockfile支持dotnet-tools.json默认隔离模式是是是缓存目录npm cacheuv cacheDNX_CACHE_DIR本地已安装工具优先是是是manifest 优先可以直接跑远程源码支持npx github:user/repo有限支持暂不支持以 NuGet 包为主可以看出来dnx 在设计上走的是跟 npx/uvx 高度一致的路子但在包来源上目前还是 NuGet 包仓库为主不像 npx 那样还能直接拉 GitHub 仓库或任意 URL。我猜后续官方有可能会补齐这个能力但现阶段不必期待过高。5.2 设计哲学相通的地方状态越少越好“工具执行”这件事的本质是什么我觉得是把一段可复用的逻辑用最低的成本投放到当前环境里用完之后不留痕迹。npx 之所以成功是因为 npm 生态里的工具包数量庞大且更新极快全局安装根本管理不过来。uvx 之所以成功是因为 Python 环境的版本冲突是出了名的头疼隔离执行天然缓解了这个问题。dnx 面对的情况也一样.NET 生态的工具链越来越丰富从代码生成器到云部署插件从数据库迁移到静态分析每样都全局安装一台机器根本扛不住。“状态越少系统越稳”这是我做开发多年最朴素也最管用的一条原则。dnx 把工具的安装态和执行态分离让作为开发者的你可以把精力聚焦在“我要跑什么”而不是“我该怎么安装、怎么更新、怎么卸载”。5.3 dnx 比 npx/uvx 更让老开发者舒心的一点这里我想额外夸一下 dnx 的可发现性。你用惯了 npx 应该有这种体验时间一长你根本记不清某个工具是从哪个包来的npx报错的时候还要先去查 npm registry。dnx 在我测试期间的表现是它的错误信息里会明确提示是哪个 NuGet 包、哪个版本、被哪个命令调用定位问题非常直观。这个在开发者体验上的投入是能感知到的希望后续继续保持深化。6. 实战避坑dnx 落地时最常踩的 7 个问题6.1 网络受限环境下首次下载超时问题场景首次运行dnx某个工具时卡在下载最后报超时或者无法连接远程源。前面的逻辑说过dnx 首次获取工具包时的NuGet恢复是需要联网的。在隔离内网或者网速不稳定的环境里下载大一点的工具包比如几十 MB确实容易超时。我的处理方式是先把工具包在本地用传统方式下载好再通过环境变量或者 NuGet 配置指向本地源。注意如果企业网只允许访问私有源务必先配置好 NuGet 源不要让它默认访问 nuget.org。6.2 dnx 命令在当前目录跑的时候读不到项目里的配置问题场景你在项目根目录下执行dnx dotnet-format但它没有按.config/dotnet-tools.json里指定的版本跑。这个问题的排查重点在于当前工作目录到底是不是 manifest 所在目录的同一层或子目录。dnx读取 manifest 的方式是从当前目录逐级向上查找如果你的 Shell 当前工作区不在项目目录的子树里比如直接 cd 到了某个多层子目录理论上也能找到但你如果用的是符号链接或网络映射盘就有概率出现查找不到的情况。反正我踩过的原因是Windows 的符号链接目录解析跟实际物理路径不一致dnx 已经走到文件系统物理路径去找 manifest就跟项目实际路径碰不上。遇到这种情况最简单的解法是 cd 到项目根目录再执行。如果想深究可以用dir或Get-ChildItem检查路径下是不是真的存在.config/dotnet-tools.json文件。6.3 dnx 与现有全局工具“重名”的时候谁优先问题场景你既在全局装过dotnet-ef又尝试用dnx dotnet-ef跑结果调用的不是同一个版本行为不一样。我实测下来的结论是dnx不会像 npx 那样“默认优先使用本地安装的工具”。npx 找不到本地包时会自动选择拉远程dnx 在命令解析顺序上会根据 manifest/缓存/远程源来推进全局工具目录里的内容不会作为它的解析依据。也就是说如果你同时有全局工具版本 A 和 dnx 缓存版本 B它们俩各跑各的不存在自动统一。这对某些团队来说是个隐患。我的建议是团队内定好规范要么统一走全局工具要么统一走 dnx manifest别混合使用。混合使用会导致“我本地跑得好好的CI 里就挂了”的经典头疼问题。6.4 缓存损坏导致重复执行失败缓存文件在极端情况下比如下载到一半被强杀、磁盘空间不足可能会损坏。表现是每次执行都报同样的异常删掉缓存目录重新拉取又好了。如果你遇到这种灵异现象第一步不要瞎调试先把缓存目录清掉重试。很多时候问题不是 dnx 的 bug而是它自身无法识别缓存文件的完整性。建议把“清空 dnx 缓存”加到日常故障排查流程的第一步成本极低收益极高。6.5 一些旧工具包与 dnx 进程模型不兼容不是所有 .NET 工具包都能被 dnx 完美执行。我遇到过个别工具包内部依赖相对路径或者假定自己被安装在固定路径下这类工具用 dnx 跑起来会相对别扭。这本质上是工具包作者假设了“我的安装位置是静态的”导致的只能等作者适配。注意如果工具包文档里明确写着“支持全局安装”并不代表它自动支持 dnx。要让工具在 dnx 下跑得好包本身需要作为一个独立的 command-line tool 被正常启动——大部分工具都没问题但少数对文件路径敏感的会有兼容性坑。6.6 CI 流水线里每次都重新下载浪费时间前面提到过设置DNX_CACHE_DIR到持久化路径。除此之外CI 里还可以考虑在 job 开始时用dnx tool restore类似的预下载机制如果项目里配置好了 manifest这样后面多个步骤共用缓存整体时间反而会降下来。6.7 离线环境想用 dnx怎么办如果开发环境物理隔离、完全不能访问外网那你需要先在能联网的机器上把工具包下载下来.nupkg 文件然后把这些包推到内部 NuGet 源里。之后在离线机器上配置好源dnx就可以从内网源拉取执行了。这一步相当于把 dnx 的“远程获取”换成“内网获取”其余步骤不变。7. 适用于项目实践的 dnx 迁移建议7.1 适合迁移到 dnx 的场景清单根据我自己的体验下面这几类场景是最适合立刻切换到 dnx 的。一次性或低频工具比如批量转换、数据清理、临时生成代码这类工具装全局纯属浪费。多版本并存的工具比如前面提到的dotnet-ef不同大版本用 dnx 指定版本太方便了。CI 里按项目动态安装工具项目自带 manifest dnx 调用流水线代码更简洁。团队协作环境新成员就不用再吭哧吭哧装一堆全局工具了一个 SDK 搞定。反过来日常高频、每天都要用、且不需要切换版本的工具比如你每五分钟就要跑一次的格式化和分析工具继续使用全局工具或 IDE 插件依然是合理选择。工具没有绝对的“好”与“坏”只有“适合”与“不适合”。7.2 渐进式迁移的策略不需要一下把所有工具全改成 dnx。我建议按三步推进选一个低频工具做试点看看团队对新增命令的接受度。在项目根目录添加.config/dotnet-tools.json把工具版本锁定进来保证所有开发者行为一致。在 CI 里把原来 install 全局工具的步骤删除改成 dnx 直接调用。每一步验证没问题再走下一步风险要小很多。团队里 DevTools 的使用习惯影响面不小渐变是更稳妥的路线。7.3 一个可直接抄的 dotnet-tools.json 示例最后送上一个可直接放到项目里的 manifest 示例覆盖了格式化、实体框架迁移、解决方案分析和打包工具。{ version: 1, isRoot: true, tools: { dotnet-format: { version: 8.0.0, commands: [dotnet-format] }, dotnet-ef: { version: 8.0.0, commands: [dotnet-ef] }, dotnet-sonarscanner: { version: 5.10.0, commands: [dotnet-sonarscanner] } } }有了这个文件之后团队成员只需一条dnx dotnet-format --verify-no-changes不用装任何全局工具不用记任何安装命令行为在所有环境完全一致。这是 dnx 最打动我的地方——它把“人需要在环境上做很多准备”这一层抹掉了。我在实际项目里已经把一整个团队的 .NET 工具链迁移到了这套模式上运行了大半年下来全局工具相关的环境问题和 CI 里工具缺失导致的报错降到了零。要说唯一需要适应的大概就是工会里老同事记忆里的 dnx 怎么突然又回来了。但用过几次之后他们都改口说这回的 dnx是真的符合这个时代了。