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

发布时间:2026-03-13 10:38  浏览量:1

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

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

在之前的版本中,我们的使用了基于反射的JSONConverterFactory动态生成转换器。在传统的 JIT 环境下,这套“全自动”逻辑跑得非常丝滑。但在 AOT 环境下,由于编译器会裁剪掉未被静态引用的代码,且禁用了运行时动态类型生成,这会导致程序直接崩溃并抛出NotSupportedException此外,为了解决不同项目对状态码字段命名(status或code)的偏好问题,本次更新也加入了自动兼容逻辑。

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

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

code / status 双向兼容

反序列化(Read)

:JSON 里出现status或code都能正确映射到MessageModel

序列化(Write)

:输出时可以按全局配置选择写成status或code

整体方案的关键点是:用一个可配置的“状态字段名”作为写出标准 + 自定义System.Text.JsonConverter 在读取时同时兼容两种字段名。2.1 写出字段名可配置:StatusFieldNameMessageModelStatusFieldName,用来配置“序列化时状态码字段的名字”。它的取值被严格限制为"status"核心代码如下(位于):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'");

}

实现细节:

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

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

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

优先读取 "status"

如果不存在或类型不匹配,再读取 "code"

最终统一写入 MessageModel

对应的核心代码在MessageModelJsonConverter: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;

}

{ "status": 0, "Msg": "ok", "data": {} }

都能正确解析为同一个对象。2.3 写入时按配置输出:status 或 code写出时的核心是:

字段名不写死

,而是从全局配置读取(也就是上面StatusFieldName最终写入的MessageModelStatusField.Name)。在中:writer.WriteStartObject;

writer.WriteNumber(MessageModelStatusField.Name, value.Status);

writer.WriteString("msg", value.Msg);

...

writer.WriteEndObject;

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

默认(不设置)输出 status

设置为 MessageModel后输出code

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

2.4 为什么用JsonConverterFactory:让泛型自动生效,并兼顾 AOTMessageModel是泛型类型,Converter 也对应是。为了让在遇到任意MessageModel[JsonConverter(typeof(MessageModelJsonConverterFactory))]

public record class MessageModelMessageModelJsonConverterFactory1) 判断是否是:public override bool CanConvert(Type typeToConvert)

{

return typeToConvert.IsGenericType &&

typeToConvert.GetGenericTypeDefinition == typeof(MessageModel);

}

2) 创建对应的泛型 Converter,并同时考虑

AOT

非 AOT

两条路径:

AOT 路径(推荐)

:通过Register预注册,避免运行时反射/动态创建

非 AOT 路径

:允许通过反射构造MessageModelJsonConverter(开发环境/普通 JIT 运行时很方便)

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

// 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。2.5 小结

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

协议兼容性强

:读取端同时接受status/code,不强迫上下游立刻统一

输出标准可控

:写出时通过StatusFieldName统一字段名,逐步推进规范化

泛型友好

:MessageModel不需要为每个T单独写 Converter 注册代码

AOT 可用

:提供预注册入口,避免在 AOT 环境中因反射/动态创建受限而不可用

在适配 AOT 时,很多开发者会困惑:

“我明明已经在 Context 里注册了模型,为什么还要手动 Register?”

这里我们可以用一个接地气的比喻来理解。

3.1 户口登记 vs 岗位培训

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

3.2 户口登记(JsonSerializable这相当于给你的 DTO 模型(如SummaryResponse)上户口。告诉编译器:“这个类是有用的,请保留它的属性结构。”如果没有这一步,序列化器连这个类有几个字段都不知道。3.3 岗位培训(Register这是针对类库自定义转换逻辑的。在 AOT 下,编译器无法在运行时临时变出一个处理SummaryResponse的转换器代码。通过MessageModelJsonConverterFactory.Register,你实际上是在给转换器做“岗前培训”。显式告诉编译器:“请为这个类型专门编译一套处理逻辑。”在 AOT 模式下,你需要从“全自动”切换为“显式声明”。首先定义你的JsonSerializerContext:[JsonSerializable(typeof(MessageModel

[JsonSerializable(typeof(SummaryResponse))]

[JsonSerializable(typeof(LoginRequest))]

[JsonSerializable(typeof(UserConfigWrapper))]

internal partial class WebAppAotJsonContext : JsonSerializerContext { }

4.2 初始化配置在Program.csbuilder.Services.ConfigureHttpJsonOptions(options =>

{

// 1. 设置元数据解析器(户口登记)

options.SerializerOptions.TypeInfoResolver = WebAppAotJsonContext.Default;

// 2. 注册业务模型到工厂(岗位培训)

MessageModelJsonConverterFactory.Register

;

// 3. 添加转换器

options.SerializerOptions.Converters.Add(new MessageModelJsonConverterFactory);

});

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

❌ 错误写法:

// 直接崩溃:Reflection-based serialization has been disabled

var json = JsonSerializer.Serialize(myObject);

✅ 正确写法:

// 必须显式递交“准入证”(TypeInfo)

var json = JsonSerializer.Serialize(

new UserConfigWrapper(configUser),

WebAppAotJsonContext.Default.UserConfigWrapper

);

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

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

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