首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >适配 Native AOT:CommonLibraries 迎来重大更新

适配 Native AOT:CommonLibraries 迎来重大更新

作者头像
桑榆肖物
发布2026-04-14 16:47:11
发布2026-04-14 16:47:11
280
举报

本文主要介绍了 Sang.AspNetCore.CommonLibraries 的最新更新。为了拥抱 .NET 的 Native AOT 特性,我们对核心类库进行了重构,并新增了对 codestatus 字段的双向兼容支持,旨在性能与兼容性之间取得平衡。

1. 为什么要更新?

随着 .NET 开始大规模推广 Native AOT(本地提前编译),传统的依赖运行时反射(Reflection)的库在 AOT 环境下会遭遇“降维打击”。

在之前的版本中,我们的 MessageModel<T> 使用了基于反射的 JsonConverterFactory 动态生成转换器。在传统的 JIT 环境下,这套“全自动”逻辑跑得非常丝滑。但在 AOT 环境下,由于编译器会裁剪掉未被静态引用的代码,且禁用了运行时动态类型生成,这会导致程序直接崩溃并抛出 NotSupportedException

此外,为了解决不同项目对状态码字段命名(statuscode)的偏好问题,本次更新也加入了自动兼容逻辑。

项目开源地址:https://github.com/sangyuxiaowu/Sang.AspNetCore.CommonLibraries?wt.mc_id=DT-MVP-5005195

2. 新特性:code / status 双向兼容

为了满足不同团队/不同后端框架对“状态码字段名”的习惯差异,有人习惯用 status,也有人习惯用 code,我在 MessageModel<T> 上实现了 code / status 双向兼容

  • 反序列化(Read):JSON 里出现 statuscode 都能正确映射到 MessageModel<T>.Status
  • 序列化(Write):输出时可以按全局配置选择写成 statuscode

整体方案的关键点是:用一个可配置的“状态字段名”作为写出标准 + 自定义 System.Text.Json Converter 在读取时同时兼容两种字段名

2.1 写出字段名可配置:StatusFieldName

MessageModel<T> 提供了一个静态属性 StatusFieldName,用来配置“序列化时状态码字段的名字”。它的取值被严格限制为 "status""code",防止被配置成其它字段名导致协议不可控。

核心代码如下(位于 MessageModel.cs):

代码语言:javascript
复制
public static string StatusFieldName
{
    get => MessageModelStatusField.Name;
    set => MessageModelStatusField.Name = value is "status" or "code"
        ? value
        : throw new ArgumentException("StatusFieldName only support 'status' or 'code'");
}

实现细节:

  • 真实存储在内部静态类 MessageModelStatusField.Name 中(默认 "status");
  • 因为它是静态配置,所以它的效果是“全局生效”的:只要你设置一次,后续序列化都会按该字段名输出。

2.2 读取时双向兼容:同时识别 status / code

仅靠属性的 [JsonPropertyName("status")] 并不能做到“读取时两种字段名都兼容”,因为默认的 System.Text.Json 会严格按字段名映射。

因此我们为 MessageModel<T> 提供了自定义 Converter,在 Read(...) 中做兼容逻辑:

  • 优先读取 "status"
  • 如果不存在或类型不匹配,再读取 "code"
  • 最终统一写入 MessageModel<T>.Status

对应的核心代码在 MessageModelJsonConverter<T>.Read(...)

代码语言:javascript
复制
var status = 0;
if (root.TryGetProperty("status", out var statusElement) && statusElement.ValueKind == JsonValueKind.Number)
{
    status = statusElement.GetInt32();
}
else if (root.TryGetProperty("code", out var codeElement) && codeElement.ValueKind == JsonValueKind.Number)
{
    status = codeElement.GetInt32();
}

这样,无论上游返回:

代码语言:javascript
复制
{ "status": 0, "msg": "ok", "data": {} }

还是:

代码语言:javascript
复制
{ "code": 0, "msg": "ok", "data": {} }

都能正确解析为同一个 MessageModel<T> 对象。

2.3 写入时按配置输出:statuscode

写出时的核心是:字段名不写死,而是从全局配置读取(也就是上面 StatusFieldName 最终写入的 MessageModelStatusField.Name)。

MessageModelJsonConverter<T>.Write(...) 中:

代码语言:javascript
复制
writer.WriteStartObject();
writer.WriteNumber(MessageModelStatusField.Name, value.Status);
writer.WriteString("msg", value.Msg);
...
writer.WriteEndObject();

因此你可以通过以下方式控制输出字段名:

  • 默认(不设置)输出 status
  • 设置为 MessageModel<T>.StatusFieldName = "code" 后输出 code

这就实现了“写出时统一口径,读入时兼容多口径”。

2.4 为什么用 JsonConverterFactory:让泛型 MessageModel<T> 自动生效,并兼顾 AOT

MessageModel<T> 是泛型类型,Converter 也对应是 MessageModelJsonConverter<T>。为了让 System.Text.Json 在遇到任意 MessageModel<任意T> 时都能自动应用正确的 Converter,我们在类型上标注:

代码语言:javascript
复制
[JsonConverter(typeof(MessageModelJsonConverterFactory))]
public record class MessageModel<T>

MessageModelJsonConverterFactory 做两件事:

1) 判断是否是 MessageModel<>

代码语言:javascript
复制
public override bool CanConvert(Type typeToConvert)
{
    return typeToConvert.IsGenericType &&
           typeToConvert.GetGenericTypeDefinition() == typeof(MessageModel<>);
}

2) 创建对应的泛型 Converter,并同时考虑 AOT非 AOT 两条路径:

  • AOT 路径(推荐):通过 Register<T>() 预注册,避免运行时反射/动态创建
  • 非 AOT 路径:允许通过反射构造 MessageModelJsonConverter<T>(开发环境/普通 JIT 运行时很方便)

