Serialisierung in .NET
Wann man diese Fertigkeit einsetzt
Nutze diese Fertigkeit, wenn:
- Ein Serialisierungsformat für APIs, Nachrichtenübermittlung oder Persistenz ausgewählt werden soll
- Von Newtonsoft.Json zu System.Text.Json migriert wird
- AOT-kompatible Serialisierung implementiert wird
- Drahtformate für verteilte Systeme entworfen werden
- Die Serialisierungsleistung optimiert werden soll
Schema-basiert vs. Reflexionsbasiert
| Aspekt | Schema-basiert | Reflexionsbasiert |
|---|---|---|
| Beispiele | Protobuf, MessagePack, System.Text.Json (Quellgen.) | Newtonsoft.Json, BinaryFormatter |
| Typinfo im Payload | Nein (externes Schema) | Ja (Typnamen eingebettet) |
| Versionierung | Explizite Feldnummern/-namen | Implizit (Typstruktur) |
| Leistung | Schnell (keine Reflexion) | Langsamer (Laufzeitreflexion) |
| AOT-kompatibel | Ja | Nein |
| Drahtkompatibilität | Hervorragend | Schlecht |
Empfehlung: Verwende schemabasierte Serialisierung für alles, was Prozessgrenzen überschreitet.
Formatempfehlungen
| Anwendungsfall | Empfohlenes Format | Warum |
|---|---|---|
| REST-APIs | System.Text.Json (Quellgen.) | Standard, AOT-kompatibel |
| gRPC | Protocol Buffers | Natives Format, hervorragende Versionierung |
| Actor-Nachrichten | MessagePack oder Protobuf | Kompakt, schnell, versionssicher |
| Event Sourcing | Protobuf oder MessagePack | Muss alte Events für immer verarbeiten |
| Caching | MessagePack | Kompakt, schnell |
| Konfiguration | JSON (System.Text.Json) | Menschenlesbar |
| Logging | JSON (System.Text.Json) | Strukturiert, analysierbar |
Zu vermeidende Formate
| Format | Problem |
|---|---|
| BinaryFormatter | Sicherheitslücken, veraltet, niemals verwenden |
| Newtonsoft.Json Standard | Typnamen im Payload brechen bei Umbenennung |
| DataContractSerializer | Komplex, schlechte Versionierung |
| XML | Ausführlich, langsam, komplex |
System.Text.Json mit Quellgeneratoren
Verwende für die JSON-Serialisierung System.Text.Json mit Quellgeneratoren für AOT-Kompatibilität und Leistung.
Einrichtung
// Definiere einen JsonSerializerContext mit allen deinen Typen
[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 { }
Verwendung
// Mit Kontext serialisieren
var json = JsonSerializer.Serialize(order, AppJsonContext.Default.Order);
// Mit Kontext deserialisieren
var order = JsonSerializer.Deserialize(json, AppJsonContext.Default.Order);
// In ASP.NET Core konfigurieren
builder.Services.ConfigureHttpJsonOptions(options =>
{
options.SerializerOptions.TypeInfoResolverChain.Insert(0, AppJsonContext.Default);
});
Vorteile
- Keine Reflexion zur Laufzeit – Alle Typinformationen werden zur Kompilierzeit generiert
- AOT-kompatibel – Funktioniert mit Native AOT Publishing
- Schneller – Keine Laufzeit-Typanalyse
- Trimmsicher – Der Linker weiß genau, was benötigt wird
Protocol Buffers (Protobuf)
Am besten geeignet für: Actor-Systeme, gRPC, Event Sourcing, jedes langlebige Drahtformat.
Einrichtung
dotnet add package Google.Protobuf
dotnet add package Grpc.Tools
Schema definieren
// orders.proto
syntax = "proto3";
message Order {
string id = 1;
string customer_id = 2;
repeated OrderItem items = 3;
int64 created_at_ticks = 4;
// Neue Felder hinzuzufügen ist immer sicher
string notes = 5; // Hinzugefügt in v2 – alte Leser ignorieren es
}
message OrderItem {
string product_id = 1;
int32 quantity = 2;
int64 price_cents = 3;
}
Versionierungsregeln
// SICHER: Neue Felder mit neuen Nummern hinzufügen
message Order {
string id = 1;
string customer_id = 2;
string shipping_address = 5; // NEU – sicher
}
// SICHER: Felder entfernen (alte Leser ignorieren unbekannte, neue lesen Standard)
// Verwende das Feld einfach nicht mehr, behalte die Nummer reserviert
message Order {
string id = 1;
// customer_id entfernt, aber Feld 2 ist reserviert
reserved 2;
}
// UNSICHER: Feldtypen ändern
message Order {
int32 id = 1; // War: string - BRICHT!
}
// UNSICHER: Feldnummern wiederverwenden
message Order {
reserved 2;
string new_field = 2; // Wiederverwendung von 2 - BRICHT!
}
MessagePack
Am besten geeignet für: Hochleistungsszenarien, kompakte Payloads, Actor-Nachrichten.
Einrichtung
dotnet add package MessagePack
dotnet add package MessagePack.Annotations
Verwendung mit Verträgen
[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; }
// Neues Feld – alte Leser überspringen unbekannte Schlüssel
[Key(4)]
public string? Notes { get; init; }
}
// Serialisieren
var bytes = MessagePackSerializer.Serialize(order);
// Deserialisieren
var order = MessagePackSerializer.Deserialize<Order>(bytes);
AOT-kompatible Einrichtung
// Quellgenerator für AOT verwenden
[MessagePackObject]
public partial class Order { } // partial aktiviert Quellgenerierung
// Resolver konfigurieren
var options = MessagePackSerializerOptions.Standard
.WithResolver(CompositeResolver.Create(
GeneratedResolver.Instance, // Generiert
StandardResolver.Instance));
Migration von Newtonsoft.Json
Häufige Probleme
| Newtonsoft | System.Text.Json | Korrektur |
|---|---|---|
$type in JSON | Standardmäßig nicht unterstützt | Verwende Diskriminatoren oder benutzerdefinierte Konverter |
JsonProperty | JsonPropertyName | Anderes Attribut |
DefaultValueHandling | DefaultIgnoreCondition | Andere API |
NullValueHandling | DefaultIgnoreCondition | Andere API |
| Private Setter | Erfordert [JsonInclude] | Explizites Opt-in |
| Polymorphie | [JsonDerivedType] (.NET 7+) | Explizite Diskriminatoren |
Migrationsmuster
// Newtonsoft (reflexionsbasiert)
public class Order
{
[JsonProperty("order_id")]
public string Id { get; set; }
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
public string? Notes { get; set; }
}
// System.Text.Json (quellgen-kompatibel)
public sealed record Order(
[property: JsonPropertyName("order_id")]
string Id,
string? Notes // Nullbehandlung über JsonSerializerOptions
);
[JsonSerializable(typeof(Order))]
[JsonSourceGenerationOptions(
PropertyNamingPolicy = JsonKnownNamingPolicy.SnakeCaseLower,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull)]
public partial class OrderJsonContext : JsonSerializerContext { }
Polymorphie mit Diskriminatoren
// .NET 7+ Polymorphie
[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);
// Serialisiert als:
// { "$type": "credit_card", "amount": 100, "last4": "1234" }
Drahtkompatibilitätsmuster
Toleranter Leser
Alter Code muss unbekannte Felder sicher ignorieren:
// Protobuf/MessagePack: Automatisch – unbekannte Felder werden übersprungen
// System.Text.Json: Konfigurieren, um sie zu erlauben
var options = new JsonSerializerOptions
{
UnmappedMemberHandling = JsonUnmappedMemberHandling.Skip
};
Einführung vor dem Schreiben (Introduce Before Read)
Setze Deserialisierer vor Serialisierer für neue Formate ein:
// Phase 1: Deserialisierer hinzufügen (überall bereitgestellt)
public Order Deserialize(byte[] data, string manifest) => manifest switch
{
"Order.V1" => DeserializeV1(data),
"Order.V2" => DeserializeV2(data), // NEU – kann V2 lesen
_ => throw new NotSupportedException()
};
// Phase 2: Serialisierer aktivieren (nächstes Release, nachdem V1 überall bereitgestellt)
public (byte[] data, string manifest) Serialize(Order order) =>
_useV2Format
? (SerializeV2(order), "Order.V2")
: (SerializeV1(order), "Order.V1");
Typnamen niemals einbetten
// SCHLECHT: Typname im Payload – Umbenennung der Klasse bricht das Drahtformat
{
"$type": "MyApp.Order, MyApp.Core",
"id": "123"
}
// GUT: Expliziter Diskriminator – refactoring-sicher
{
"type": "order",
"id": "123"
}
Leistungsvergleich
Ungefährer Durchsatz (höher ist besser):
| Format | Serialisieren | Deserialisieren | Größe |
|---|---|---|---|
| MessagePack | ★★★★★ | ★★★★★ | ★★★★★ |
| Protobuf | ★★★★★ | ★★★★★ | ★★★★★ |
| System.Text.Json (Quellgen.) | ★★★★☆ | ★★★★☆ | ★★★☆☆ |
| System.Text.Json (Reflexion) | ★★★☆☆ | ★★★☆☆ | ★★★☆☆ |
| Newtonsoft.Json | ★★☆☆☆ | ★★☆☆☆ | ★★★☆☆ |
Für heiße Pfade bevorzuge MessagePack oder Protobuf.
Akka.NET Serialisierung
Verwende für Akka.NET-Actor-Systeme schemabasierte Serialisierung:
akka {
actor {
serializers {
messagepack = "Akka.Serialization.MessagePackSerializer, Akka.Serialization.MessagePack"
}
serialization-bindings {
"MyApp.Messages.IMessage, MyApp" = messagepack
}
}
}
Siehe Akka.NET Serialisierungsdokumentation.
Best Practices
TUN
// Quellgeneratoren für System.Text.Json verwenden
[JsonSerializable(typeof(Order))]
public partial class AppJsonContext : JsonSerializerContext { }
// Explizite Feldnummern/-Schlüssel verwenden
[MessagePackObject]
public class Order
{
[Key(0)] public string Id { get; init; }
}
// Records für unveränderliche Nachrichtentypen verwenden
public sealed record OrderCreated(OrderId Id, CustomerId CustomerId);
NICHT TUN
// BinaryFormatter niemals verwenden (niemals)
var formatter = new BinaryFormatter(); // Sicherheitsrisiko!
// Typnamen nicht in das Drahtformat einbetten
settings.TypeNameHandling = TypeNameHandling.All; // Bricht bei Umbenennung!
// Keine Reflexionsserialisierung für heiße Pfade verwenden
JsonConvert.SerializeObject(order); // Langsam, nicht AOT-kompatibel
Ressourcen
- System.Text.Json Quellgenerierung: 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 Serialisierung: https://getakka.net/articles/networking/serialization.html
- Drahtkompatibilität: https://getakka.net/community/contributing/wire-compatibility.html