System.Text.Json Source Generator 深度解析:为什么 Native AOT 不喜欢反射?
前言
前两篇我们已经从两个方向介绍了 Native AOT:
text
上一篇:
C#
↓
IL
↓
Native AOT
↓
ARM64 Native Code
以及:
text
Linux Native World:
Object
↓
Linker
↓
Sysroot
↓
glibc
↓
ELF
现在回到 ASP.NET Core 应用层。
当真正把一个普通 ASP.NET Core 项目改造成 Native AOT 项目时,很快会遇到一个非常典型的问题:
text
JSON Serialization
普通 ASP.NET Core 中,我们可能非常自然地写:
csharp
var json = JsonSerializer.Serialize(deviceStatus);
或者 Minimal API:
csharp
app.MapGet(
"/api/status",
() => new DeviceStatus(
"Running",
42.5));
在普通 JIT 环境中,这几乎不需要考虑任何额外问题。
但是进入:
text
Native AOT
+
Trimming
以后,事情开始发生变化。
因为 JSON 序列化必须回答一个问题:
DeviceStatus到底有哪些属性?这些属性怎么读取?怎么创建对象?怎么把 JSON 转换回来?
传统实现可以在运行时:
text
Reflection
↓
发现 Type
↓
发现 Property
↓
生成序列化 Metadata
↓
执行 JSON Serialization
而 Native AOT 更希望:
text
Build Time
↓
已经知道 Type
↓
提前生成 Metadata / Serialization Code
↓
Native Compile
这就是:
text
System.Text.Json Source Generator
存在的重要原因之一。
Microsoft 当前文档明确指出,System.Text.Json 默认通过运行时反射收集序列化 metadata,而 Source Generation 可以把这一步移动到编译阶段,从而减少运行时反射、改善启动时间和内存使用,并更适合 trimming 和 Native AOT。
一、先从最简单的 JSON 序列化开始
假设有一个类型:
csharp
public sealed class DeviceStatus
{
public string State { get; set; } = "";
public double Temperature { get; set; }
public bool Online { get; set; }
}
对象:
csharp
var status = new DeviceStatus
{
State = "Running",
Temperature = 42.5,
Online = true
};
然后:
csharp
var json =
JsonSerializer.Serialize(status);
得到:
json
{
"State": "Running",
"Temperature": 42.5,
"Online": true
}
代码非常简单。
但是问题来了。
二、JsonSerializer 怎么知道有哪些属性?
我们只调用了:
csharp
JsonSerializer.Serialize(status)
并没有告诉它:
text
State 是 string
Temperature 是 double
Online 是 bool
那么这些信息从哪里来?
传统方式就是:
text
Reflection
大致可以理解为:
text
DeviceStatus
│
▼
typeof(DeviceStatus)
│
▼
Reflection
│
├── Property: State
│
├── Property: Temperature
│
└── Property: Online
│
▼
Build JSON Contract
│
▼
Serialize
三、这里的 Contract 是什么意思
JSON Serializer 不能只知道:
text
这是 DeviceStatus
它还需要知道很多东西。
例如:
text
有哪些 Property?
Property Name 是什么?
Property Type 是什么?
可不可以读取?
可不可以写入?
有没有 JsonIgnore?
有没有 JsonPropertyName?
有没有 Converter?
Constructor 是什么?
Nullable 怎么处理?
Enum 怎么处理?
继承类型怎么处理?
这些信息组合起来,就形成一个类型的:
text
JSON Contract
或者:
text
Serialization Metadata
四、传统 Reflection 模式
普通运行时模式可以简化成:
text
第一次 Serialize<DeviceStatus>
│
▼
Reflection Scan
│
▼
Build Metadata
│
▼
Cache Metadata
│
▼
Serialize
以后再次:
text
Serialize<DeviceStatus>
通常就可以利用缓存:
text
Metadata Cache
而不是每一次都重新扫描。
Microsoft 的 System.Text.Json 文档也说明,reflection 模式会在某个类型第一次参与序列化或反序列化时收集并缓存 metadata。
五、JIT 环境为什么对此比较宽容
普通 .NET Runtime:
text
CoreCLR
+
JIT
+
完整 Runtime Metadata
应用在运行时可以:
csharp
typeof(DeviceStatus)
然后:
csharp
type.GetProperties()
甚至:
csharp
Activator.CreateInstance(type)
因为 Runtime 保留了大量:
text
Type Metadata
Method Metadata
Property Metadata
Constructor Metadata
动态能力。
所以运行时可以问:
这个 Type 有什么?
然后再决定:
我要怎么处理它。
六、Native AOT 的思维完全不同
Native AOT 更接近:
text
Build Time
│
▼
分析整个应用
│
▼
判断哪些代码会被使用
│
▼
Trim
│
▼
AOT Compile
│
▼
Native Executable
这里有一个核心目标:
不需要的东西尽量不要进入最终程序。
于是出现了一个矛盾。
七、Reflection 和 Trimming 为什么会冲突
假设:
csharp
public sealed class DeviceStatus
{
public string State { get; set; }
public double Temperature { get; set; }
}
表面看:
text
State
Temperature
可能没有任何显式访问。
编译器静态分析发现:
text
没有代码直接调用:
status.State
status.Temperature
但是运行时:
csharp
JsonSerializer.Serialize(status);
可能通过 Reflection 去读取这些 Property。
问题是:
Trimmer 怎么知道未来 Reflection 会使用哪些成员?
如果无法确定:
text
可能保留大量 Metadata
或者:
text
可能产生 trimming warning
甚至某些动态场景根本无法安全分析。
八、最典型的问题:Type 是运行时才决定的
例如:
csharp
Type? type =
Type.GetType(
configuration.TypeName);
然后:
csharp
object? obj =
Activator.CreateInstance(type!);
这里:
text
configuration.TypeName
可能来自:
text
JSON 配置
数据库
网络
用户输入
编译器根本不知道:
text
运行时最终是什么 Type。
于是:
text
Static Analysis
失去确定性。
九、Native AOT 真正"不喜欢"的不是 Reflection 三个字
严格来说:
text
Native AOT
并不是:
text
完全禁止 Reflection。
真正的问题是:
text
Unbounded Reflection
也就是:
编译阶段无法确定反射究竟会访问哪些代码和 Metadata。
ASP.NET Core Native AOT 文档明确指出,Native AOT publish 时会 trimming 未使用代码,因此应用不能依赖无边界运行时反射;推荐使用 Source Generator 来生成避免这类反射需求的代码。
十、Source Generator 的基本思路
既然运行时:
text
Reflection
不好分析,
那就:
不要等运行时再发现类型。
改成:
text
Build Time
就告诉编译器:
text
我需要 DeviceStatus。
例如:
csharp
[JsonSerializable(typeof(DeviceStatus))]
internal partial class AppJsonContext
: JsonSerializerContext
{
}
这相当于明确告诉 System.Text.Json:
text
DeviceStatus
需要 JSON 支持。
十一、然后 Source Generator 做什么
编译阶段:
text
Roslyn
│
▼
发现 JsonSerializable
│
▼
分析 DeviceStatus
│
├── State
├── Temperature
└── Online
│
▼
Generate C# Source
│
▼
正常参与 Compilation
也就是说:
Source Generator 不是运行时生成代码。
而是:
text
编译阶段生成新的 C# Source Code。
十二、完整链路发生了变化
Reflection 方式:
text
Runtime
│
▼
Reflection
│
▼
Discover Type
│
▼
Build Metadata
│
▼
Serialize
Source Generation:
text
Build Time
│
▼
Discover Type
│
▼
Generate Metadata Code
│
▼
Compile
│
▼
Runtime
│
▼
Use Generated Metadata
│
▼
Serialize
这就是最核心的变化。
十三、JsonSerializerContext 是什么
来看:
csharp
[JsonSerializable(typeof(DeviceStatus))]
internal partial class AppJsonContext
: JsonSerializerContext
{
}
这里最核心的是:
csharp
JsonSerializerContext
可以把它理解为:
一组提前生成好的 JSON 类型描述信息的入口。
例如:
text
AppJsonContext
│
├── DeviceStatus
├── DeviceConfiguration
├── NetworkStatus
└── SystemInformation
每一个类型都会对应:
text
JsonTypeInfo<T>
十四、为什么一定是 partial
注意:
csharp
internal partial class AppJsonContext
必须是:
text
partial
因为:
text
你写一部分
+
Source Generator 生成另一部分
最终组合成:
text
AppJsonContext
概念上:
text
AppJsonContext.cs
+
AppJsonContext.g.cs
↓
Compiler
↓
AppJsonContext
这就是 C#:
text
partial type
非常典型的用途。
十五、JsonSerializableAttribute 做了什么
例如:
csharp
[JsonSerializable(typeof(DeviceStatus))]
它的作用就是注册:
text
DeviceStatus
告诉 Source Generator:
text
这个 Type 需要 JSON Contract。
多个类型:
csharp
[JsonSerializable(typeof(DeviceStatus))]
[JsonSerializable(typeof(DeviceConfiguration))]
[JsonSerializable(typeof(NetworkStatus))]
[JsonSerializable(typeof(SystemInformation))]
internal partial class AppJsonContext
: JsonSerializerContext
{
}
于是:
text
AppJsonContext
包含这些类型的 JSON Metadata。
十六、JsonTypeInfo 是什么
假设:
csharp
AppJsonContext.Default.DeviceStatus
它本质上提供:
text
JsonTypeInfo<DeviceStatus>
可以把:
text
JsonTypeInfo<T>
理解成:
System.Text.Json 对类型 T 的序列化契约描述。
里面描述类似:
text
Type
Properties
Converters
Constructor
Polymorphism
Number Handling
Object Creation
这些信息不需要运行时重新 Reflection 得到。
十七、于是可以这样序列化
传统:
csharp
var json =
JsonSerializer.Serialize(status);
Source Generation:
csharp
var json =
JsonSerializer.Serialize(
status,
AppJsonContext.Default.DeviceStatus);
这里:
text
AppJsonContext.Default.DeviceStatus
已经明确告诉 JsonSerializer:
text
应该使用哪个 JsonTypeInfo。
Microsoft 当前官方示例也是通过生成的 Context Default 实例取得相应 JsonTypeInfo<T>,再传给 JsonSerializer。
十八、反序列化也是一样
例如 JSON:
json
{
"State": "Running",
"Temperature": 42.5,
"Online": true
}
可以:
csharp
DeviceStatus? status =
JsonSerializer.Deserialize(
json,
AppJsonContext.Default.DeviceStatus);
这里也不需要:
text
运行时重新发现 DeviceStatus Contract。
十九、Source Generation 不只是为了 Native AOT
这是另一个重要认识。
Source Generator 的优势包括:
text
减少运行时 Reflection
减少 Runtime Metadata 创建
降低启动开销
降低部分内存开销
更利于 Trimming
更适合 Native AOT
所以即使:
text
JIT Application
也可以使用。
Microsoft 当前资料把启动时间、private memory、trim 安全性等都列为 Source Generation 相比 reflection 模式的重要收益。
二十、System.Text.Json Source Generation 有两种主要模式
这里开始进入真正容易混淆的地方。
Source Generator 不是只有一种生成方式。
主要有:
text
Metadata Mode
和:
text
Serialization Optimization Mode
也经常叫:
text
Fast Path
当前 JsonSourceGenerationMode 定义了 Metadata 和 Serialization 两种 flag;默认模式会同时生成 metadata 初始化逻辑和优化序列化逻辑。
二十一、Metadata Mode 是什么
Metadata Mode:
text
Build Time
│
▼
分析 Type
│
▼
Generate Json Metadata
│
▼
Runtime JsonSerializer
│
▼
根据 Metadata Serialize / Deserialize
也就是说:
Source Generator 负责提前建立 Contract。
但真正执行 JSON 的还是通用:
text
JsonSerializer
引擎。
二十二、Metadata Mode 的意义
传统 Reflection:
text
Runtime
↓
Reflection
↓
Build Metadata
↓
JsonSerializer
Metadata Source Generation:
text
Compile Time
↓
Build Metadata
↓
Runtime
↓
JsonSerializer
真正省掉的是:
text
Runtime Reflection Metadata Discovery
因此 Metadata Mode 同时适用于:
text
Serialization
+
Deserialization
Microsoft 当前文档明确说明,Metadata Mode 把 metadata 收集移动到编译阶段,可以同时改善 serialization 和 deserialization 的启动性能。
二十三、Serialization Optimization Mode 又是什么
这个模式更进一步。
不只是生成:
text
Metadata
而是直接生成:
text
专门针对某个 Type 的序列化代码。
例如概念上:
csharp
writer.WriteStartObject();
writer.WriteString(
"state",
value.State);
writer.WriteNumber(
"temperature",
value.Temperature);
writer.WriteBoolean(
"online",
value.Online);
writer.WriteEndObject();
这类代码直接调用:
text
Utf8JsonWriter
避免部分通用序列化路径。
二十四、Fast Path 为什么快
普通 Generic Serializer:
text
Type Metadata
↓
遍历 Property
↓
判断 Converter
↓
读取 Value
↓
Write JSON
Fast Path:
text
Generated DeviceStatus Serializer
↓
直接读取 State
↓
直接 WriteString
↓
直接读取 Temperature
↓
直接 WriteNumber
因此:
text
Generic Dispatch
Metadata Traversal
等运行时开销可以减少。
Microsoft 文档将这种模式称为 serialization-optimization/fast-path,它直接生成使用 Utf8JsonWriter 的优化序列化逻辑。
二十五、Fast Path 目前不是万能的
特别注意:
text
Serialization Optimization
只针对:
text
Serialization
当前没有对应的:
text
Fast-path Deserialization
官方文档明确指出 fast path 不支持任何形式的反序列化;纯 fast-path 模式也不支持一般意义上的异步 serialization path。
所以不能理解成:
text
Fast Path
=
所有 JSON 操作都更快。
二十六、为什么默认通常同时生成两种
如果没有指定:
csharp
GenerationMode
当前默认:
text
Default
会生成:
text
Metadata
+
Serialization Fast Path
也就是:
text
需要 Fast Path 时可以走 Fast Path
不能走 Fast Path 时仍然可以回退到 Metadata
这往往是更加实用的模式。
二十七、只生成 Metadata
可以:
csharp
[JsonSourceGenerationOptions(
GenerationMode =
JsonSourceGenerationMode.Metadata)]
[JsonSerializable(typeof(DeviceStatus))]
internal partial class AppJsonContext
: JsonSerializerContext
{
}
这时重点是:
text
AOT Compatibility
+
No Runtime Reflection
而不是追求:
text
Fast Path Serialization
二十八、只生成 Serialization Fast Path
例如:
csharp
[JsonSourceGenerationOptions(
GenerationMode =
JsonSourceGenerationMode.Serialization)]
[JsonSerializable(typeof(DeviceStatus))]
internal partial class AppJsonContext
: JsonSerializerContext
{
}
但是需要特别谨慎。
因为 Fast Path:
text
不支持所有 JsonSerializer 功能
如果某些 option 或 attribute 需要 fallback,而你又没有生成 Metadata:
text
就可能无法正常 fallback。
官方文档也明确建议:如果需要支持 fallback,应包含 Metadata Mode。
因此普通设备 Web API:
一般没有必要为了"理论最快"而强行只使用 Serialization Mode。
二十九、推荐默认策略
对于多数:
text
ASP.NET Core
+
Minimal API
+
Native AOT
项目,可以先:
csharp
[JsonSerializable(typeof(DeviceStatus))]
[JsonSerializable(typeof(DeviceConfiguration))]
internal partial class AppJsonContext
: JsonSerializerContext
{
}
保持默认:
text
Metadata
+
Fast Path
然后再根据:
text
Binary Size
Performance
Feature Compatibility
实际测量决定是否调整。
三十、JsonSourceGenerationOptions 是什么
除了:
csharp
[JsonSerializable]
还有:
csharp
[JsonSourceGenerationOptions]
例如:
csharp
[JsonSourceGenerationOptions(
PropertyNamingPolicy =
JsonKnownNamingPolicy.CamelCase,
WriteIndented = false)]
[JsonSerializable(typeof(DeviceStatus))]
internal partial class AppJsonContext
: JsonSerializerContext
{
}
这样:
csharp
DeviceStatus.State
JSON 就可以生成:
json
{
"state": "Running"
}
而不是:
json
{
"State": "Running"
}
三十一、为什么把配置放在 Attribute 上有优势
传统:
csharp
var options =
new JsonSerializerOptions
{
PropertyNamingPolicy =
JsonNamingPolicy.CamelCase
};
这是:
text
Runtime Configuration
而:
csharp
[JsonSourceGenerationOptions(
PropertyNamingPolicy =
JsonKnownNamingPolicy.CamelCase)]
属于:
text
Compile-Time Configuration
这样 Source Generator 在生成代码的时候已经知道:
text
命名策略是什么。
.NET 8 及以后,大量 JsonSerializerOptions 常用设置也可以直接通过 JsonSourceGenerationOptionsAttribute 配置。
三十二、ASP.NET Core Minimal API 怎么注册 Context
一个典型 Native AOT Minimal API:
csharp
using System.Text.Json.Serialization;
var builder =
WebApplication.CreateSlimBuilder(args);
builder.Services.ConfigureHttpJsonOptions(
options =>
{
options.SerializerOptions
.TypeInfoResolverChain
.Insert(
0,
AppJsonContext.Default);
});
var app = builder.Build();
app.MapGet(
"/api/status",
() =>
new DeviceStatus(
"Running",
42.5,
true));
app.Run();
public sealed record DeviceStatus(
string State,
double Temperature,
bool Online);
[JsonSerializable(typeof(DeviceStatus))]
internal partial class AppJsonContext
: JsonSerializerContext
{
}
这里最关键的是:
csharp
TypeInfoResolverChain.Insert(
0,
AppJsonContext.Default);
ASP.NET Core 当前 Native AOT 文档同样采用这种方式把 JsonSerializerContext 注册到 Minimal API 的 HTTP JSON options 中,并要求 HTTP body/response 中涉及的类型包含在 Context 中。
三十三、TypeInfoResolver 是什么
先理解:
text
JsonSerializer
在序列化某个 Type 时,需要:
text
JsonTypeInfo
于是它需要一个组件回答:
给你
DeviceStatus,对应的 JsonTypeInfo 是什么?
负责回答这个问题的就是:
text
IJsonTypeInfoResolver
可以抽象成:
text
Type
↓
Resolver
↓
JsonTypeInfo
三十四、JsonSerializerContext 本身也是 Resolver
Source-generated:
text
AppJsonContext.Default
本质上可以承担:
text
TypeInfo Resolver
职责。
因此:
text
DeviceStatus Type
↓
AppJsonContext
↓
JsonTypeInfo<DeviceStatus>
所以它可以放进:
text
TypeInfoResolverChain
三十五、什么叫 Resolver Chain
假设一个项目比较大:
text
DeviceJsonContext
ConfigurationJsonContext
DiagnosticsJsonContext
没必要一定全部塞进一个巨大 Context。
可以:
csharp
options.SerializerOptions
.TypeInfoResolverChain
.Add(DeviceJsonContext.Default);
options.SerializerOptions
.TypeInfoResolverChain
.Add(ConfigurationJsonContext.Default);
options.SerializerOptions
.TypeInfoResolverChain
.Add(DiagnosticsJsonContext.Default);
于是:
text
Type
↓
Resolver 1
↓
找不到?
↓
Resolver 2
↓
找不到?
↓
Resolver 3
直到找到相应 Contract。
从 .NET 8 开始,TypeInfoResolverChain 可以直接 prepend/append resolver,而且 resolver 顺序是有意义的:第一个返回非 null 类型信息的 resolver 生效。
三十六、为什么推荐按领域拆 Context
小项目:
csharp
AppJsonContext
完全够。
但是项目逐渐变大以后:
text
Status
Configuration
Firmware
Network
Diagnostics
System
可以设计:
text
Serialization
│
├── StatusJsonContext
├── ConfigurationJsonContext
├── SystemJsonContext
└── DiagnosticsJsonContext
好处:
text
类型边界明确
文件不会越来越巨大
领域职责清晰
方便定位遗漏类型
但也不要过度拆分。
三十七、Minimal API 最容易踩的坑:漏注册 Response Type
例如:
csharp
app.MapGet(
"/api/status",
() =>
new DeviceStatus(
"Running",
42.5,
true));
但是 Context 里只有:
csharp
[JsonSerializable(
typeof(DeviceConfiguration))]
没有:
csharp
DeviceStatus
JIT Debug 环境可能让你产生一种错觉:
text
怎么运行得好好的?
而 Native AOT publish 后可能:
text
JSON metadata 不存在
从而出现异常。
所以在 Native AOT Minimal API 中:
API Body 和 Response 中出现的所有 JSON 类型,都应该有明确的序列化契约。
ASP.NET Core Native AOT 文档也明确要求 Minimal API HTTP body/response 使用的类型配置在已注册的 JsonSerializerContext 中。
三十八、为什么 JIT 下没问题,AOT 下出问题
这是很多开发者第一次做 AOT 时最困惑的事情。
开发环境:
bash
dotnet run
通常是:
text
CoreCLR
+
JIT
+
Reflection
所以:
text
漏了 JsonSerializable
可能仍然通过 Reflection 正常工作。
然后:
bash
dotnet publish -r linux-arm64
Native AOT:
text
Trim
+
Static Analysis
+
AOT
运行时不再允许你随意依赖默认 reflection fallback。
于是问题才真正暴露。
三十九、最好主动关闭 Reflection Fallback
这是一个很好的 Native AOT 开发策略。
目标是:
不要等部署到 AOT Binary 才发现某处偷偷用了 Reflection JSON。
System.Text.Json 官方文档专门建议在希望严格验证 Source Generation 的场景中禁用 reflection-based defaults,这样误用 reflection serializer 时会更早抛出明确异常。
这样可以把:
text
生产环境问题
尽量变成:
text
开发阶段问题。
四十、不要到处 new JsonSerializerOptions
例如:
csharp
var options =
new JsonSerializerOptions();
var json =
JsonSerializer.Serialize(
status,
options);
如果这个:
text
options
没有:
text
TypeInfoResolver
你可能又重新进入:
text
Reflection-based Contract Resolution
在 Native AOT 项目里,这种代码必须特别警惕。
四十一、更明确的写法
例如:
csharp
var json =
JsonSerializer.Serialize(
status,
AppJsonContext
.Default
.DeviceStatus);
这种代码非常明确:
text
不会去猜
不会动态发现
直接使用指定 Contract
对于基础设施层代码,这往往比:
csharp
Serialize<T>(value)
更加容易审计。
四十二、集合类型也需要考虑
例如:
csharp
List<DeviceStatus>
不要只想到:
csharp
[JsonSerializable(typeof(DeviceStatus))]
实际 API 可能返回:
csharp
DeviceStatus[]
或者:
csharp
List<DeviceStatus>
这也是具体类型。
例如:
csharp
[JsonSerializable(typeof(DeviceStatus))]
[JsonSerializable(typeof(DeviceStatus[]))]
[JsonSerializable(typeof(List<DeviceStatus>))]
internal partial class AppJsonContext
: JsonSerializerContext
{
}
具体注册哪些集合形式,要根据真实 API Contract 决定。
四十三、请求 DTO 和响应 DTO 建议明确
例如不要让 API 直接接受:
text
整个 Domain Object
推荐:
csharp
public sealed record UpdateDeviceRequest(
string Name,
int SampleInterval);
public sealed record DeviceStatusResponse(
string State,
double Temperature,
DateTimeOffset UpdatedAt);
Context:
csharp
[JsonSerializable(
typeof(UpdateDeviceRequest))]
[JsonSerializable(
typeof(DeviceStatusResponse))]
internal partial class ApiJsonContext
: JsonSerializerContext
{
}
这样:
text
API Contract
和:
text
Internal Domain Model
分离。
四十四、这种方式对 AOT 反而更友好
显式 DTO 本来就是良好的 API 设计。
Native AOT 又进一步鼓励:
text
显式 Type
显式 Contract
有限动态行为
因此:
text
DTO
+
Source Generation
非常自然。
结构:
text
HTTP JSON
↓
Request DTO
↓
Application Service
↓
Domain
↓
Response DTO
↓
HTTP JSON
而不是:
text
HTTP
↓
随便 Serialize 整个业务对象
四十五、Enum 怎么处理
例如:
csharp
public enum DeviceState
{
Initializing,
Running,
Error
}
默认可能产生数字:
json
{
"state": 1
}
Web API 通常更希望:
json
{
"state": "Running"
}
在 Source Generation 中可以配置字符串 Enum。
例如:
csharp
[JsonSourceGenerationOptions(
UseStringEnumConverter = true)]
[JsonSerializable(typeof(DeviceStatus))]
internal partial class AppJsonContext
: JsonSerializerContext
{
}
当前 System.Text.Json Source Generation 支持通过 JsonSourceGenerationOptions 设置统一的 enum string policy。
四十六、或者使用泛型 JsonStringEnumConverter
对于特定 Enum:
csharp
[JsonConverter(
typeof(
JsonStringEnumConverter<DeviceState>))]
public enum DeviceState
{
Initializing,
Running,
Error
}
泛型 Converter 比旧式:
csharp
JsonStringEnumConverter
在 AOT 语境下通常更明确,因为具体 Enum Type 在编译阶段可知。
四十七、自定义 Converter 怎么办
例如:
csharp
public sealed class DeviceIdConverter
: JsonConverter<DeviceId>
{
...
}
可以正常使用,但仍然需要确保:
text
Converter 本身 AOT Compatible。
也就是不要在 Converter 内部重新:
text
无限 Reflection
动态创建 Type
Reflection.Emit
否则:
text
外层 Source Generation
解决不了:
text
内部动态行为。
四十八、Fast Path 和 Converter 要特别注意
Fast Path 并不支持所有:
text
JsonSerializerOptions
Attribute
Converter
组合。
如果当前配置无法使用 Fast Path,Serializer 可以:
text
Fallback to Metadata Mode
前提是:
text
你同时生成了 Metadata。
官方兼容表中明确列出了若干 fast-path 不支持的 customization,例如部分 converter 和 attribute 场景。
因此再次说明:
默认同时生成 Metadata + Serialization 通常更加稳妥。
四十九、Source Generator 的真正优势不是"少写代码"
很多 Source Generator 技术给人的感觉是:
text
帮我少写 Boilerplate。
但是 System.Text.Json 这里更重要的意义其实是:
text
Runtime Discovery
↓
Compile-Time Knowledge
也就是把:
text
动态
转成:
text
静态。
这和 Native AOT 的整体哲学完全一致。
五十、Native AOT 的核心思想其实一直相同
前面我们看到:
text
JIT
↓
AOT
是:
text
运行时编译
↓
编译时编译
现在 JSON 又是:
text
Runtime Reflection
↓
Compile-Time Source Generation
再比如 Regex 也可以:
text
Runtime Regex Compilation
↓
GeneratedRegex
整体趋势都是:
能在 Build Time 确定的事情,就尽量不要拖到 Runtime。
五十一、这就是 AOT-friendly Design
所谓:
text
AOT Friendly
并不只是:
text
项目能 publish 成功。
真正的设计倾向是:
text
Explicit Type
Explicit Contract
Static Registration
Source Generation
Known Dependency
Known Execution Path
而减少:
text
Runtime Discovery
Assembly Scanning
Reflection
Dynamic Loading
Runtime Code Generation
五十二、如何查看 Source Generator 到底生成了什么
这是学习 Source Generator 非常好的方法。
在 .csproj:
xml
<PropertyGroup>
<EmitCompilerGeneratedFiles>
true
</EmitCompilerGeneratedFiles>
</PropertyGroup>
然后:
bash
dotnet build
ASP.NET Core Native AOT 官方文档同样推荐用 EmitCompilerGeneratedFiles 查看 Source Generator 实际生成的代码。
五十三、生成代码在哪里
通常会出现在:
text
obj/
Debug/
net10.0/
generated/
具体目录结构可能因 SDK 和 Generator 而变化。
你会看到 Source Generator 生成:
text
*.g.cs
相关文件。
五十四、为什么我非常推荐看一次生成代码
因为看完以后:
text
Source Generator
就不再是黑盒。
你会发现它本质上就是:
text
读取你的类型定义
↓
生成普通 C#
↓
继续交给 Roslyn 编译
不是:
text
神秘 Runtime 技术。
五十五、Source Generator 在整个编译链的位置
完整:
text
C# Source
│
▼
Roslyn Compilation
│
├── Analyzer
│
└── Source Generator
│
▼
Generated C# Source
│
▼
Roslyn Compile
│
▼
IL
│
▼
Native AOT
│
▼
Native Code
也就是说 Source Generator 发生在:
text
Native AOT 之前。
五十六、它和 Native AOT Compiler 不是同一个东西
这是一个很容易混淆的地方。
text
Source Generator
作用:
text
生成更多 C# Source。
而:
text
Native AOT Compiler
作用:
text
IL
↓
Native Machine Code
完整链:
text
你的 C#
│
▼
Source Generator
│
▼
更多 C#
│
▼
Roslyn
│
▼
IL
│
▼
Native AOT Compiler
│
▼
ARM64 Machine Code
两个阶段完全不同。
五十七、为什么 Source Generator 对启动性能有帮助
Reflection 模式第一次遇到 Type:
text
Application Started
↓
First JSON Request
↓
Reflection
↓
Build Contract
↓
Cache
↓
Serialize
Source Generation:
text
Application Started
↓
First JSON Request
↓
Generated Contract
↓
Serialize
因此:
text
First Use Cost
降低。
对设备软件这种:
text
CPU 资源有限
启动阶段希望稳定
的环境尤其有意义。
五十八、为什么可能减少内存
Reflection 模式需要运行时:
text
Discover Metadata
↓
Create Metadata Objects
↓
Cache
Source Generation 可以提前生成相应描述。
因此:
text
运行时动态 metadata 初始化工作
可以减少。
这也是官方文档将降低 private memory usage 列为 Source Generation 收益之一的原因。
五十九、为什么它有利于 Binary Trimming
Trimmer 最喜欢:
text
显式调用关系。
例如:
text
A → B → C
非常容易分析。
最难的是:
text
A
↓
string typeName
↓
Reflection
↓
???
Source Generator 把:
text
???
变成:
text
Generated Code
↓
DeviceStatus
↓
Property A
↓
Property B
调用关系重新变得:
text
静态可见。
因此更适合:
text
Trimming。
六十、AOT Warning 和 JSON
如果 Native AOT publish 中看到:
text
IL2026
通常与:
text
RequiresUnreferencedCode
相关。
意味着:
这个代码路径可能依赖无法被 Trimmer 完整分析的成员。
如果看到:
text
IL3050
通常与:
text
RequiresDynamicCode
相关。
意味着:
这段代码可能需要运行时动态代码能力,而 Native AOT 无法保证支持。
System.Text.Json 的某些 reflection-based API 本身就带有这类 AOT/trimming 注解,官方推荐改用接受 JsonTypeInfo 或 JsonSerializerContext 的 overload。
六十一、不要用"Suppress Warning"解决第一反应
看到:
text
IL2026
IL3050
不要第一反应:
text
SuppressMessage
NoWarn
应该先问:
text
为什么这里需要 Reflection?
是否有 Source Generator API?
能不能显式注册 Type?
是不是第三方库不支持 AOT?
因为 AOT Warning 与普通:
text
unused variable
不同。
它很可能是在告诉你:
text
Native AOT Runtime Behavior
可能发生变化。
六十二、推荐项目结构
例如:
text
DeviceWeb
│
├── Program.cs
│
├── Api
│ ├── StatusEndpoints.cs
│ └── ConfigurationEndpoints.cs
│
├── Contracts
│ ├── DeviceStatusResponse.cs
│ └── UpdateConfigurationRequest.cs
│
├── Serialization
│ └── AppJsonContext.cs
│
├── Services
│
└── Infrastructure
其中:
text
Serialization
专门放:
text
JSON Source Generation Contract
非常清晰。
六十三、AppJsonContext 示例
csharp
using System.Text.Json.Serialization;
namespace DeviceWeb.Serialization;
[JsonSourceGenerationOptions(
PropertyNamingPolicy =
JsonKnownNamingPolicy.CamelCase,
UseStringEnumConverter = true)]
[JsonSerializable(
typeof(DeviceStatusResponse))]
[JsonSerializable(
typeof(UpdateConfigurationRequest))]
[JsonSerializable(
typeof(SystemInformationResponse))]
internal partial class AppJsonContext
: JsonSerializerContext
{
}
这样 JSON Policy 也集中管理。
六十四、Program.cs
csharp
var builder =
WebApplication.CreateSlimBuilder(args);
builder.Services.ConfigureHttpJsonOptions(
options =>
{
options.SerializerOptions
.TypeInfoResolverChain
.Insert(
0,
AppJsonContext.Default);
});
var app = builder.Build();
app.MapDeviceEndpoints();
app.Run();
Program.cs 并不需要知道:
text
每个 Property 如何序列化。
它只知道:
text
我们的 API JSON Contract
由 AppJsonContext 提供。
六十五、Endpoint
例如:
csharp
public static class DeviceEndpoints
{
public static void MapDeviceEndpoints(
this WebApplication app)
{
app.MapGet(
"/api/status",
(
DeviceStatusService service) =>
{
var snapshot =
service.GetSnapshot();
return new DeviceStatusResponse(
snapshot.State,
snapshot.Temperature,
snapshot.UpdatedAt);
});
}
}
这里:
text
Domain Snapshot
转换成:
text
API Response DTO
再交给:
text
System.Text.Json
六十六、最终运行链路
整个 HTTP JSON 返回过程:
text
Browser
│
│ GET /api/status
▼
Kestrel
│
▼
Minimal API Endpoint
│
▼
DeviceStatusService
│
▼
DeviceStatusResponse
│
▼
JsonSerializer
│
▼
TypeInfoResolverChain
│
▼
AppJsonContext
│
▼
JsonTypeInfo<DeviceStatusResponse>
│
▼
Generated Serialization Code
│
▼
UTF-8 JSON
│
▼
HTTP Response
整个过程不需要:
text
运行时临时 Reflection 扫描 DeviceStatusResponse。
六十七、反序列化请求链
POST:
text
Browser
│
│ JSON
▼
Kestrel
│
▼
System.Text.Json
│
▼
TypeInfoResolver
│
▼
AppJsonContext
│
▼
JsonTypeInfo<UpdateConfigurationRequest>
│
▼
UpdateConfigurationRequest
│
▼
Endpoint
这就是:
text
Native AOT Friendly JSON Pipeline。
六十八、Source Generator 不是 DTO 自动生成器
注意不要混淆。
text
Source Generator
不会替你设计:
text
DeviceStatusResponse
这个 DTO 仍然是:
csharp
public sealed record DeviceStatusResponse(...)
Source Generator 生成的是:
text
如何序列化 DeviceStatusResponse
相关代码和 Metadata。
所以:
text
Domain Design
仍然属于开发人员职责。
六十九、Source Generation 也不能解决所有 AOT 问题
例如一个第三方库:
text
运行时扫描所有 Assembly
自动发现 Plugin
Reflection.Emit 动态生成 Proxy
即使你的 JSON 已经:
text
Source Generated
这个库本身仍然可能:
text
AOT Incompatible。
所以:
text
System.Text.Json Source Generation
只是整个 AOT Compatibility 的:
text
一个重要组成部分。
七十、整个思维模型
把整个问题完整串起来:
text
普通 JIT
──────────────────────────
C#
↓
IL
↓
JIT Runtime
↓
JSON Request
↓
Reflection
↓
Build Metadata
↓
Serialize
Native AOT
──────────────────────────
C#
↓
Json Source Generator
↓
Generated Metadata / Code
↓
IL
↓
Native AOT
↓
Native Executable
JSON Request
↓
Generated JsonTypeInfo
↓
Serialize
这就是两个模型最大的区别。
七十一、几个最容易犯的错误
错误 1:以为 Native AOT 完全不能 Reflection
不准确。
更准确:
text
Native AOT 不适合
无法静态分析的无限动态 Reflection。
错误 2:认为 Source Generator 只是性能优化
不准确。
性能只是收益之一。
在 Native AOT 中更重要的是:
text
把 Runtime Discovery
变成
Build-Time Knowledge。
错误 3:只注册 Model,却漏注册 API 实际集合类型
例如实际返回:
text
DeviceStatus[]
却只考虑:
text
DeviceStatus
要根据真实 API Contract 检查。
错误 4:Debug 正常就认为 AOT 一定正常
错误。
必须测试真正:
text
dotnet publish
后的:
text
Native AOT Binary。
ASP.NET Core 官方也建议频繁执行 AOT publish,尽早发现 analyzer 和 trimming/AOT compatibility 问题。
错误 5:为了性能只开启 Serialization Mode
如果你的序列化选项需要:
text
Metadata Fallback
纯 Fast Path 可能反而产生兼容问题。
错误 6:出现 IL2026/IL3050 就直接 suppress
应该先找到真正:
text
Reflection / Dynamic Code
来源。
七十二、对于设备端 ASP.NET Core,我的推荐
如果项目是:
text
ASP.NET Core Minimal API
+
Linux ARM64
+
Native AOT
+
工业设备 Web
建议从一开始就按照下面的方式设计。
API 使用明确 DTO
text
Request DTO
Response DTO
所有 JSON 类型显式注册
csharp
[JsonSerializable(...)]
使用一个或少量几个 JsonSerializerContext
不要让 Context 无限制增长,也不要拆得过碎。
默认同时生成 Metadata + Fast Path
优先稳定和功能完整。
在开发阶段就检查 AOT Warning
不要最后发布时才处理。
定期执行真正 Native AOT Publish
不要只:
bash
dotnet run
查看一次 generated source
真正理解 Source Generator 做了什么。
七十三、最重要的工程原则
如果整篇只记一个原则:
Native AOT 喜欢"编译阶段就知道答案"的代码。
JSON 也是一样。
传统模型:
text
运行到这里再看看这个 Type 是什么。
AOT-friendly 模型:
text
编译阶段已经明确告诉你这个 Type 是什么。
所以:
text
Reflection
逐渐变成:
text
Source Generation
不是偶然。
而是:
text
Native AOT Architecture
自然产生的结果。
总结
普通 System.Text.Json:
text
Object
↓
Runtime Reflection
↓
Type Metadata
↓
JsonSerializer
↓
JSON
而 Source Generation:
text
Build Time
↓
Analyze Type
↓
Generate JsonTypeInfo
↓
Generate Serialization Code
↓
Compile
↓
Native AOT
运行时:
text
Object
↓
Generated Contract
↓
JsonSerializer
↓
JSON
其中最核心的几个概念:
text
JsonSerializableAttribute
↓
声明需要支持的 Type
JsonSerializerContext
↓
生成类型 Contract 的入口
JsonTypeInfo<T>
↓
描述某个具体 Type 的 JSON Contract
IJsonTypeInfoResolver
↓
Type → JsonTypeInfo
TypeInfoResolverChain
↓
组合多个 Resolver
JsonSourceGenerationMode.Metadata
↓
提前生成 Metadata
JsonSourceGenerationMode.Serialization
↓
生成 Fast-Path Serialization Code
最终可以得到:
text
ASP.NET Core Minimal API
↓
Explicit DTO
↓
JsonSerializerContext
↓
Source Generated Contract
↓
Native AOT
这也是一套非常适合 Linux ARM64 工业设备的 JSON 架构。
因为设备软件通常具有:
text
API 类型有限
数据模型明确
功能固定
部署环境固定
强调启动速度
强调内存占用
强调长期稳定运行
这些特点天然适合:
text
Static Contract
+
Source Generation
+
Native AOT
这种设计方式。
从更高层看,Native AOT 带来的真正变化并不只是:
text
把 C# 编译成本机代码。
它实际上在推动整个应用架构从:
text
Runtime Dynamic Discovery
逐渐转向:
text
Compile-Time Explicit Knowledge。
这也是理解 Native AOT 最重要的一条主线。
下一篇
下一篇建议继续讲:
《ASP.NET Core Native AOT 项目应该怎么设计?从 CreateSlimBuilder、DI 到 Minimal API 的完整架构》
前面几篇我们已经分别把:
text
Kestrel
BackgroundService
Native AOT
ARM64 Cross Compile
System.Text.Json Source Generator
拆开了。
下一篇开始把这些东西重新组合起来,设计一个完整的:
text
Linux ARM64
+
ASP.NET Core
+
Minimal API
+
BackgroundService
+
Native AOT
工程架构。
重点讨论:
text
为什么使用 CreateSlimBuilder?
Program.cs 应该放什么?
Endpoint 应该怎么拆?
Service 生命周期怎么选?
Singleton / Scoped / Transient 到底怎么用?
BackgroundService 和 Runtime Service 如何协作?
API DTO 和 Domain Model 怎么隔离?
JsonSerializerContext 放在哪里?
Configuration 怎么组织?
Infrastructure 层怎么隔离 Linux / Hardware?
怎样避免 Program.cs 越写越大?
最终项目目录应该怎么设计?
下一篇就从"底层原理系列"正式进入:
ASP.NET Core Native AOT 工业设备项目的完整架构设计。