工厂的关键逻辑(简化理解)是:

代码语言:javascript
复制
// AOT:先取预注册的 Converter
if (Converters.TryGetValue(typeToConvert, out var converter))
{
    return converter;
}

// 非 AOT:用反射创建
var dataType = typeToConvert.GetGenericArguments()[0];
var converterType = typeof(MessageModelJsonConverter<>).MakeGenericType(dataType);
return (JsonConverter)Activator.CreateInstance(converterType)!;

如果处在 AOT 场景且没有预注册,会抛出更明确的异常提示你必须先 Register<T>()

2.5 小结

经过上面的处理,我们实现了以下收益,同时兼顾了性能与兼容性:

  • 协议兼容性强:读取端同时接受 status / code,不强迫上下游立刻统一
  • 输出标准可控:写出时通过 StatusFieldName 统一字段名,逐步推进规范化
  • 泛型友好MessageModel<T> 不需要为每个 T 单独写 Converter 注册代码
  • AOT 可用:提供预注册入口,避免在 AOT 环境中因反射/动态创建受限而不可用

3. 深度解析:AOT 模式下的“双重认证”

在适配 AOT 时,很多开发者会困惑:“我明明已经在 Context 里注册了模型,为什么还要手动 Register?” 这里我们可以用一个接地气的比喻来理解。

3.1 户口登记 vs 岗位培训

在 Native AOT 的世界里,编译器是一个极其严谨且“抠门”的管家。为了节省空间,他会清理掉所有看起来没用的代码。

3.2 户口登记(JsonSerializable

这相当于给你的 DTO 模型(如 SummaryResponse)上户口。告诉编译器:“这个类是有用的,请保留它的属性结构。”如果没有这一步,序列化器连这个类有几个字段都不知道。

3.3 岗位培训(Register<T>

这是针对类库自定义转换逻辑的。在 AOT 下,编译器无法在运行时临时变出一个处理 SummaryResponse 的转换器代码。通过 MessageModelJsonConverterFactory.Register<T>(),你实际上是在给转换器做“岗前培训”。显式告诉编译器:“请为这个类型专门编译一套处理逻辑。”

4. 使用方法与 AOT 避坑指南

4.1 显式注册元数据

在 AOT 模式下,你需要从“全自动”切换为“显式声明”。首先定义你的 JsonSerializerContext

代码语言:javascript
复制
[JsonSerializable(typeof(MessageModel<string>))]
[JsonSerializable(typeof(MessageModel<SummaryResponse>))]
[JsonSerializable(typeof(SummaryResponse))]
[JsonSerializable(typeof(LoginRequest))]
[JsonSerializable(typeof(UserConfigWrapper))]
internal partial class WebAppAotJsonContext : JsonSerializerContext { }

4.2 初始化配置

Program.cs 中,完成“户口登记”与“岗位培训”:

代码语言:javascript
复制
builder.Services.ConfigureHttpJsonOptions(options =>
{
    // 1. 设置元数据解析器(户口登记)
    options.SerializerOptions.TypeInfoResolver = WebAppAotJsonContext.Default;

    // 2. 注册业务模型到工厂(岗位培训)
    MessageModelJsonConverterFactory.Register<SummaryResponse>();
    MessageModelJsonConverterFactory.Register<string>();

    // 3. 添加转换器
    options.SerializerOptions.Converters.Add(new MessageModelJsonConverterFactory());
});

4.3 警惕“裸奔”的 JsonSerializer 调用

这是最容易踩坑的地方。在 AOT 环境下,如果你手动调用 JsonSerializer(例如写入本地配置文件),绝对不能使用单参数的重载版本,否则会因为尝试反射而报错。

❌ 错误写法:

代码语言:javascript
复制
// 直接崩溃:Reflection-based serialization has been disabled
var json = JsonSerializer.Serialize(myObject);

✅ 正确写法:

代码语言:javascript
复制
// 必须显式递交“准入证”(TypeInfo)
var json = JsonSerializer.Serialize(
    new UserConfigWrapper(configUser), 
    WebAppAotJsonContext.Default.UserConfigWrapper
);

5. 总结

Native AOT 是 .NET 发展的必然趋势。虽然它要求开发者从“反射驱动”转向“显式声明”,增加了一定的手动注册工作,但带来的极致启动速度和低内存占用是显著的。

在 AOT 的世界里,编译器不再允许“撞运气”的行为。Sang.AspNetCore.CommonLibraries 的这次更新,正是为了帮助开发者在享受 AOT 红利的同时,依然能保留优雅的一致性返回体验。

如果你正在尝试将应用迁移到 Native AOT,欢迎参考我仓库中的示例项目进行实践。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2026-03-12,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 桑榆肖物 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1. 为什么要更新?
  • 2. 新特性:code / status 双向兼容
    • 2.1 写出字段名可配置:StatusFieldName
    • 2.2 读取时双向兼容:同时识别 status / code
    • 2.3 写入时按配置输出:status 或 code
    • 2.4 为什么用 JsonConverterFactory:让泛型 MessageModel<T> 自动生效,并兼顾 AOT
    • 2.5 小结
  • 3. 深度解析:AOT 模式下的“双重认证”
    • 3.1 户口登记 vs 岗位培训
    • 3.2 户口登记(JsonSerializable)
    • 3.3 岗位培训(Register<T>)
  • 4. 使用方法与 AOT 避坑指南
    • 4.1 显式注册元数据
    • 4.2 初始化配置
    • 4.3 警惕“裸奔”的 JsonSerializer 调用
  • 5. 总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档