XiHan Framework 处于预览阶段

查看获取方式
跳转到内容

JSON 序列化与 Native AOT ​

这一章讲框架为什么要求源生成的序列化、哪些地方需要登记 JsonSerializerContext、怎样把上下文插进 HTTP 的解析链、缓存与领域事件对序列化器的要求、哪些包能以 Native AOT 发布,以及发布 AOT 应用的步骤。读完你能把一个用 Standard 组合的应用发布成单个原生可执行文件。前置知识是操作契约与配置模型与选项。

为什么需要 ​

Native AOT 把应用编译成原生代码:启动快、内存小、不带 JIT。代价是运行期不能再靠反射发现类型,JsonSerializer.Serialize(obj) 这种依赖反射元数据的调用会在裁剪后失败或在编译期报警告。只要应用里有一处反射序列化,AOT 发布就会失败或留下隐患。

框架的原语层、内核与全部抽象包都不使用运行期反射:服务显式注册,操作与仓储的登记代码由源生成器产出,配置按键显式读取,JSON 编解码只经源生成的类型元数据(JsonTypeInfo)。这些包的项目都设置了 IsAotCompatible,裁剪与 AOT 分析器的警告在构建时视为错误。应用要做的只有一件事:把与框架交换的类型登记到 JsonSerializerContext,并把上下文交到需要它的地方。框架不提供对象映射与序列化封装,序列化直接使用 System.Text.Json。

零反射的约束 ​

应用与框架交换的类型要在编译期登记到 JsonSerializerContext,登记方式按用途不同:

用途登记方式
操作的请求与响应项目里任一 JsonSerializerContext 上的 [JsonSerializable];Kernel.Generators 找不到时报 XHG002
领域事件映射成消息AddDomainEvent<T>(topic, type, jsonTypeInfo, subject) 的 JsonTypeInfo<T> 参数
共享缓存的值容器里注册带 TypeInfoResolver 的 JsonSerializerOptions,或直接注册 ICacheSerializer
最小 API 端点的返回值ConfigureHttpJsonOptions 把上下文插到解析链最前面
应用自己的选项绑定项目属性 EnableConfigurationBindingGenerator,或像框架一样按键读取

最小示例 ​

我们把一个带下单操作的应用发布成 Native AOT。先写序列化上下文,登记全部与外界交换的类型:

csharp
// ShopJsonContext.cs
using System.Text.Json.Serialization;

namespace Shop;

public sealed record PlaceOrderRequest(string Name, int Amount);

public sealed record OrderResponse(Guid Id, string Name, int Amount);

public sealed record OrderPlaced(Guid OrderId, DateTimeOffset At);

public sealed record Greeting(string Message);

[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase, PropertyNameCaseInsensitive = true, NumberHandling = JsonNumberHandling.AllowReadingFromString)]
[JsonSerializable(typeof(PlaceOrderRequest))]
[JsonSerializable(typeof(OrderResponse))]
[JsonSerializable(typeof(OrderPlaced))]
[JsonSerializable(typeof(Greeting))]
internal sealed partial class ShopJsonContext : JsonSerializerContext;

Program.cs 把上下文插到 HTTP JSON 解析链最前面,其余与普通应用一样:

csharp
// Program.cs
using Shop;
using XiHan.Framework.Hosting.AspNetCore;
using XiHan.Framework.Profiles.Standard;
using XiHan.Framework.Protocols.Http;
using XiHan.Framework.Protocols.Http.Routing;

var builder = WebApplication.CreateBuilder(args);

// 源生成的序列化上下文放在解析链最前面,端点返回的类型不走反射序列化
builder.Services.ConfigureHttpJsonOptions(options => options.SerializerOptions.TypeInfoResolverChain.Insert(0, ShopJsonContext.Default));

builder.AddXiHan(xihan => xihan.UseStandardProfile().UseFeature<HttpProtocolFeature>().UseFeature<OrdersFeature>());

var app = builder.Build();
app.UseXiHan();
app.MapXiHanHealth();
app.MapXiHanOperations();
app.MapGet("/greet", (string? name) => new Greeting($"Hello, {name ?? "XiHan"}!"));
app.Run();

项目文件声明 AOT 发布并打开最小 API 的请求委托生成器:

xml
<Project Sdk="Microsoft.NET.Sdk.Web">
  <PropertyGroup>
    <PublishAot>true</PublishAot>
    <EnableRequestDelegateGenerator>true</EnableRequestDelegateGenerator>
    <EnableConfigurationBindingGenerator>true</EnableConfigurationBindingGenerator>
  </PropertyGroup>
