C# 高级教程 - 12 序列化进阶

基础教程中我们学习了 JSON 和 XML 的基本序列化。但真实世界的序列化需求复杂得多:二进制序列化、循环引用处理、自定义转换器、跨语言序列化协议、以及性能敏感的零分配序列化。本文将深入探讨 System.Text.Json、MessagePack、Protobuf 等现代序列化方案的核心原理与实际应用。

System.Text.Json 高级特性

自定义 JsonConverter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
using System.Text.Json;
using System.Text.Json.Serialization;

// 自定义 DateTime 格式
public class CustomDateTimeConverter : JsonConverter<DateTime>
{
private readonly string _format;

public CustomDateTimeConverter(string format = "yyyy/MM/dd HH:mm:ss")
{
_format = format;
}

public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert,
JsonSerializerOptions options)
{
return DateTime.Parse(reader.GetString()!);
}

public override void Write(Utf8JsonWriter writer, DateTime value,
JsonSerializerOptions options)
{
writer.WriteStringValue(value.ToString(_format));
}
}

// 使用
var options = new JsonSerializerOptions
{
Converters = { new CustomDateTimeConverter() }
};
var json = JsonSerializer.Serialize(new { Date = DateTime.Now }, options);
// {"Date":"2025/05/11 20:00:00"}

条件序列化(ShouldSerialize)

1
2
3
4
5
6
7
8
9
public class Product
{
public string Name { get; set; } = "";
public decimal Price { get; set; }
public decimal? DiscountPrice { get; set; }

// 约定方法:当 DiscountPrice 为空时不序列化
public bool ShouldSerializeDiscountPrice() => DiscountPrice.HasValue;
}

JsonIgnore 条件

