一台开发机上往往装着好几个版本的 .NET SDK,不同项目对 SDK 的要求又不一样——老项目必须用旧版 SDK 才能编译,新项目想用最新的。Windows 下管理和切换这些 SDK 版本,最可靠的方式是项目级的 global.json。本文整理具体做法,以及几个关于「切换 SDK 版本」的常见误解。
先明确一个前提:选择 SDK 版本和指定项目目标框架(TargetFramework)是两件独立的事。SDK 版本决定用哪一版 dotnet CLI 和 MSBuild 来构建,<TargetFramework> 决定代码编译出来跑在哪个运行时上。改 global.json 不会改变项目的目标框架。
一、用 global.json 指定 SDK 版本(推荐)
1. 基本用法
在项目根目录创建 global.json:
{
"sdk": {
"version": "6.0.400",
"rollForward": "disable"
}
}version 指定要使用的 SDK 版本,rollForward 控制找不到该版本时的行为。"rollForward": "disable" 表示严格锁定——只用这一版,不允许自动升级,找不到就直接报错。这对需要复现历史构建结果的项目很有用。
也可以用命令生成,不用手写:
dotnet new globaljson --sdk-version 8.0.302 --roll-forward latestFeatureglobal.json 允许写注释,C# 风格(//、/* */)和 JavaScript 风格都可以,所以上面那种带注释的写法是合法的。
2. version 的写法要求
version 必须是完整版本号,如 6.0.400、8.0.302。不支持 8、8.0、8.0.x 这类简写,也不支持通配符和版本范围。写成 8.0 会报 Version '8.0' is not valid for the 'sdk/version' value。
版本号的结构是 x.y.znn:
| 位置 | 含义 | 示例(8.0.302) |
|---|---|---|
x | 主版本 major | 8 |
y | 次版本 minor | 0 |
z | 功能带 feature band | 3 |
nn | 补丁版本 patch | 02 |
理解 feature band 很重要:8.0.302 和 8.0.400 的 feature band 不同,很多 rollForward 策略的滚动范围就是按这个层级划分的。
3. rollForward 完整取值
| 值 | 行为 |
|---|---|
disable | 不向前滚动,必须精确匹配指定的版本 |
patch | 使用指定版本;找不到则滚动到同一 feature band 内的最新 patch;仍找不到则失败。这是 global.json 的默认策略 |
feature | 使用指定 major/minor/feature band 下的最新 patch;找不到则升到同 major/minor 的下一个 feature band |
minor | 在 feature 基础上,还允许升到同一 major 下的下一个 minor |
major | 在 minor 基础上,还允许升到下一个 major |
latestPatch | 使用同 major/minor/feature band 中,版本号 ≥ 指定值的最新已安装 patch;找不到则失败 |
latestFeature | 使用同 major/minor 中,版本号 ≥ 指定值的最高 feature band + patch |
latestMinor | 使用同 major 下,版本号 ≥ 指定值的最高 minor + feature band + patch |
latestMajor | 使用版本号 ≥ 指定值的最高已安装 SDK |
两组策略的区别在于「下限」:patch/feature/minor/major 这一组是从指定版本开始逐级向上找,允许降级到同 feature band 内更低的 patch;而 latestXxx 这一组要求候选版本不低于指定值,只会向上取。
4. 默认策略是什么
这里容易记混,分两种情况:
- 没有 global.json,或文件里只写了
allowPrerelease没写version:使用本机已安装的最高版本 SDK,等价于latestMajor。 - 写了
version但没写rollForward:默认策略是patch,不是latestPatch。
如果希望「至少不低于某个版本,装了更新的就用更新的」,明确写 latestMajor 或 latestFeature:
{
"sdk": {
"version": "7.0.200",
"rollForward": "latestMajor"
}
}这段配置表示不允许使用低于 7.0.200 的 SDK,7.0.200 及以后的版本(包括 8.0.x)都可以用。CI 环境里通常用这种写法,既能保证下限,又不会因为构建机升级了 SDK 就失败。
5. allowPrerelease
控制是否允许选中预览版 SDK,取值 true/false。不显式设置时,默认值取决于调用方式:不在 Visual Studio 中运行时默认为 true;在 Visual Studio 中则看是否启用了「使用 .NET SDK 的预览版」选项。
{
"sdk": {
"allowPrerelease": false
}
}6. global.json 是怎么被找到的
.NET SDK 里有两个组件会去查找 global.json,它们的起点不一样,这是排查「命令行生效、Visual Studio 不生效」这类问题的关键:
- SDK muxer(处理
dotnetCLI 命令):从当前工作目录开始向上逐级查找。注意是当前目录,不一定等于项目目录——所以要确保你在项目的正确子目录下执行命令。 - MSBuild 项目 SDK 解析器(构建时解析项目):从解决方案文件(.sln)所在目录开始向上查找;如果没有 sln,则从当前项目文件所在目录开始;两者都没有则用当前工作目录。
因此,如果要让 Visual Studio 也遵循指定的 SDK 版本,global.json 应该放在 .sln 所在目录或它的上层目录。
二、用命令行查看版本
# 查看已安装的所有 SDK 版本
dotnet --list-sdks
# 查看当前生效的 SDK 版本
dotnet --version
# 查看详细信息:已安装的 SDK、运行时、以及 global.json 是否解析失败
dotnet --infodotnet --version 输出的是「在当前目录下实际生效的版本」,如果它和 global.json 里写的对不上,说明当前目录不在该文件的作用范围内,或者 rollForward 把版本滚动到了别的版本上。dotnet --info 会直接显示 SDK 解析失败的原因,排查时优先看它。
三、关于「切换版本」的几个常见误解
1. DOTNET_ROOT 不能用来切换 SDK 版本
常见的一种写法是:
set DOTNET_ROOT=C:\Progra~1\dotnet\sdk\6.0.400这有两处不对:
- 路径层级错了。
DOTNET_ROOT指向的是 .NET 的安装根目录,Windows 上默认是C:\Program Files\dotnet——也就是包含dotnet.exe、sdk\、shared\的那一层,而不是sdk\6.0.400这样的具体版本目录。 - 它管的不是 SDK。官方说明里
DOTNET_ROOT的作用是「当运行时不在默认位置时,指定运行时所在位置」,而且只在通过生成的 exe(apphost)启动应用时生效,用于让程序找到运行时。它不参与dotnetCLI 命令的 SDK 选择。
换句话说,设 DOTNET_ROOT 既不会改变 dotnet build 用的 SDK,路径写对了也一样。
2. SDK 版本的真正决定方式
要让某个命令用特定版本的 SDK,可靠的做法有两个:
# 方式一:在 global.json 生效的目录下执行,让解析器自己选
cd D:\projects\legacy-app
dotnet --version
# 方式二:如果装有多套独立的 .NET 根目录,直接调用目标那一套的 dotnet.exe
"C:\dotnet-sdk-6\dotnet.exe" --version方式二适用于「多个独立安装根目录」的场景(比如一套来自 Visual Studio,另一套来自独立安装包)。此时 PATH 里排在前面的 dotnet.exe 会被优先使用——这才是调整 PATH 顺序有意义的场景。
而如果是标准的单根目录安装(所有 SDK 都在 C:\Program Files\dotnet\sdk\ 下共存),各个版本目录本来就不在 PATH 里,调整 PATH 顺序没有任何作用,版本选择完全由 global.json 和 rollForward 决定。
3. Visual Studio 只保留一份 SDK
需要注意:Visual Studio 只会安装一份 .NET SDK。升级 Visual Studio 时,它会删掉旧版本 SDK 再装上新版本——即使新旧的 major 版本不同也一样会被删掉。
如果项目通过 global.json 锁定了某个旧版本 SDK,而这个版本恰好是被 VS 删掉的那一个,构建就会失败。规避办法是改用官方下载页提供的独立安装包安装该版本 SDK,这样不会受 VS 升级影响;代价是这个版本不会再随 VS 自动获得更新。
四、注意事项汇总
- 项目级配置优先于全局设置:
global.json只对它所处目录及其子目录生效,不同项目可以各放一份,互不影响,天然做到版本隔离。 - 多个 SDK 共存不会互相干扰:安装新版本 SDK 不会卸载旧版本,它们可以并存。
- 版本没装会直接报错:
rollForward为disable或latestXxx且找不到匹配版本时,构建会失败并提示找不到兼容的 SDK。用dotnet --list-sdks核对本机实际装了哪些。 - CI 环境建议用范围策略:写死
disable要求构建机上装有一模一样的版本;用latestFeature/latestMajor更省事,同时还能保证版本下限。 - 使用 package lock file 时,官方建议把
rollForward设为disable,让 SDK 版本和依赖图保持锁定。
五、常见问题
| 现象 | 原因与处理 |
|---|---|
dotnet --version 和 global.json 写的不一致 | 当前工作目录不在该文件作用范围内(muxer 从当前目录向上找),或 rollForward 滚动到了其他版本 |
报错 Version '8.0' is not valid for the 'sdk/version' value | version 必须写完整版本号,如 8.0.302 |
| 提示找不到兼容的 .NET SDK | 目标版本没安装,用 dotnet --list-sdks 确认后补装,或调整 rollForward |
| 命令行生效,Visual Studio 不生效 | 把 global.json 放到 .sln 所在目录或上层(MSBuild 从 sln 目录开始查找) |
| 升级 VS 后旧版本 SDK 消失了 | VS 只保留一份 SDK,升级时会删掉旧的。改用独立安装包安装需要长期保留的版本 |
设了 DOTNET_ROOT 但版本没变 | 该变量不参与 SDK 选择,且应指向安装根目录而非 sdk\版本号 目录 |
小结
Windows 下切换 .NET SDK 版本,日常只需要记住一件事:在项目里放 global.json。想严格锁定就写 disable,想留出升级余地就写 latestFeature 或 latestMajor。至于环境变量,DOTNET_ROOT 管的是运行时查找、只在 apphost 启动应用时生效,调整 PATH 顺序也只在装有多套独立根目录时才有意义——这两条都是常见的误区。
评论区