</Project>

发布:

bash
dotnet publish -c Release -o ./publish

./publish 里是一个原生可执行文件,没有 .dll。运行它,curl localhost:5000/greet?name=Ada 返回 {"message":"Hello, Ada!"},POST /orders 照常返回 201。构建期如果某个请求或响应类型漏登记,Kernel.Generators 报 XHG002 让编译失败;端点返回了未登记的类型,发布期会出现 IL2026 / IL3050 这类裁剪与 AOT 警告。

登记 JsonSerializerContext ​

示例选项的含义:属性名用 camelCase;读取不区分大小写;允许从字符串读数字,因为 HTTP 适配器把路由值与查询串(都是字符串)合并进请求对象。

  • Kernel.Generators 从当前项目的 JsonSerializerContext 取序列化元数据,并为每个操作生成请求与响应的 JSON Schema。引用的其他程序集里的上下文不参与解析:契约类型放在哪个项目,就在那个项目里登记。
  • 嵌套在请求或响应里的类型(集合元素、子对象)不必单独 [JsonSerializable],生成器会随根类型一起生成元数据。
  • 多个上下文可以共存,用 TypeInfoResolverChain 按顺序组合;同一类型登记在两个上下文里时按链上的顺序取第一个。

端点返回值与解析链 ​

操作契约的请求与响应由框架用登记的元数据编解码,不经 HTTP 的 JsonOptions。但 MapGet 这类最小 API 端点返回的对象由 ASP.NET Core 序列化,走 Microsoft.AspNetCore.Http.Json.JsonOptions 的解析链,缺省末尾是反射解析器。TypeInfoResolverChain.Insert(0, ShopJsonContext.Default) 把源生成的上下文放到最前面,端点返回的类型先在这里命中;AOT 发布时反射解析器不可用,没有命中的类型会抛出 NotSupportedException。EnableRequestDelegateGenerator 则让端点参数的绑定在编译期生成,去掉参数绑定里的反射。

领域事件与消息 ​

聚合记录的领域事件要映射成消息发布,登记时直接传源生成的 JsonTypeInfo<T>:

csharp
// 在特性的 Configure 里:OrderPlaced 发到 orders 主题,类型 orders.placed,subject 是订单
context.Services.AddDomainEvent<OrderPlaced>("orders", "orders.placed", ShopJsonContext.Default.OrderPlaced, static placed => $"order/{placed.OrderId:N}");

ShopJsonContext.Default.OrderPlaced 是生成器按 [JsonSerializable(typeof(OrderPlaced))] 生成的属性。消息信封的其他字段见领域事件与发件箱。

缓存序列化器 ​

跨进程缓存(Redis、Remote)把值序列化后存进字节存储,序列化器在 AddXiHan 装配时按以下顺序决定:

  1. 容器里已登记的 ICacheSerializer。
  2. 已登记的 JsonSerializerOptions:用它构造 JsonCacheSerializer。
  3. 两者都没有:非 AOT 的 Provider 退回 JsonCacheSerializer.CreateReflectionBased(),它标注了 RequiresUnreferencedCode / RequiresDynamicCode,AOT 下不可用。

JsonCacheSerializer 要求选项带 TypeInfoResolver(源生成上下文),没有时构造即抛 ArgumentException;上下文里没有登记的类型抛 NotSupportedException,不悄悄退回反射。AOT 应用必须走前两种方式,并且要在 AddXiHan 之前登记:

csharp
using System.Text.Json;
using XiHan.Framework.Caching.Abstractions.Serialization;

builder.Services.AddSingleton<ICacheSerializer>(new JsonCacheSerializer(new JsonSerializerOptions { TypeInfoResolver = ShopJsonContext.Default }));

内存缓存不序列化,所以只在内存缓存下跑过的代码换成 Redis 后,值类型第一次需要能被序列化;不在上下文里的类型会在那时抛出。缓存的用法见缓存读写。

包的 AOT 兼容性 ​

下表以各项目的 IsAotCompatible 设置为准,各包参考页的信息表也标出了这一项。

