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 移轉、wire 格式相容模式,以及效能取捨。產出為建議、程式碼範例與設定片段,而非可執行的結果。
適用情境
在為 .NET 的 API、訊息傳遞或持久化選擇序列化格式時使用。也適合從 Newtonsoft.Json 移轉到 System.Text.Json、實作相容於 AOT 的序列化、為分散式系統設計 wire 格式,或最佳化序列化效能。
執行需求
不需要指令碼或執行階段相依性,僅為純說明性參考。範例提及 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 從公開儲存庫中收錄這些內容。

檢舉或申請下架