适配 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,欢迎参考我仓库中的示例项目进行实践。