码农刚子 · 六年制造业 ERP / .NET 实战 · 只写生产环境踩过的坑

2026 年 11 月 10 日,是条躲不过的死线。

这一天之后,.NET 8 和 .NET 9 都终止官方支持,不再有安全补丁。你线上跑着的 ERP 接口、后台任务、内部工具,等于在公网里"裸奔"——漏洞不再有人修,合规审计一查一个准。

但你一定也遇到过这种冲动:既然都要升,今晚就改个版本号发上去吧?

据一线实践,一拍脑袋直接升 .NET 10 的项目,"升级当天线上就崩"的概率,反而比"拖着不升"更高。升级从来不是把 net8.0 改成 net10.0 那么一行的事。下面这几道坑,是我在制造业 ERP 系统上真刀真枪升过、崩过、才总结出来的。

一、先看清:你跳过的不是一个版本

把版本支持周期摆出来,就明白为什么这次不能无脑升:

版本 类型 支持终止
.NET 8 LTS 2026-11-10
.NET 9 STS 2026-11-10
.NET 10 LTS 2028-11-10

从 .NET 8 直接跳 .NET 10,中间隔了一个 .NET 9。这意味着你跨了两个大版本,累积的 breaking change 比 8→9 那一跳要多得多。微软的官方迁移文档也是按 8→9、9→10 分别给的,没有"一步到位"的捷径。

换句话说:.NET 10 是 LTS,能稳到 2028 年,方向没错;但过程必须按"跨两代"的复杂度来对待。

二、升级前必做的 4 项检查(自检表)

动代码之前,先花半小时把下面四件事过一遍,能挡掉八成的升级事故。

① 第三方依赖有没有 net10.0 / netstandard2.1 兼容包

这是最容易翻车的地方。你项目里引用的组件,往往不是微软官方那几个,而是社区或商业库。重点盯这三类:

  • UI 组件库(如 Radzen.Blazor 这类)
  • 轻量 ORM(如 Dapper 及其扩展)
  • 旧版 SDK、内部打包的私有 NuGet

⚠ 经验表明:不少库在 .NET 大版本发布后几周内才跟上 net10.0 包。升之前先去 NuGet 上看一眼目标版本支持的 TargetFramework。

② 项目文件里的 TargetFramework 及相关开关

不只是改 TFM,这几个现有设置要心里有数:

<PropertyGroup>
  <TargetFramework>net10.0</TargetFramework>
  <LangVersion>latest</LangVersion>
  <ImplicitUsings>enable</ImplicitUsings>
  <Nullable>enable</Nullable>
</PropertyGroup>

LangVersion、ImplicitUsings、Nullable 的当前值,决定了你老代码升上去后会不会"看起来一样、实际已变"。

③ 扫描已 Obsolete / 已移除的 API

最典型的是 BinaryFormatter——它在 .NET 9 起被彻底移除,升级后直接编译不过。你代码里但凡有 BinaryFormatter.Serialize / Deserialize,要提前换成 System.Text.Json 或 MemoryPack 之类。

可以用 Roslyn 分析器或 dotnet list package --deprecated 辅助扫描,别靠肉眼。

④ 全球化 / 编码的默认行为

容器化部署(docker 镜像)里,.NET 默认开启 Invariant Globalization,也就是全球化不变模式。结果是:中文排序、本地化资源、ToString() 的某些格式,可能和你开发机上的表现悄悄不一样。

如果你的系统有中文字段排序、按区域设置格式化日期/数字的逻辑,升级前务必在容器里实测一遍。

三、生产环境真实踩过的坑

下面几个坑,都是"本地能跑、一上生产就炸"的典型。

坑1:依赖库主版本没出 net10 包,build 直接红

我见过最尴尬的情况:核心业务依赖的某个组件,官方还没发布兼容 .NET 10 的版本。CI 一拉包就报错,编译根本过不去。

两种兜底:要么等官方更新,要么在依赖项目里临时加 net10.0 到 TargetFrameworks 做多目标编译(前提是它的代码本身兼容)。但多 target 只是临时止血,不是长久方案,上线前要记到技术债清单里。

坑2:ImplicitUsings 开启后,老代码"重复 using"

ImplicitUsings 是 .NET 6 起就有的隐式命名空间机制。升到新版本后,如果旧代码里保留了手写 using System;、using System.Linq;,而全局又自动引入了同名命名空间,编译器会报 CS0105(命名空间重复)。

更隐蔽的是:新增的隐式命名空间,可能和你自建的类或扩展方法重名,编译期不报错,运行期行为却变了。建议升级时统一清理冗余 using,并打开 TreatWarningsAsErrors 让隐患暴露。

坑3:System.Text.Json 默认行为变化,前端解析崩

多数情况下,跨大版本后 System.Text.Json 的默认行为(命名策略、空值处理、保留引用、数字处理等)会有微调。后果很直接:接口返回的字段大小写或结构变了,前端按老字段名解析,拿到 undefined,页面白屏。

✓ 落地经验:别依赖默认,序列化选项显式声明最稳:

var options = new JsonSerializerOptions
{
    PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
    DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
};

升级后把前后端契约过一遍契约测试,比事后救火便宜得多。

坑4:开了 trimming / AOT 的项目,反射失效

⚠ 注意:这个坑只影响显式开了 <PublishAot>true</PublishAot> 或 trimming 的项目,普通项目不受影响。

一旦启用 AOT,反射、运行时按字符串 Type.GetType、动态 JSON 映射到 object、依赖运行期类型发现的库,都会在发布后失效——本地 dotnet run 正常,一 dotnet publish 就炸。这类项目升级时,要把所有"靠反射运行时探活"的代码,改成显式 [JsonSerializable] 源生成或提前注册。

坑5(补充):EF Core 8→10 的可空引用与查询翻译

启用 Nullable 后,EF Core 对查询翻译更严格了。部分在 8 上"默默走客户端评估"的 LINQ,到 10 可能直接抛异常;可空引用警告若被当作错误(TreatWarningsAsErrors),编译也会卡住。建议升级后跑一遍完整查询回归,别只信单测绿。

四、安全落地 6 步清单

把上面这些消化掉,按这套流程走,基本能平稳过渡:

  1. 备份 + 开分支:数据库全量备份,代码拉 upgrade/net10 长分支,绝不主干直接改。
  2. 改 TFM:net8.0 → net10.0,顺手核对 LangVersion / ImplicitUsings / Nullable。
  3. 升 NuGet:把所有依赖升到支持 net10.0 的版本,删掉被移除的包(如老 BinaryFormatter 相关)。
  4. 扫弃用与警告:跑构建,把 Obsolete 警告、CS 警告当错误处理,逐个清零。
  5. 本地 build / test 全绿:单测、集成测、契约测全过,再谈发布。
  6. 灰度发布观察:先小流量灰度,盯错误率、延迟、内存,稳了再全量。

✓ 关键原则:先小流量灰度,再全量。制造业系统的高峰期(如月末结账、排产)一旦崩,影响的是真金白银的产线,没有回滚预案别碰。

五、写在最后

升级 .NET 10 不是赶时髦,是 2026-11-10 死线逼出来的必答题。坑都在这儿了,照着清单走,你能避开绝大多数线上事故。

如果这篇文章帮到了你,欢迎关注微信公众号「CSharp精选营」,后台回复 「避坑」 领取我整理的《.NET 生产环境避坑手册》(含完整升级检查表 + 命令速查)。

PCB / 制造业 ERP 垂直踩坑系列,我会持续更——只写生产环境里真踩过的坑,不整虚的。


本文为通用 .NET 升级方法论分享,不涉及任何企业专有业务细节。