1
2
3
4
5
6
7
8
9
10
11
public class User
{
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? MiddleName { get; set; }

[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
public int DefaultValue { get; set; } // 为 0 时不序列化

[JsonInclude] // 非公开属性/字段序列化
private string InternalId { get; set; } = "";
}

循环引用处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Employee
{
public string Name { get; set; } = "";
public Employee? Manager { get; set; }
public List<Employee>? Reports { get; set; }
}

// 方式 1:Preserve(添加 $id/$ref)
var options = new JsonSerializerOptions
{
ReferenceHandler = ReferenceHandler.Preserve
};
var json = JsonSerializer.Serialize(employee, options);

// 方式 2:IgnoreCycles(.NET 6+)
var options2 = new JsonSerializerOptions
{
ReferenceHandler = ReferenceHandler.IgnoreCycles
};

MessagePack

MessagePack 是一种高效的二进制序列化格式,比 JSON 更小更快。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 安装: dotnet add package MessagePack

using MessagePack;

[MessagePackObject]
public class Person
{
[Key(0)]
public string Name { get; set; } = "";

[Key(1)]
public int Age { get; set; }

[Key(2)]
public string[]? Tags { get; set; }
}

// 序列化
var person = new Person { Name = "张三", Age = 28, Tags = ["C#", ".NET"] };
byte[] bytes = MessagePackSerializer.Serialize(person);

// 反序列化
Person deserialized = MessagePackSerializer.Deserialize<Person>(bytes);

// 压缩模式(进一步减小体积)
MessagePackSerializer.Serialize(person, MessagePackSerializerOptions.Standard
.WithCompression(MessagePackCompression.Lz4BlockArray));

性能对比(非官方基准):

格式 大小(1000 个对象) 序列化时间 反序列化时间
JSON (UTF-8) ~120 KB ~5ms ~6ms
MessagePack ~85 KB ~3ms ~3ms
MessagePack + LZ4 ~40 KB ~5ms ~4ms
Protobuf ~60 KB ~2ms ~2ms

Protobuf(Google Protocol Buffers)

Protobuf 擅长跨语言数据交换,.NET 中使用 protobuf-net 或 Google.Protobuf。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// 安装: dotnet add package protobuf-net

using ProtoBuf;

[ProtoContract]
public class Order
{
[ProtoMember(1)]
public int Id { get; set; }

[ProtoMember(2)]
public string Customer { get; set; } = "";

[ProtoMember(3)]
public decimal Amount { get; set; }

[ProtoMember(4)]
public List<OrderItem> Items { get; set; } = new();
}

[ProtoContract]
public class OrderItem
{
[ProtoMember(1)]
public string ProductName { get; set; } = "";

[ProtoMember(2)]
public int Quantity { get; set; }
}

// 序列化
using var stream = new MemoryStream();
Serializer.Serialize(stream, order);

// 反序列化
stream.Position = 0;
var deserialized = Serializer.Deserialize<Order>(stream);

自定义序列化 —— ISerializable(.NET 原生)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
[Serializable]
public class ComplexData : ISerializable
{
public string Name { get; set; }
public DateTime Timestamp { get; set; }
public byte[]? Payload { get; set; }

public ComplexData() { Name = ""; }

// 反序列化构造函数
protected ComplexData(SerializationInfo info, StreamingContext context)
{
Name = info.GetString("Name") ?? "";
Timestamp = info.GetDateTime("Timestamp");
Payload = (byte[]?)info.GetValue("Payload", typeof(byte[]));
}

// 序列化方法
public void GetObjectData(SerializationInfo info, StreamingContext context)
{
info.AddValue("Name", Name);
info.AddValue("Timestamp", Timestamp);
info.AddValue("Payload", Payload);
}
}

序列化性能优化

1. Source Generator(.NET 6+)

1
2
3
4
5
6
7
8
9
10
11
using System.Text.Json.Serialization;

[JsonSerializable(typeof(Product[]))]
[JsonSerializable(typeof(Order))]
internal partial class AppJsonContext : JsonSerializerContext
{
}

// 无反射序列化(编译时生成)
string json = JsonSerializer.Serialize(product, AppJsonContext.Default.Product);
var product2 = JsonSerializer.Deserialize(json, AppJsonContext.Default.Product);

2. Utf8JsonWriter——零分配写入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
using System.Text.Json;

// 手动构建 JSON(零对象分配,除 byte[] 外)
byte[] utf8Json;
using (var stream = new MemoryStream())
using (var writer = new Utf8JsonWriter(stream))
{
writer.WriteStartObject();
writer.WriteString("name", "张三");
writer.WriteNumber("age", 28);
writer.WriteStartArray("tags");
writer.WriteStringValue("C#");
writer.WriteStringValue(".NET");
writer.WriteEndArray();
writer.WriteEndObject();
writer.Flush();
utf8Json = stream.ToArray();
}

3. 管道式反序列化

1
2
3
4
5
6
7
8
9
10
11
// 流式反序列化超大 JSON 数组
using var fs = File.OpenRead("huge_data.json");
var data = await JsonSerializer.DeserializeAsync<Data[]>(fs);

// 或逐项处理(流式)
using var doc = await JsonDocument.ParseAsync(fs);
foreach (var element in doc.RootElement.EnumerateArray())
{
var item = JsonSerializer.Deserialize<Data>(element.GetRawText());
Process(item);
}

序列化安全

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 危险:反序列化时类型注入
string maliciousJson = @"{""$type"":""System.IO.FileInfo, mscorlib""}";

// 安全措施:
// 1. Newtonsoft.Json: SerializationBinder 白名单
// 2. System.Text.Json: 默认不包含类型信息
// 3. BinaryFormatter: 已经标记为 Obsolete 并将在 .NET 9+ 抛出异常

// 安全的类型绑定器示例(Newtonsoft.Json)
class SafeBinder : ISerializationBinder
{
private static readonly HashSet<string> AllowedTypes = new()
{
"MyApp.Data.Person, MyApp",
"MyApp.Data.Order, MyApp",
};

public Type? BindToType(string? assemblyName, string typeName)
{
var fullName = $"{typeName}, {assemblyName}";
return AllowedTypes.Contains(fullName)
? Type.GetType(fullName)
: throw new SecurityException($"类型 {fullName} 不在白名单中");
}

public void BindToName(Type serializedType,
out string? assemblyName, out string? typeName)
{
assemblyName = null; typeName = null;
}
}

跨语言序列化选择指南

协议 适用场景 优点 缺点
JSON Web API、配置文件 人类可读、广泛支持 体积大、解析慢
MessagePack .NET 内部 RPC、缓存 体积小、速度快 跨语言支持有限
Protobuf 微服务间 RPC (gRPC) 强类型 Schema、跨语言 需要 .proto 文件
XML 遗留系统、文档格式 Schema 验证、命名空间 臃肿、解析慢
BinaryFormatter ❌ 已废弃 - 不安全、不可移植

总结:现代 C# 序列化的关键决策在于平衡性能、体积、可读性和跨语言兼容性。System.Text.Json 是 Web API 的首选,MessagePack 适合本地缓存和进程间通信,Protobuf 是 gRPC 微服务的标配。Source Generator 的引入让序列化实现了零反射开销,而自定义转换器和流式 API 则为极端性能场景提供了精细控制。

ByteFisher
分享编程技术 · 记录钓鱼乐趣
扫码关注
▸ 扫码关注 ◂
分享: