Serialization

aaronontheweb/dotnet-skills/skills/serialization

作者 aarononthewebe426ed93a9f3无许可证1.2K 个星标收录于 2026年10月8日更新于 2026年10月8日仓库3周前更新

Choose the right serialization format for .NET applications. Prefer schema-based formats (Protobuf, MessagePack) over reflection-based (Newtonsoft.Json). Use System.Text.Json with AOT source generators for JSON scenarios.

AI 生成的概览

指导 .NET 开发者选择序列化格式,并迁移到基于架构、兼容 AOT 的序列化方案。

功能
该技能为 .NET 应用提供序列化格式选择指导,比较 Protobuf、MessagePack 等基于架构的方案与 Newtonsoft.Json 等基于反射的方案。内容涵盖 System.Text.Json 源生成器、Protocol Buffers、MessagePack、从 Newtonsoft.Json 迁移、线格式兼容模式以及性能权衡。产出为建议、代码示例和配置片段,而非可执行结果。
适用场景
在为 .NET 的 API、消息传递或持久化选择序列化格式时使用。也适用于从 Newtonsoft.Json 迁移到 System.Text.Json、实现兼容 AOT 的序列化、为分布式系统设计线格式,或优化序列化性能。
运行要求
无需脚本或运行时依赖,仅为纯说明性参考。示例涉及 Google.Protobuf、Grpc.Tools、MessagePack 和 MessagePack.Annotations 等 .NET 包,但阅读指南本身无需安装任何内容。

Serialization in .NET

When to Use This Skill

Use this skill when:

  • Choosing a serialization format for APIs, messaging, or persistence
  • Migrating from Newtonsoft.Json to System.Text.Json
  • Implementing AOT-compatible serialization
  • Designing wire formats for distributed systems
  • Optimizing serialization performance

Schema-Based vs Reflection-Based

AspectSchema-BasedReflection-Based
ExamplesProtobuf, MessagePack, System.Text.Json (source gen)Newtonsoft.Json, BinaryFormatter
Type info in payloadNo (external schema)Yes (type names embedded)
VersioningExplicit field numbers/namesImplicit (type structure)
PerformanceFast (no reflection)Slower (runtime reflection)
AOT compatibleYesNo
Wire compatibilityExcellentPoor

Recommendation: Use schema-based serialization for anything that crosses process boundaries.


Format Recommendations

Use CaseRecommended FormatWhy
REST APIsSystem.Text.Json (source gen)Standard, AOT-compatible
gRPCProtocol BuffersNative format, excellent versioning
Actor messagingMessagePack or ProtobufCompact, fast, version-safe
Event sourcingProtobuf or MessagePackMust handle old events forever
CachingMessagePackCompact, fast
ConfigurationJSON (System.Text.Json)Human-readable
LoggingJSON (System.Text.Json)Structured, parseable

Formats to Avoid

FormatProblem
BinaryFormatterSecurity vulnerabilities, deprecated, never use
Newtonsoft.Json defaultType names in payload break on rename
DataContractSerializerComplex, poor versioning
XMLVerbose, slow, complex

System.Text.Json with Source Generators

For JSON serialization, use System.Text.Json with source generators for AOT compatibility and performance.

Setup

csharp
// Define a JsonSerializerContext with all your types[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 { }

Usage

csharp
// Serialize with contextvar json = JsonSerializer.Serialize(order, AppJsonContext.Default.Order);
// Deserialize with contextvar order = JsonSerializer.Deserialize(json, AppJsonContext.Default.Order);
// Configure in ASP.NET Corebuilder.Services.ConfigureHttpJsonOptions(options =>{    options.SerializerOptions.TypeInfoResolverChain.Insert(0, AppJsonContext.Default);});

Benefits

  • No reflection at runtime - All type info generated at compile time
  • AOT compatible - Works with Native AOT publishing
  • Faster - No runtime type analysis
  • Trim-safe - Linker knows exactly what's needed

Protocol Buffers (Protobuf)

Best for: Actor systems, gRPC, event sourcing, any long-lived wire format.

Setup

bash
dotnet add package Google.Protobufdotnet add package Grpc.Tools

Define Schema

protobuf
// orders.protosyntax = "proto3";
message Order {    string id = 1;    string customer_id = 2;    repeated OrderItem items = 3;    int64 created_at_ticks = 4;
    // Adding new fields is always safe    string notes = 5;  // Added in v2 - old readers ignore it}
message OrderItem {    string product_id = 1;    int32 quantity = 2;    int64 price_cents = 3;}

Versioning Rules

protobuf
// SAFE: Add new fields with new numbersmessage Order {    string id = 1;    string customer_id = 2;    string shipping_address = 5;  // NEW - safe}
// SAFE: Remove fields (old readers ignore unknown, new readers use default)// Just stop using the field, keep the number reservedmessage Order {    string id = 1;    // customer_id removed, but field 2 is reserved    reserved 2;}
// UNSAFE: Change field typesmessage Order {    int32 id = 1;  // Was: string - BREAKS!}
// UNSAFE: Reuse field numbersmessage Order {    reserved 2;    string new_field = 2;  // Reusing 2 - BREAKS!}

MessagePack

Best for: High-performance scenarios, compact payloads, actor messaging.

Setup

bash
dotnet add package MessagePackdotnet add package MessagePack.Annotations

Usage with Contracts

csharp
[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; }
    // New field - old readers skip unknown keys    [Key(4)]    public string? Notes { get; init; }}
// Serializevar bytes = MessagePackSerializer.Serialize(order);
// Deserializevar order = MessagePackSerializer.Deserialize<Order>(bytes);

AOT-Compatible Setup

csharp
// Use source generator for AOT[MessagePackObject]public partial class Order { }  // partial enables source gen
// Configure resolvervar options = MessagePackSerializerOptions.Standard    .WithResolver(CompositeResolver.Create(        GeneratedResolver.Instance,  // Generated        StandardResolver.Instance));

Migrating from Newtonsoft.Json

Common Issues

NewtonsoftSystem.Text.JsonFix
$type in JSONNot supported by defaultUse discriminators or custom converters
JsonPropertyJsonPropertyNameDifferent attribute
DefaultValueHandlingDefaultIgnoreConditionDifferent API
NullValueHandlingDefaultIgnoreConditionDifferent API
Private settersRequires [JsonInclude]Explicit opt-in
Polymorphism[JsonDerivedType] (.NET 7+)Explicit discriminators

Migration Pattern

csharp
// Newtonsoft (reflection-based)public class Order{    [JsonProperty("order_id")]    public string Id { get; set; }
    [JsonProperty(NullValueHandling = NullValueHandling.Ignore)]    public string? Notes { get; set; }}
// System.Text.Json (source-gen compatible)public sealed record Order(    [property: JsonPropertyName("order_id")]    string Id,
    string? Notes  // Null handling via JsonSerializerOptions);
[JsonSerializable(typeof(Order))][JsonSourceGenerationOptions(    PropertyNamingPolicy = JsonKnownNamingPolicy.SnakeCaseLower,    DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull)]public partial class OrderJsonContext : JsonSerializerContext { }

Polymorphism with Discriminators

csharp
// .NET 7+ polymorphism[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);
// Serializes as:// { "$type": "credit_card", "amount": 100, "last4": "1234" }

Wire Compatibility Patterns

Tolerant Reader

Old code must safely ignore unknown fields:

csharp
// Protobuf/MessagePack: Automatic - unknown fields skipped// System.Text.Json: Configure to allowvar options = new JsonSerializerOptions{    UnmappedMemberHandling = JsonUnmappedMemberHandling.Skip};

Introduce Read Before Write

Deploy deserializers before serializers for new formats:

csharp
// Phase 1: Add deserializer (deployed everywhere)public Order Deserialize(byte[] data, string manifest) => manifest switch{    "Order.V1" => DeserializeV1(data),    "Order.V2" => DeserializeV2(data),  // NEW - can read V2    _ => throw new NotSupportedException()};
// Phase 2: Enable serializer (next release, after V1 deployed everywhere)public (byte[] data, string manifest) Serialize(Order order) =>    _useV2Format        ? (SerializeV2(order), "Order.V2")        : (SerializeV1(order), "Order.V1");

Never Embed Type Names

csharp
// BAD: Type name in payload - renaming class breaks wire format{    "$type": "MyApp.Order, MyApp.Core",    "id": "123"}
// GOOD: Explicit discriminator - refactoring safe{    "type": "order",    "id": "123"}

Performance Comparison

Approximate throughput (higher is better):

FormatSerializeDeserializeSize
MessagePack★★★★★★★★★★★★★★★
Protobuf★★★★★★★★★★★★★★★
System.Text.Json (source gen)★★★★☆★★★★☆★★★☆☆
System.Text.Json (reflection)★★★☆☆★★★☆☆★★★☆☆
Newtonsoft.Json★★☆☆☆★★☆☆☆★★★☆☆

For hot paths, prefer MessagePack or Protobuf.


Akka.NET Serialization

For Akka.NET actor systems, use schema-based serialization:

hocon
akka {  actor {    serializers {      messagepack = "Akka.Serialization.MessagePackSerializer, Akka.Serialization.MessagePack"    }    serialization-bindings {      "MyApp.Messages.IMessage, MyApp" = messagepack    }  }}

See Akka.NET Serialization Docs.


Best Practices

DO

csharp
// Use source generators for System.Text.Json[JsonSerializable(typeof(Order))]public partial class AppJsonContext : JsonSerializerContext { }
// Use explicit field numbers/keys[MessagePackObject]public class Order{    [Key(0)] public string Id { get; init; }}
// Use records for immutable message typespublic sealed record OrderCreated(OrderId Id, CustomerId CustomerId);

DON'T

csharp
// Don't use BinaryFormatter (ever)var formatter = new BinaryFormatter();  // Security risk!
// Don't embed type names in wire formatsettings.TypeNameHandling = TypeNameHandling.All;  // Breaks on rename!
// Don't use reflection serialization for hot pathsJsonConvert.SerializeObject(order);  // Slow, not AOT-compatible

Resources

来源与署名

来源:aaronontheweb/dotnet-skills位于skills/serialization提交e426ed9

许可证: 无许可证

内容归原作者所有。SourceWeft 从公开仓库中收录这些内容。

举报或申请下架