【.NET】System.Text.Json 源生成器:启动快、分配少的序列化

前言

System.Text.Json 从 .NET 6 开始提供源生成器(source generation)模式:编译时生成序列化代码,运行时不再做反射。对启动时间敏感的 CLI 工具、AOT 编译应用和高吞吐 API 服务都有明显收益。

本文讲源生成器的两种模式、与反射模式的差异和迁移要点,示例基于 .NET 8。

一、反射模式的隐性成本

默认的 JsonSerializer 反射模式在第一次序列化某个类型时构建元数据:扫描属性、生成委托、缓存起来。这带来几个问题:

  • 首次请求延迟尖峰,冷启动的服务尤其明显。
  • 反射元数据占用内存,类型多时可观。
  • Native AOT / trimming 场景反射不可用或易被裁剪。

源生成器把「构建元数据」这一步提前到编译期,运行时只剩直接调用。

二、两种模式

元数据提取模式:生成序列化元数据,替换运行时反射,普通 JIT 应用首选。序列化优化模式:进一步生成强类型序列化代码,路径更快但会增大二进制体积。

[JsonSourceGenerationOptions(WriteIndented = true)]
[JsonSerializable(typeof(Order))]
[JsonSerializable(typeof(List<Order>))]
internal partial class OrderJsonContext : JsonSerializerContext {{ }}

// 使用:传入 Context 类型
string json = JsonSerializer.Serialize(order, OrderJsonContext.Default.Order);
var parsed = JsonSerializer.Deserialize(json, OrderJsonContext.Default.Order);

关键点:所有要序列化的类型(包括集合元素类型、嵌套类型)都要用 JsonSerializable 声明,漏声明的类型在运行时抛 NotSupportedException——这个报错编译期就应发现,所以类型清单要随模型一起维护。

三、与 ASP.NET Core 集成

builder.Services.AddControllers().AddJsonOptions(options =>
{{
    // MVC 内部仍用反射模式;要全局启用源生成需要在最小 API 场景配合 TypedResults
}});

如实说明:MVC 控制器管线目前对源生成器的支持有限,收益最明显的场景是最小 API + TypedResults、手动序列化、信号协议。Web 项目里最实际的做法是:把频繁序列化的热点 DTO 单独走 JsonContext,其余保持默认,渐进迁移。

最小 API 返回 TypedResults.Ok(dto) 时会走框架的序列化配置;显式传 Context 可以吃到源生成收益:Results.Json(dto, OrderJsonContext.Default.Order)。

四、配置与反射模式的差异

源生成模式下的配置集中在 JsonSourceGenerationOptions 特性里:命名策略(camelCase)、缩进、默认值忽略、枚举转字符串等。部分运行时选项(如自定义 Converter 实例)不能直接用,要用 JsonConverter 特性挂在类型上。

能力 反射模式 源生成
任意类型直接序列化 支持 需要显式声明
运行时切换命名策略 JsonSerializerOptions 即时生效 编译期固定(可生成多个 Context)
启动延迟 首次序列化有尖峰 无
Native AOT 不支持 支持
二进制体积 最小 随类型数量增长

五、迁移要点

  • 先给热点 DTO 建 Context,跑全量序列化回归测试,确认输出和反射模式一致(默认设置下应当一致)。
  • 异步序列化(SerializeAsync)配合流式场景在源生成下同样可用。
  • 动态类型、object 属性、多态序列化在源生成下的限制更多,遇到时评估是否留在反射模式。
  • 版本升级 .NET 9/10 后留意生成器的行为变化,Context 类要重新编译而非沿用旧 DLL。
两个模式可以在同一个应用共存:热点类型用源生成,长尾类型继续反射,用不同的 JsonSerializerOptions 区分即可。

六、生产使用注意事项

  • 类型清单加进 CI 检查:新增 DTO 忘记声明时,单测应当在开发期就报错而不是线上。
  • 对比收益时分别测「冷启动首请求」和「稳态吞吐」,两个指标受益幅度不同。
  • 注意 Context 生成的代码对程序集内 partial class 的要求,跨程序集复用要把 Context 放在被序列化类型可达的位置。
  • 序列化选项变更(如加字段)后确认契约测试,特别是对外 API。

总结

System.Text.Json 源生成器把序列化的运行时反射成本换成编译期生成:启动更快、内存更省、AOT 可用,代价是类型要显式声明、运行时配置灵活性下降。

推荐的落地顺序是:新项目从第一个 DTO 就建 Context 养成习惯;存量项目先给启动敏感或高吞吐的热点类型接入,回归测试确认输出一致后逐步铺开。序列化是性能优化的第二站,先测再改。

© 版权声明
THE END
喜欢就支持一下吧
点赞9 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容