子系统兼容 Native AOT不兼容
基础Primitives、Kernel、Domain、全部 .Abstractions—
宿主与协议Hosting.AspNetCore、Hosting.CommandLine、Protocols.Http、Protocols.Grpc、Protocols.Mcp、Protocols.CloudEvents—
组合包Profiles.Minimal、Profiles.Standard、Profiles.Full—
持久化Persistence.InMemoryPersistence.EntityFrameworkCore(EF Core 运行期构建模型)、Persistence.SqlSugar(实体映射与表达式翻译基于反射)
缓存Caching.InMemory、Caching.HybridCaching.Redis(StackExchange.Redis 未声明 AOT 兼容)、Caching.Remote(无源生成序列化器时退回反射)
消息Messaging.InMemoryMessaging.RabbitMQ(RabbitMQ.Client 未声明 AOT 兼容)、Messaging.Kafka(librdkafka 本机库)
安全Security.InMemory、Security.Database、Security.Ip2RegionSecurity.Jwt、Security.OpenIdConnect(Microsoft.IdentityModel 未声明 AOT 兼容)
多租户、本地化、审计、调度、工作流全部 InMemory 与 Database 实现、Scheduling.Kubernetes—
对象存储ObjectStorage.FileSystemObjectStorage.S3(AWS SDK 请求编组基于反射)
配置与观测Configuration.Nacos、Observability.Host、Observability.OpenTelemetry—
AIAI.AbstractionsAI.OpenAI、AI.VectorData(SDK 未声明 AOT 兼容或基于反射映射)
插件—Plugins.Protocol、Plugins.InProcess、Plugins.OutOfProcess、Plugins.Wasm(protobuf 描述符反射、程序集加载或原生绑定)
工具链Kernel.Generators 与 Persistence.Generators 生成的代码生成器、分析器与 Cli 本身是编译期或工具组件,不随应用发布

Database 包本身兼容,但它们经仓储访问数据库,实际能否 AOT 取决于应用选中的持久化 Provider:当前 EF Core 与 SqlSugar Provider 都不兼容,Persistence.InMemory 兼容。Plugins.InProcess 是框架里唯一允许反射的地方(加载插件程序集),相关 API 标注了 RequiresDynamicCode。

发布 AOT 应用 ​

仓库的 framework/samples/SimpleApp 是 AOT 冒烟项目,项目文件里声明了 PublishAot,CI 每次都以 Native AOT 发布它。按它的写法:

  1. 只引用兼容 AOT 的包,持久化用 Persistence.InMemory 或自己实现兼容 AOT 的 Provider。
  2. 项目文件声明 PublishAot,并打开最小 API 的请求委托生成器与配置绑定生成器(见上面的项目文件)。PublishAot 要写在项目文件里,不要在命令行传 -p:PublishAot=true:命令行属性是全局属性,会一路传给引用的项目,引用了 netstandard2.0 的分析器或生成器项目(如 Kernel.Generators)时会报 NETSDK1207。
  3. 把源生成的上下文放到 HTTP JSON 解析链最前面。
  4. 共享缓存与领域事件按上文登记序列化器与 JsonTypeInfo。
  5. dotnet publish -c Release -o ./publish。

发布时出现的裁剪或 AOT 警告都要处理,不要用 NoWarn 压掉;它们通常指向某个未登记的序列化类型或引用了不兼容 AOT 的包。

常见问题 ​

不发布 AOT,还需要登记 JsonSerializerContext 吗 ​

需要。操作的请求与响应必须登记,否则 Kernel.Generators 报 XHG002,生成不出 Schema;这是 OpenAPI、gRPC 与 MCP 共用的契约元数据,与是否 AOT 无关。只有最小 API 端点返回值与缓存值在非 AOT 下可以退回反射。

为什么框架不提供对象映射 ​

映射库多数在运行期用反射或表达式树,与零反射冲突。实体与响应之间的转换写成显式的构造或静态方法,例如 new OrderResponse(order.Id, order.Name, order.Amount);需要大量映射时选用编译期生成代码的映射库,保持零反射。

发布时报 IL2026 或 IL3050 怎么办 ​

看警告指向的调用:多半是某个类型走了反射序列化(端点返回值没进解析链、缓存值没登记)或引用了不兼容 AOT 的包。登记类型或换 Provider,不要 NoWarn。

请求类型在另一个类库里,怎么登记 ​

Kernel.Generators 只解析当前项目里的 JsonSerializerContext。把上下文放在声明操作的项目里,[JsonSerializable] 可以引用其他程序集的类型。

怎样确认应用真的没有反射 ​

以 PublishAot 发布并运行一遍全部端点。框架自己的包在构建时就把 AOT 分析器的警告视为错误,应用项目也可以在项目文件里设 IsAotCompatible 让分析器在每次构建时检查。

相关包与下一步 ​

Released under The MIT License