名称: 序列化 描述: 为.NET应用程序选择合适的序列化格式。优先选择基于模式的格式(Protobuf、MessagePack),而非基于反射的格式(Newtonsoft.Json)。对于JSON场景,使用带有AOT源生成器的System.Text.Json。 可调用: false
.NET中的序列化
何时使用此技能
在以下情况下使用此技能:
- 为API、消息传递或持久化选择序列化格式时
- 从Newtonsoft.Json迁移到System.Text.Json时
- 实现AOT兼容的序列化时
- 为分布式系统设计有线格式时
- 优化序列化性能时
基于模式 vs 基于反射
| 方面 | 基于模式 | 基于反射 |
|---|---|---|
| 示例 | Protobuf、MessagePack、System.Text.Json(源生成) | Newtonsoft.Json、BinaryFormatter |
| 有效载荷中的类型信息 | 无(外部模式) | 有(嵌入类型名称) |
| 版本控制 | 显式字段编号/名称 | 隐式(类型结构) |
| 性能 | 快(无反射) | 较慢(运行时反射) |
| AOT兼容 | 是 | 否 |
| 有线兼容性 | 优秀 | 差 |
建议:对于任何跨越进程边界的情况,使用基于模式的序列化。
格式推荐
| 使用场景 | 推荐格式 | 原因 |
|---|---|---|
| REST API | System.Text.Json(源生成) | 标准,AOT兼容 |
| gRPC | Protocol Buffers | 原生格式,版本控制优秀 |
| Actor消息传递 | MessagePack 或 Protobuf | 紧凑,快速,版本安全 |
| 事件溯源 | Protobuf 或 MessagePack | 必须永久处理旧事件 |
| 缓存 | MessagePack | 紧凑,快速 |
| 配置 | JSON(System.Text.Json) | 人类可读 |
| 日志记录 | JSON(System.Text.Json) | 结构化,可解析 |
应避免的格式
| 格式 | 问题 |
|---|---|
| BinaryFormatter | 安全漏洞,已弃用,切勿使用 |
| Newtonsoft.Json默认 | 有效载荷中的类型名称在重命名时会破坏 |
| DataContractSerializer | 复杂,版本控制差 |
| XML | 冗长,慢,复杂 |
使用源生成器的System.Text.Json
对于JSON序列化,使用带有源生成器的System.Text.Json以实现AOT兼容性和性能。
设置
// 定义一个包含所有类型的JsonSerializerContext
[JsonSerializable(typeof(Order))]
[JsonSerializable(typeof(OrderItem))]
[JsonSerializable(typeof(Customer))]
[JsonSerializable(typeof(List<Order>))]
[JsonSourceGenerationOptions(
PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull)]
public partial class AppJsonContext : JsonSerializerContext { }
用法
// 使用上下文序列化
var json = JsonSerializer.Serialize(order, AppJsonContext.Default.Order);
// 使用上下文反序列化
var order = JsonSerializer.Deserialize(json, AppJsonContext.Default.Order);
// 在ASP.NET Core中配置
builder.Services.ConfigureHttpJsonOptions(options =>
{
options.SerializerOptions.TypeInfoResolverChain.Insert(0, AppJsonContext.Default);
});
优势
- 运行时无反射 - 所有类型信息在编译时生成
- AOT兼容 - 适用于Native AOT发布
- 更快 - 无需运行时类型分析
- 修剪安全 - 链接器确切知道需要什么
Protocol Buffers (Protobuf)
最佳适用:Actor系统、gRPC、事件溯源、任何长期存在的有线格式。
设置
dotnet add package Google.Protobuf
dotnet add package Grpc.Tools
定义模式
// orders.proto
syntax = "proto3";
message Order {
string id = 1;
string customer_id = 2;
repeated OrderItem items = 3;
int64 created_at_ticks = 4;
// 添加新字段总是安全的
string notes = 5; // 在v2中添加 - 旧读取器忽略它
}
message OrderItem {
string product_id = 1;
int32 quantity = 2;
int64 price_cents = 3;
}
版本控制规则
// 安全:使用新编号添加新字段
message Order {
string id = 1;
string customer_id = 2;
string shipping_address = 5; // 新增 - 安全
}
// 安全:移除字段(旧读取器忽略未知字段,新读取器使用默认值)
// 只需停止使用该字段,保留编号为保留
message Order {
string id = 1;
// customer_id 已移除,但字段2被保留
reserved 2;
}
// 不安全:更改字段类型
message Order {
int32 id = 1; // 原来是:string - 会破坏!
}
// 不安全:重用字段编号
message Order {
reserved 2;
string new_field = 2; // 重用2 - 会破坏!
}
MessagePack
最佳适用:高性能场景、紧凑有效载荷、Actor消息传递。
设置
dotnet add package MessagePack
dotnet add package MessagePack.Annotations
使用合约的用法
[MessagePackObject]
public sealed class Order
{
[Key(0)]
public required string Id { get; init; }
[Key(1)]
public required string CustomerId { get; init; }
[Key(2)]
public required IReadOnlyList<OrderItem> Items { get; init; }
[Key(3)]
public required DateTimeOffset CreatedAt { get; init; }
// 新字段 - 旧读取器跳过未知键
[Key(4)]
public string? Notes { get; init; }
}
// 序列化
var bytes = MessagePackSerializer.Serialize(order);
// 反序列化
var order = MessagePackSerializer.Deserialize<Order>(bytes);
AOT兼容设置
// 为AOT使用源生成器
[MessagePackObject]
public partial class Order { } // partial 启用源生成
// 配置解析器
var options = MessagePackSerializerOptions.Standard
.WithResolver(CompositeResolver.Create(
GeneratedResolver.Instance, // 生成的
StandardResolver.Instance));
从Newtonsoft.Json迁移
常见问题
| Newtonsoft | System.Text.Json | 修复方法 |
|---|---|---|
JSON中的 $type |
默认不支持 | 使用鉴别器或自定义转换器 |
JsonProperty |
JsonPropertyName |
不同的属性 |
DefaultValueHandling |
DefaultIgnoreCondition |
不同的API |
NullValueHandling |
DefaultIgnoreCondition |
不同的API |
| 私有setter | 需要 [JsonInclude] |
显式选择加入 |
| 多态性 | [JsonDerivedType] (.NET 7+) |
显式鉴别器 |
迁移模式
// Newtonsoft(基于反射)
public class Order
{
[JsonProperty("order_id")]
public string Id { get; set; }
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
public string? Notes { get; set; }
}
// System.Text.Json(源生成兼容)
public sealed record Order(
[property: JsonPropertyName("order_id")]
string Id,
string? Notes // 通过JsonSerializerOptions处理空值
);
[JsonSerializable(typeof(Order))]
[JsonSourceGenerationOptions(
PropertyNamingPolicy = JsonKnownNamingPolicy.SnakeCaseLower,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull)]
public partial class OrderJsonContext : JsonSerializerContext { }
使用鉴别器的多态性
// .NET 7+ 多态性
[JsonDerivedType(typeof(CreditCardPayment), "credit_card")]
[JsonDerivedType(typeof(BankTransferPayment), "bank_transfer")]
public abstract record Payment(decimal Amount);
public sealed record CreditCardPayment(decimal Amount, string Last4) : Payment(Amount);
public sealed record BankTransferPayment(decimal Amount, string AccountNumber) : Payment(Amount);
// 序列化为:
// { "$type": "credit_card", "amount": 100, "last4": "1234" }
有线兼容性模式
容错读取器
旧代码必须安全地忽略未知字段:
// Protobuf/MessagePack:自动 - 跳过未知字段
// System.Text.Json:配置为允许
var options = new JsonSerializerOptions
{
UnmappedMemberHandling = JsonUnmappedMemberHandling.Skip
};
先读后写
在部署新格式的序列化器之前先部署反序列化器:
// 阶段1:添加反序列化器(部署到各处)
public Order Deserialize(byte[] data, string manifest) => manifest switch
{
"Order.V1" => DeserializeV1(data),
"Order.V2" => DeserializeV2(data), // 新增 - 可以读取V2
_ => throw new NotSupportedException()
};
// 阶段2:启用序列化器(下一个版本,在V1部署到各处之后)
public (byte[] data, string manifest) Serialize(Order order) =>
_useV2Format
? (SerializeV2(order), "Order.V2")
: (SerializeV1(order), "Order.V1");
切勿嵌入类型名称
// 错误:有效载荷中的类型名称 - 重命名类会破坏有线格式
{
"$type": "MyApp.Order, MyApp.Core",
"id": "123"
}
// 正确:显式鉴别器 - 重构安全
{
"type": "order",
"id": "123"
}
性能比较
近似吞吐量(越高越好):
| 格式 | 序列化 | 反序列化 | 大小 |
|---|---|---|---|
| MessagePack | ★★★★★ | ★★★★★ | ★★★★★ |
| Protobuf | ★★★★★ | ★★★★★ | ★★★★★ |
| System.Text.Json(源生成) | ★★★★☆ | ★★★★☆ | ★★★☆☆ |
| System.Text.Json(反射) | ★★★☆☆ | ★★★☆☆ | ★★★☆☆ |
| Newtonsoft.Json | ★★☆☆☆ | ★★☆☆☆ | ★★★☆☆ |
对于热点路径,优先选择MessagePack或Protobuf。
Akka.NET序列化
对于Akka.NET actor系统,使用基于模式的序列化:
akka {
actor {
serializers {
messagepack = "Akka.Serialization.MessagePackSerializer, Akka.Serialization.MessagePack"
}
serialization-bindings {
"MyApp.Messages.IMessage, MyApp" = messagepack
}
}
}
参见 Akka.NET序列化文档。
最佳实践
应该做
// 为System.Text.Json使用源生成器
[JsonSerializable(typeof(Order))]
public partial class AppJsonContext : JsonSerializerContext { }
// 使用显式字段编号/键
[MessagePackObject]
public class Order
{
[Key(0)] public string Id { get; init; }
}
// 对不可变消息类型使用记录
public sealed record OrderCreated(OrderId Id, CustomerId CustomerId);
不应该做
// 不要使用BinaryFormatter(永远不要)
var formatter = new BinaryFormatter(); // 安全风险!
// 不要在有线格式中嵌入类型名称
settings.TypeNameHandling = TypeNameHandling.All; // 重命名时会破坏!
// 不要对热点路径使用反射序列化
JsonConvert.SerializeObject(order); // 慢,不兼容AOT
资源
- System.Text.Json源生成:https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json/source-generation
- Protocol Buffers:https://protobuf.dev/
- MessagePack-CSharp:https://github.com/MessagePack-CSharp/MessagePack-CSharp
- Akka.NET序列化:https://getakka.net/articles/networking/serialization.html
- 有线兼容性:https://getakka.net/community/contributing/wire-compatibility.html