上周把开发机上的 Visual Studio 2022 从 17.2 推到 17.4,顺手把 .NET 7 SDK 一起装上,当时想的是“多一个目标框架而已,又不影响老项目”。结果第二天早上打开一个跑了大半年的 .NET 6 项目,Ctrl+Shift+B 直接红了一片,输出窗口第一条就是error NETSDK1045: 当前的 .NET SDK 不支持将 .NET 6.0 作为目标。这个报错最让人上火的地方在于——我明明刚装完新版 SDK,机器上 .NET 6 的运行时和 SDK 都还在,dotnet --list-sdks能列出来,可 VS 就是不认。
后来花了差不多一个下午,从 SDK 解析链路一路查到分析器、再翻到 VS2022 的升级日志,才把根因钉死。整个过程里有好几次判断是错的,走了两段弯路,但也正因为走了弯路,才把 NETSDK1045 这个错误的触发条件摸得比较透。下面把完整链路摊开写,包括我当时怎么想的、为什么这么查、哪些证据是决定性的。如果你也在做 SDK 版本升级、多目标框架迁移,或者单纯被 NETSDK1045 卡住过,这篇应该能帮你少绕两圈。
1. NETSDK1045 那条报错,值得逐字拆开读
大部分人看到 NETSDK1045 的第一反应是“SDK 版本不对”,然后去装 SDK。这个反应方向没错,但太粗。这条错误信息本身其实包含了三个独立的信息块,每一块指向的根因都不一样,拆开读能省掉一半的试错。
1.1 错误文本的三个组成部分
以我这次遇到的完整报错为例,实际文本是这样的:
error NETSDK1045: 当前的 .NET SDK 不支持将 .NET 6.0 作为目标。 请将 .NET 5.0 或更低版本作为目标,或者使用支持 .NET 6.0 的 .NET SDK 版本。拆成三块看:
- 第一块是“当前的 .NET SDK”——注意它说的是“当前的”,也就是 MSBuild 在评估阶段实际解析到的那个 SDK 实例,不是 PATH 里的第一个,也不是你心里以为的那个。
- 第二块是“不支持将 .NET 6.0 作为目标”——这是项目的
TargetFramework,来自项目文件或者被某个 props 改写后的最终值。 - 第三块是“请将 .NET 5.0 或更低版本作为目标”——这句最有用。它明确告诉你,被解析到的那个 SDK 认为自己的最高支持版本就是 5.0。也就是说,MSBuild 眼里的 SDK 版本号大概在 5.x 这一档。
这个推断非常关键。因为如果解析到的真是 7.0.100,它不可能不认识 net6.0;如果解析到的是 6.0.x,它认识 net6.0,也不会报这个错。只有当解析到的 SDK 版本低于目标框架版本时,才会触发这个判定。所以看到这句“请将 .NET 5.0 或更低版本作为目标”,基本可以断定:MSBuild 解析到的 SDK 是 5.x 或者更早的东西,而不是我新装的 7.0.100。
1.2 判定发生在哪一步:BundledVersions 与 CheckForUnsupportedNETCoreVersion
搞清楚判定逻辑,排查才有方向。这个检查不是运行时做的,是 MSBuild 在项目评估完成后、编译开始前做的。
具体机制是这样:每个 .NET SDK 的安装目录下都有一个Microsoft.NETCoreSdk.BundledVersions.props,里面用KnownFrameworkReference项列出了这个 SDK 认识的所有目标框架。6.0.x 的 SDK 里不会有 net7.0 的条目,7.0.x 的 SDK 里则同时有 net6.0 和 net7.0。当项目声明的TargetFramework在KnownFrameworkReference里找不到对应项时,Microsoft.NET.Sdk.targets会调用一个叫CheckForUnsupportedNETCoreVersion的构建任务,这个任务比对两个属性:
NETCoreSdkVersion:当前解析到的 SDK 版本TargetFrameworkVersion:项目声明的目标框架版本
比对逻辑很简单——目标版本高于 SDK 版本,就抛 NETSDK1045,并在报错里把 SDK 版本降一档给出建议。所以这条错误本质上是“版本倒挂”的信号,而不是“SDK 缺失”。这个区别很重要:SDK 缺失报的是另一条错(NETSDK1045 之外的Unable to locate the .NET SDK),方向完全不一样。
记住一句话:NETSDK1045 几乎永远不是“没装 SDK”,而是“装了一堆 SDK,但被解析到的那个版本不够”。
1.3 为什么升级 .NET 7 会反向波及 .NET 6 项目
这里有个反直觉的点,也是我当时最想不通的地方:我装的是 7.0,项目要的是 6.0,7.0 明明向下兼容 6.0,为什么会挂?
答案是:升级动作本身改变了 SDK 的解析结果,而不是改变了 SDK 的能力。安装 .NET 7 SDK 和升级 VS2022 这两个动作,至少会在三个地方留下痕迹:
- 安装程序可能清理掉旧的 SDK 目录(尤其是同 major 版本的早期 patch)。
- VS2022 升级会替换
MSBuild\Current\Bin下的整套 MSBuild 二进制,包括 SDK Resolver。 - 升级过程可能写入或修改环境变量,比如
MSBuildSDKsPath、DOTNET_ROOT。
这三件事单独看都是正常的升级行为,但组合起来就可能让 MSBuild 解析到一个“看起来还在、实际上版本很低”的 SDK 目录。比如某个残留的MSBuildSDKsPath指向了旧版 VS 自带的 SDK 副本,那个副本的BundledVersions.props停留在 5.0 时代,于是 net6.0 就成了“不支持的更高版本”。
我后来在升级日志里找到的正是这条线索,具体过程放在第 4 节。先把前面两轮排查讲清楚,因为那两轮虽然没直接定位,但排除掉了大量干扰项。
2. 第一层排查:版本声明链路到底断在哪一环
排查这种版本类问题,我的习惯是从“声明”查到“解析”,而不是反过来乱试。声明链路是:项目文件里的TargetFramework→Directory.Build.props等上层 props 的覆盖 →global.json的 SDK 版本约束 → 实际解析到的 SDK。这条链上任何一环出问题,都会表现成同一个报错。
2.1dotnet --info该看哪几行
第一步永远是dotnet --info,但很多人只看最上面那段版本号就结束了,其实下面几段信息量更大:
dotnet --info输出里要重点盯这几个字段:
| 字段 | 含义 | 本次排查中的作用 |
|---|---|---|
Version | 当前默认 SDK 版本 | 确认默认解析到的是哪个 |
.NET SDKs installed | 已安装的 SDK 列表 | 确认 6.0.x 是否还在 |
.NET runtimes installed | 已安装的运行时列表 | 运行时在不在不影响编译,但影响运行 |
Base Path | 当前 SDK 的物理路径 | 定位到具体目录,后面查文件用 |
global.json file | 生效的 global.json 路径 | 看有没有被上层目录的配置劫持 |
我当时的输出里,Version: 7.0.100,SDKs installed里列了6.0.404和7.0.100两个,global.json file显示的是解决方案根目录下的那个文件。从表面看一切正常——6.0.404 在,7.0.100 也在,凭什么解析不到 6.0?
这里就能看出dotnet --info的局限:它给的是dotnet CLI 视角的解析结果,而这个结果和VS 内部 MSBuild 视角的解析结果不一定一致。这就是下一节要讲的事。
2.2 global.json 的 rollForward 决定了“找不到时怎么办”
我的项目根目录确实有个global.json,是半年前为了统一团队 SDK 版本加的,内容大概是:
{ "sdk": { "version": "6.0.400" } }关键点在rollForward没写。没写的时候,默认行为是patch级别的滚动——也就是说,如果机器上有 6.0.400,直接用;没有 6.0.400 但同 feature band(6.0.4xx)下有别的 patch,可以向上滚;但如果 6.0.400 这个 feature band 整个消失,就只能报错。
而升级安装程序的行为往往是:装 7.0.100 的时候顺手把 6.0.400 清掉了,留下了 6.0.404。按默认 rollForward 策略,6.0.400 → 6.0.404 属于同一 feature band 内向上滚动,理论上应该能过。
但问题在于,rollForward的完整取值有disable、patch、feature、minor、major、latestPatch、latestFeature、latestMinor、latestMajor这么多种,不同 .NET SDK 版本对默认值的实现也有细微差别。我当时的做法是把global.json临时改名成global.json.bak,再编译一次:
mv global.json global.json.bak # 重新执行构建 dotnet build结果命令行编译成功了。这是一个非常重要的分水岭——它证明global.json 确实参与了问题,但命令行能过说明 SDK 本身没坏。于是问题被缩小到“为什么 VS 里过不去”。
2.3 命令行能过、VS 里过不去,差在哪
这个现象是本次排查的第一个转折点。同一个解决方案,dotnet build绿,VS 里红,说明两者走的不是同一条 SDK 解析路径。
差异主要来自三个地方:
第一,MSBuild 实例不同。dotnet build用的是 SDK 自带的 MSBuild(在sdk\<version>\MSBuild.dll里),而 VS 用的是Program Files\...\MSBuild\Current\Bin\MSBuild.exe。这两套 MSBuild 的 SDK Resolver 实现是同一个代码库,但加载的Microsoft.DotNet.MSBuildSdkResolver.dll版本可能不同。
第二,环境变量继承不同。VS 是常驻进程,很多环境变量是在启动 VS 那一刻快照下来的。你后面在系统属性里改了 PATH,已经开着的 VS 不会感知。我这次就是先开的 VS,后装的新 SDK。
第三,VS 有自己的一套 SDK 预检查机制。VS 在加载解决方案时会先做一次目标框架兼容性探测,如果探测失败,可能直接跳过编译,连详细日志都不给。
验证方法很直接——关掉 VS,从开始菜单重新打开(确保拿到最新环境变量),再编译。我这么做了,错误依旧。到这里,第一层排查的结论是:global.json 有影响但不是唯一原因,VS 端还有别的东西在干扰。
3. 第二层排查:分析器和构建属性这两个干扰项
排查到这一步,我开始怀疑项目本身。因为报错位置很奇怪——它出现在分析器加载之后,输出窗口里紧跟着一串CS8032: 无法从 ... 创建分析器实例的警告。当时第一反应是:是不是某个分析器包升级后改了行为,连带把 TargetFramework 的评估搞乱了?
3.1 分析器加载失败为什么容易被误判成 SDK 问题
CS8032这类警告和 NETSDK1045 在输出窗口里是挨着的,视觉上特别容易让人建立错误的因果联系。实际上两者的层级完全不同:
- NETSDK1045 发生在项目评估阶段,此时还没有任何 C# 代码被编译,分析器根本没启动。
- CS8032 发生在编译阶段,是 Roslyn 在加载分析器程序集时失败。
也就是说,分析器问题在时间线上晚于 SDK 判定问题,它不可能是 NETSDK1045 的因。但人眼看到“报错挨着警告”就会本能联想,我在这里确实浪费了差不多二十分钟。
验证手法很简单,用属性直接把分析器全关掉:
dotnet build /p:RunAnalyzers=false /p:RunAnalyzersDuringBuild=false也可以在项目文件里临时加一行:
<PropertyGroup> <RunAnalyzers>false</RunAnalyzers> <RunAnalyzersDuringBuild>false</RunAnalyzersDuringBuild> <EnableNETAnalyzers>false</EnableNETAnalyzers> </PropertyGroup>结果错误照旧。这就干净利落地排除了分析器这个方向。不过顺手记一下:那批 CS8032 是另一个独立问题,某个分析器包的 netstandard2.0 版本依赖了更高版本的System.Collections.Immutable,和 SDK 自带的 Roslyn 版本撞了。这个留到后面单独处理,和主线无关。
3.2 上层 props 文件偷偷改了 TargetFramework
排除分析器之后,下一个怀疑对象是所有会改写TargetFramework的地方。很多人只知道项目文件里那一行,实际上能改它的地方不少:
- 解决方案根目录的
Directory.Build.props - 项目目录的
Directory.Build.props(会覆盖上层) - 各级
Directory.Build.targets - NuGet 包里的
build/*.props和buildTransitive/*.props - 命令行传的
/p:TargetFramework=xxx
我这次是真踩到了一个。仓库根目录的Directory.Build.props里有这么一段:
<Project> <PropertyGroup Condition="'$(TargetFramework)' == ''"> <TargetFramework>net6.0</TargetFramework> </PropertyGroup> </Project>这段本身没问题,但它说明了TargetFramework可能来自上层。更隐蔽的是判断条件——Condition="'$(TargetFramework)' == ''",如果某个子项目先被别的 props 赋了一个空值或异常值,这里就会静默覆盖。
排查这类覆盖,最有效的手段是让 MSBuild 把“属性最终值”打出来。可以用预处理模式:
dotnet msbuild MyProject.csproj /pp:preprocessed.xml生成的preprocessed.xml是所有 props 和 targets 展开后的完整结果,搜索TargetFramework就能看到它被赋值了几次、每次在哪个文件、条件是什么。这个文件通常有几万行,但用编辑器的查找功能定位很快。
提示:
/pp输出的文件不要提交到仓库,它体积大且包含本机绝对路径,容易被误提交。
我查下来的结果是:TargetFramework最终值确实是net6.0,没有被改写。也就是说声明侧是干净的,问题百分之百在解析侧。
3.3MSBuildSDKsPath这个环境变量是隐藏杀手
声明侧排干净之后,我开始查环境变量。这里有一个特别容易被忽略的变量:MSBuildSDKsPath。
它的作用是强制指定 SDK 的根目录。正常情况下不该设置它,MSBuild 会自己去找。但有些老工具、老版本 VS 或者某些构建脚本会设置它,设完之后就一直留在系统环境变量里,新装的 SDK 完全不起作用。
查这个变量:
# PowerShell Get-ChildItem Env: | Where-Object { $_.Name -like "*MSBuild*" -or $_.Name -like "*DOTNET*" }# bash env | grep -iE "msbuild|dotnet"我当时的输出里,DOTNET_ROOT指向的是C:\Program Files\dotnet,正常。MSBuildSDKsPath没设置。倒是有一个DOTNET_MSBUILD_SDK_RESOLVER_SDKS_DIR,值指向一个老路径——这个变量是 SDK Resolver 的专用覆写入口,优先级比MSBuildSDKsPath还高。
这个发现让我一度以为找到答案了。把它删掉、重启 VS、重新编译——还是报同样的错。所以它也不是根因,或者至少不是唯一根因。
到这里,第二层排查基本把项目侧和环境变量侧的嫌疑都洗清了。剩下的唯一可能是:VS2022 升级过程本身动了什么。这就必须去看升级日志了。
4. 第三层排查:翻升级日志,找到 SDK 被替换的证据
说实话,之前我从来没认真看过 VS 的安装升级日志,觉得那是给支持工程师看的东西。这次被逼着翻了半个多小时,才发现里面信息密度相当高,尤其是和 SDK 组件相关的记录。
4.1 升级日志在哪、看什么
VS2022 的安装和升级日志主要有这么几个位置:
| 日志类型 | 典型路径 | 关注内容 |
|---|---|---|
| 引导程序日志 | %TEMP%\dd_bootstrapper_*.log | 升级前的环境检查、组件清单 |
| 安装引擎日志 | %TEMP%\dd_setup_*.log | 每个 MSI/包的实际安装、卸载动作 |
| 实例配置日志 | %ProgramData%\...\_Instances\<实例ID>\dd_setup_*.log | 组件启用/禁用状态变更 |
| 错误日志 | %TEMP%\dd_setup_*_errors.log | 失败项,优先看这个 |
按时间排序,找升级那天的文件。文件名里的时间戳格式是yyyymmddhhmmss,很好定位。
打开之后不要从头读,直接搜关键词。我搜的是sdk、dotnet、uninstall、remove,命中了几条关键记录:
Package 'Microsoft.NetCore.Sdk.6.0.400,version=...' was removed Package 'Microsoft.NetCore.Sdk.7.0.100,version=...' was installed这两条坐实了一个事实:升级过程确实卸载了 6.0.400 这个具体版本。这也就解释了为什么global.json里写的6.0.400会失效。
但按 rollForward 的逻辑,6.0.400 没了应该滚到 6.0.404 才对。继续往下翻,又找到一条:
Package 'Microsoft.NetCore.Sdk.6.0.404,version=...' state changed to 'Disabled'这条才是真正致命的。6.0.404 的包还在磁盘上,但状态被标记为 Disabled——意思是 VS 的实例不再“注册”这个 SDK 组件。而 VS 内部的 SDK Resolver 查询 SDK 列表时,走的是已注册组件这条通道,不是直接扫目录。于是它在自己的视图里只看到一个可用的 SDK:7.0.100。
那为什么 7.0.100 会报“不支持 net6.0”呢?因为还有一个前提没交代:这个项目是多目标的,项目文件里写的是:
<TargetFrameworks>net6.0;netstandard2.0</TargetFrameworks>而在多目标场景下,MSBuild 会为每个 TFM 分别求值一次。如果global.json里的版本约束导致解析失败,某些版本组合下会 fallback 到一个内置的兜底 SDK 描述符,那个描述符的版本信息是不完整的,NETCoreSdkVersion可能被算成一个低于目标的值。这就是报错里“请将 .NET 5.0 或更低版本作为目标”这句话的来源。
4.2MSBuild.exe和dotnet build的解析结果为什么不一样
到这里可以回答第 2.3 节留下的问题了。
dotnet build走的是 CLI 的 hostfxr,它解析 SDK 的方式是直接读C:\Program Files\dotnet\sdk目录下的物理文件夹,按版本号排序挑符合global.json约束的。6.0.404 的文件夹在,它就能选上,所以命令行能编过。
VS 的 MSBuild 走的是Microsoft.DotNet.MSBuildSdkResolver,在查询候选 SDK 时除了看目录,还要过一层VS 实例的组件注册表。被标记为 Disabled 的组件,在这条通道里是查不到的。这就是两边结果分叉的根本原因。
这个机制值得单独强调一下,因为它解释了一大类“命令行正常、IDE 报错”的诡异现象。判断方法很简单:如果命令行通过、IDE 不通过,优先怀疑组件注册状态,而不是 SDK 文件本身。
4.3 用二进制日志把解析结果钉死
日志推断归推断,还是要拿直接证据。MSBuild 的 binlog 是最可靠的手段:
dotnet build MySolution.sln -bl:build.binlog然后用 MSBuild Structured Log Viewer 打开(这是一个独立的开源查看器,不依赖 VS)。在里面的操作:
- 顶部搜索框输入
NETCoreSdkVersion,找到PropertyInitialValue或PropertyReassignment事件,看这个属性在构建全过程被赋值了几次。 - 搜
TargetFrameworkVersion,对比它和 SDK 版本的大小关系。 - 展开
CheckForUnsupportedNETCoreVersion这个 Task 节点,看它的输入参数。
我的 binlog 里,NETCoreSdkVersion在某次求值中显示为5.0.0,而项目目标框架是6.0,正好倒挂。这个5.0.0不是任何真实安装的 SDK 版本,它是 SDK Resolver 在解析失败后返回的一个占位值。
拿到这个证据,根因就彻底清楚了:升级过程把 6.0.404 的组件状态改成了 Disabled,导致 VS 侧的 Resolver 在按global.json约束查找时找不到可用的 6.0.x SDK,返回了兜底描述符,NETCoreSdkVersion被算成 5.0.0,触发 NETSDK1045。
5. 四种修复路径的对比与取舍
根因明确之后,修复方案就不止一种了。我列了个表,把能想到的路子都摆出来,对比之后再选。
5.1 方案对比表
| 方案 | 操作 | 优点 | 代价 | 适用场景 |
|---|---|---|---|---|
| 装回 6.0.4xx SDK | 重新运行 .NET 6 安装包 | 最快,改动最小 | 机器上 SDK 越堆越多,版本管理混乱 | 临时救火、单机项目 |
| 修改 global.json 的 rollForward | 加"rollForward": "latestFeature" | 不改环境,团队可同步 | 只解决版本约束问题,解决不了组件 Disabled | 团队协作、CI 一致性 |
| 在 VS Installer 里重新勾选组件 | 勾选 .NET 6 SDK 组件并修复 | 从根上恢复组件注册状态 | 需要重启 VS,耗时几分钟 | 确认是组件状态问题 |
| 升级项目 TFM 到 net7.0 | 改项目文件 | 顺势完成版本升级 | 涉及依赖包全面验证,风险大 | 有完整回归测试的项目 |
5.2 为什么最后选了“修复 VS 组件 + 放宽 rollForward”组合
单装 SDK 我没选,原因是治标不治本。目录里再放一个 6.0.4xx,如果 VS 那边组件状态还是 Disabled,Resolver 照样查不到,下次 VS 一更新又得重来。
直接升 TFM 到 net7.0 我也没选。这个项目依赖了三个内部包,其中两个的 nupkg 只发布了 net6.0 目标,强行升级要么改包要么加<NoWarn>硬扛,成本远高于收益。
最后我选的是组合拳:
第一步,在 VS Installer 里对这个实例点“修改”,找到“.NET 6.0 运行时”和“.NET SDK”相关组件,确认勾选状态,然后点“修复”。这一步会让 VS 重新扫描磁盘上的 SDK 目录并把 6.0.404 重新注册为可用。
第二步,把global.json的约束放宽,让它对轻微版本漂移有容忍度:
{ "sdk": { "version": "6.0.400", "rollForward": "latestFeature", "allowPrerelease": false } }latestFeature的含义是:在满足主版本号相同的前提下,允许向上滚动到更高的 feature band。这样 6.0.400 找不到时,可以滚到 6.0.4xx 里的任意版本,甚至 6.0.5xx、6.0.6xx。对内部项目来说这个容忍度是合理的,因为我们并没有依赖某个 SDK patch 的特定行为。
相比之下,patch(默认)只允许在同一 feature band 内滚,minor允许跨小版本但风险更大,disable就是完全锁死、一点不许动。选择哪个,取决于项目对 SDK 行为的敏感程度。
5.3 修复后的验证清单
改完之后我没有直接宣布完事,而是按这个清单逐项验证:
- 关掉所有 VS 进程,重新打开,确保环境变量和组件状态都是最新的。VS 常驻会导致组件注册信息不刷新,这一步不能省。
- 清理中间产物。
bin和obj里的缓存会保留上一次评估结果,不删可能看到假象。直接删obj目录最保险,因为它存了project.assets.json和一堆 props 缓存。 - 在 VS 里执行“重新生成解决方案”,不是“生成”。两者的区别在于“重新生成”会先清理。
- 命令行再跑一次
dotnet build,确认两边行为一致。 - 打一次 binlog,搜
NETCoreSdkVersion,确认它现在是6.0.404而不是5.0.0。
第 5 步是我认为最有价值的一步。前面的现象都可能是缓存导致的巧合,只有 binlog 里那个属性值变了,才算真正修好。
6. 复盘:这类升级事故以后怎么少踩
这次排查花了小半天,事后想想,真正耗费时间的不是技术难度,而是排查顺序。如果一开始就去翻升级日志,可能二十分钟就结束了。所以复盘的重点不在“学会了 NETSDK1045”,而在“下次怎么更快定位”。
6.1 SDK 版本要当成代码来管理
global.json这个东西,很多项目要么没有,要么写了但没人维护。我的建议是把它当成一份正式的配置文件纳入版本控制,并且写清楚滚动策略:
{ "sdk": { "version": "6.0.400", "rollForward": "latestFeature", "allowPrerelease": false } }三个字段各有作用:version是基准版本,rollForward定义了漂移容忍度,allowPrerelease防止在正式项目里误用预览版 SDK。前两个必须显式写,不要依赖默认值——默认值在不同 SDK 版本之间是有差异的,靠默认等于把行为交给了运气。
另外建议在仓库根目录放一个说明文件,写清楚“本项目最低需要 6.0.400 SDK,推荐 6.0.4xx 系列”,新同事入职照着装,能省掉大量沟通。
6.2 建立“命令行与 IDE 一致性”的检查习惯
这次最核心的教训是:命令行能编不代表 IDE 能编,反之亦然。这两个通道在 SDK 解析上有实质差异,不能互相替代验证。
我的做法是在项目里加一个简单的诊断脚本,出问题时先跑一遍,把关键信息一次性打出来:
echo "=== SDK 列表 ===" dotnet --list-sdks echo "=== 当前解析版本 ===" dotnet --version echo "=== global.json 位置 ===" dotnet --info | grep -i "global.json" echo "=== 环境变量 ===" env | grep -iE "MSBuildSDKsPath|DOTNET_ROOT|DOTNET_MSBUILD"四行命令,能在十秒内把第 1、2 节排查的内容全部展开。养成出问题先跑这个的习惯,比凭记忆瞎试效率高得多。
注意:
dotnet --version在项目目录下执行和在项目目录外执行,结果可能不同,因为前者会读取global.json。定位问题时一定要在项目目录下执行。
6.3 几条踩出来的土办法
第一,升级 VS 之前先记下dotnet --list-sdks的快照。升级之后对比一下,哪个 SDK 消失了、哪个版本号变了,一目了然。我这次就是没做快照,只能靠升级日志反推,多花了不少时间。
第二,VS 装完新 SDK 后强制重启一次。不是关窗口,是从任务管理器确认所有devenv.exe进程都退出。VS 有些环境变量和组件信息是进程启动时快照的,不重启拿不到新值。
第三,怀疑组件状态问题时,优先用 VS Installer 的“修复”而不是重装。修复会重新扫描并注册组件,耗时几分钟;重装动辄半小时起步,收益一样甚至更少。
第四,多目标项目出问题,先临时砍成单目标验证。把<TargetFrameworks>net6.0;netstandard2.0</TargetFrameworks>临时改成<TargetFramework>net6.0</TargetFramework>,如果错误消失,说明问题出在多目标求值的交叉环节,排查范围立刻缩小一半。验证完记得改回去。
第五,不要忽视obj目录的污染。它的project.assets.json里记录了上次还原时的 SDK 和目标框架信息,版本变化后这些信息会过期,但不会被自动清理。遇到“改了配置没生效”的情况,删obj永远是第一反应。
最后补充一个我这次才意识到的点:NETSDK1045 里的“请将 .NET X.0 或更低版本作为目标”这句话,给的版本号是推断值,不一定是真实安装的 SDK 版本。我最初看到“5.0”还去查机器上是不是有个 5.0 的 SDK,查了半天没有。后来才明白那是 Resolver 在解析失败后填充的兜底描述符里的常量。搞清楚这一点之后,再看这个报错就不会被带偏了——重点不是去核实那个版本号是否存在,而是去查为什么解析会失败。