本文主要介绍了
Sang.AspNetCore.CommonLibraries的最新更新。为了拥抱 .NET 的 Native AOT 特性,我们对核心类库进行了重构,并新增了对code与status字段的双向兼容支持,旨在性能与兼容性之间取得平衡。
随着 .NET 开始大规模推广 Native AOT(本地提前编译),传统的依赖运行时反射(Reflection)的库在 AOT 环境下会遭遇“降维打击”。
在之前的版本中,我们的 MessageModel<T> 使用了基于反射的 JsonConverterFactory 动态生成转换器。在传统的 JIT 环境下,这套“全自动”逻辑跑得非常丝滑。但在 AOT 环境下,由于编译器会裁剪掉未被静态引用的代码,且禁用了运行时动态类型生成,这会导致程序直接崩溃并抛出 NotSupportedException。
此外,为了解决不同项目对状态码字段命名(status 或 code)的偏好问题,本次更新也加入了自动兼容逻辑。
项目开源地址:https://github.com/sangyuxiaowu/Sang.AspNetCore.CommonLibraries?wt.mc_id=DT-MVP-5005195
为了满足不同团队/不同后端框架对“状态码字段名”的习惯差异,有人习惯用 status,也有人习惯用 code,我在 MessageModel<T> 上实现了 code / status 双向兼容:
status 或 code 都能正确映射到 MessageModel<T>.Statusstatus 或 code整体方案的关键点是:用一个可配置的“状态字段名”作为写出标准 + 自定义 System.Text.Json Converter 在读取时同时兼容两种字段名。
StatusFieldNameMessageModel<T> 提供了一个静态属性 StatusFieldName,用来配置“序列化时状态码字段的名字”。它的取值被严格限制为 "status" 或 "code",防止被配置成其它字段名导致协议不可控。
核心代码如下(位于 MessageModel.cs):
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");status / code仅靠属性的 [JsonPropertyName("status")] 并不能做到“读取时两种字段名都兼容”,因为默认的 System.Text.Json 会严格按字段名映射。
因此我们为 MessageModel<T> 提供了自定义 Converter,在 Read(...) 中做兼容逻辑:
"status""code"MessageModel<T>.Status对应的核心代码在 MessageModelJsonConverter<T>.Read(...):
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": {} }
还是:
{ "code": 0, "msg": "ok", "data": {} }
都能正确解析为同一个 MessageModel<T> 对象。
status 或 code写出时的核心是:字段名不写死,而是从全局配置读取(也就是上面 StatusFieldName 最终写入的 MessageModelStatusField.Name)。
在 MessageModelJsonConverter<T>.Write(...) 中:
writer.WriteStartObject();
writer.WriteNumber(MessageModelStatusField.Name, value.Status);
writer.WriteString("msg", value.Msg);
...
writer.WriteEndObject();
因此你可以通过以下方式控制输出字段名:
statusMessageModel<T>.StatusFieldName = "code" 后输出 code这就实现了“写出时统一口径,读入时兼容多口径”。
JsonConverterFactory:让泛型 MessageModel<T> 自动生效,并兼顾 AOTMessageModel<T> 是泛型类型,Converter 也对应是 MessageModelJsonConverter<T>。为了让 System.Text.Json 在遇到任意 MessageModel<任意T> 时都能自动应用正确的 Converter,我们在类型上标注:
[JsonConverter(typeof(MessageModelJsonConverterFactory))]
public record class MessageModel<T>
MessageModelJsonConverterFactory 做两件事:
1) 判断是否是 MessageModel<>:
public override bool CanConvert(Type typeToConvert)
{
return typeToConvert.IsGenericType &&
typeToConvert.GetGenericTypeDefinition() == typeof(MessageModel<>);
}
2) 创建对应的泛型 Converter,并同时考虑 AOT 与 非 AOT 两条路径:
Register<T>() 预注册,避免运行时反射/动态创建MessageModelJsonConverter<T>(开发环境/普通 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<T>()。
经过上面的处理,我们实现了以下收益,同时兼顾了性能与兼容性:
status / code,不强迫上下游立刻统一StatusFieldName 统一字段名,逐步推进规范化MessageModel<T> 不需要为每个 T 单独写 Converter 注册代码在适配 AOT 时,很多开发者会困惑:“我明明已经在 Context 里注册了模型,为什么还要手动 Register?” 这里我们可以用一个接地气的比喻来理解。
在 Native AOT 的世界里,编译器是一个极其严谨且“抠门”的管家。为了节省空间,他会清理掉所有看起来没用的代码。
JsonSerializable)这相当于给你的 DTO 模型(如 SummaryResponse)上户口。告诉编译器:“这个类是有用的,请保留它的属性结构。”如果没有这一步,序列化器连这个类有几个字段都不知道。
Register<T>)这是针对类库自定义转换逻辑的。在 AOT 下,编译器无法在运行时临时变出一个处理 SummaryResponse 的转换器代码。通过 MessageModelJsonConverterFactory.Register<T>(),你实际上是在给转换器做“岗前培训”。显式告诉编译器:“请为这个类型专门编译一套处理逻辑。”
在 AOT 模式下,你需要从“全自动”切换为“显式声明”。首先定义你的 JsonSerializerContext:
[JsonSerializable(typeof(MessageModel<string>))]
[JsonSerializable(typeof(MessageModel<SummaryResponse>))]
[JsonSerializable(typeof(SummaryResponse))]
[JsonSerializable(typeof(LoginRequest))]
[JsonSerializable(typeof(UserConfigWrapper))]
internal partial class WebAppAotJsonContext : JsonSerializerContext { }
在 Program.cs 中,完成“户口登记”与“岗位培训”:
builder.Services.ConfigureHttpJsonOptions(options =>
{
// 1. 设置元数据解析器(户口登记)
options.SerializerOptions.TypeInfoResolver = WebAppAotJsonContext.Default;
// 2. 注册业务模型到工厂(岗位培训)
MessageModelJsonConverterFactory.Register<SummaryResponse>();
MessageModelJsonConverterFactory.Register<string>();
// 3. 添加转换器
options.SerializerOptions.Converters.Add(new MessageModelJsonConverterFactory());
});
这是最容易踩坑的地方。在 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 的世界里,编译器不再允许“撞运气”的行为。Sang.AspNetCore.CommonLibraries 的这次更新,正是为了帮助开发者在享受 AOT 红利的同时,依然能保留优雅的一致性返回体验。
如果你正在尝试将应用迁移到 Native AOT,欢迎参考我仓库中的示例项目进行实践。