System.Text.Json Source Generator 深度解析:为什么 Native AOT 不喜欢反射

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 定义了 MetadataSerialization 两种 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 注解,官方推荐改用接受 JsonTypeInfoJsonSerializerContext 的 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 工业设备项目的完整架构设计。

相关推荐
金士顿2 小时前
ASP.NET Core Native AOT 到底是什么?从 C#、IL、JIT 一直讲到 Linux ARM64 ELF
嵌入式·asp.net core
做运维的阿瑞2 小时前
Python 标准库汇总:分类速览与常用模块清单
linux·运维·python
百万蹄蹄向前冲2 小时前
密码都对Node.js却连不上Linux数据库
linux·mysql
彧azz2 小时前
Linux 网络编程学习总结
linux·网络·笔记·学习·面试
susplus3 小时前
【ARM 裸机开发 (IMX6ULL-mini)】GNU工具、Makefile 工程构建、链接脚本详解|C 语言点灯 + 蜂鸣器驱动
linux·arm·makefile·imx6ull
Shadow(⊙o⊙)3 小时前
Linux进阶知识1.0
linux·运维·服务器
>Andre<4 小时前
UFS5.0标准中文全译·卷一:范围、术语与架构
android·linux·嵌入式硬件
用户0510122572964 小时前
Linux下有关QT显示环境变量设置
linux·嵌入式
闲云自留地4 小时前
告别命令恐惧症,openEuler 基础操作 + 文本 + 网络管理实战
linux·运维·云计算