Protobuf in C# serialisieren und deserialisieren

Es gibt eine lange Liste von Formaten, welche für die Serialisierung und Deserialisierung von .NET Objekten verwendet werden kann. Xml und Json sind dabei lediglich die populärsten Vertreter. In diesem Artikel möchte ich mich mit Protobuf (oder ausgeschrieben “Protocol Buffers”) beschäftigen. Protobuf ist ein Format von Google. Es ist auf Performance und möglichst wenig Platzverbrauch ausgelegt. Zudem gibt es Unterstützung für viele verschiedene Programmiersprachen – C# ist nur eine davon. Protobuf lässt sich mit den Bibliotheken von Google in Java, Python, Objective-C, C++, Kotlin, Dart, Go, Ruby, PHP und natürlich C# verwenden. Es findet auch im Protokoll gRPC Verwendung. Letzteres ist einer der Gründe, warum ich mich mit Protobuf mehr in der Tiefe beschäftigt habe. Schließlich interessiert mich nicht nur die Serialisierung selbst oder Sachen wie die Performance, sondern auch wie mit Kompatibilitätsthemen (neue Felder, umbenannte Felder, etc.) umgegangen wird. In diesem Artikel schauen wir uns diese Themen anhand zweier alternativer Bibliotheken an. Diese sind Google.Protobuf von Google und protobuf-net von Marc Gravell.

Allgemeines zu Protobuf

Die Dokumentation zu Protobuf von Google ist unter [1] zu finden. Ich beschreibe es hier im Artikel in abgekürzter Form: Protobuf ist ein binäres Format, welches über proto Dateien beschrieben wird. Letzteres lässt sich gut mit XML Schema [2] oder JSON Schema [3] vergleichen. Allgemein ist es ein bevorzugter Ansatz bei Protobuf, dass zunächst das Schema in Form einer proto Datei beschrieben wird. Anschließend werden die zugehörigen C# Klassen mithilfe des Protobuf Compilers protoc generiert. Gleiches gilt für den notwendigen Code in anderen Programmiersprachen. Eine Installationsanleitung zu protoc findet man unter [4]. Nachfolgend ein Beispiel für eine proto Datei.

syntax="proto3";

option csharp_namespace = "HappyCoding.ProtobufSerialization.UsingGoogleProtobuf.Data";

message MyTestMessage {
  string firstName = 1;
  string lastName = 2;
  int32 age = 3;
  repeated string emails = 4;
}

In diesem kleinen Beispiel sehen wir folgende Elemente:

  • In Zeile 1 setzen wir die Syntax auf “proto3”. Hierbei handelt es sich um die neueste Version des Formats.
  • In Zeile 3 konfigurieren wir den namespace für die generierten C# Klassen. Diese option hat keine Auswirkung, wenn für eine andere Programmiersprache wie etwa Java Klassen generiert werden.
  • In den Zeilen 5 bis 10 wird die Datenstruktur definiert. Anstelle von Klassen spricht man in Protobuf von Nachrichten (message). Die Schlüssel der Felder sind dabei nicht etwa die Namen (z. B. firstName, lastName, etc.), sondern die Zahlen hinter den = Zeichen.

Weitere Infos zu Protobuf

Weiter möchte ich hier im Artikel auf das proto Format nicht eingehen. Es gibt viele Quellen im Internet, bei denen das Format umfassend beschrieben ist. Ein gutes Beispiel ist die Protobuf Dokumentation von Google unter [1]. Das Format ist tatsächlich auch weniger komplex, als man anfangs denkt. Wichtig zu wissen sind dabei aus meiner Sicht folgende Dinge:

  • Es gibt viele verschiedene Zahlenformate, welche sich darin unterscheiden, wie viel Platz (bytes) sie für unterschiedliche Werte benötigen.
  • Komplexere Datentypen wie DateTime, DateTimeOffset usw. sieht das proto Format nicht vor. Hier kann man Definitionen von Google ziehen oder sich selber zusammenbauen.
  • Es gibt kein Nullable in Protobuf. Ein Nullable Wert muss über einen eigenen Typ definiert werden.

Protobuf in C# mit Google.Protobuf verwenden

Schauen wir uns jetzt etwas C# Code an. Zunächst verwenden wir das Nuget Paket Google.Protobuf [5]. Im Projekt erstellen wir die proto Datei MyTestMessage.proto mit dem Inhalt von oben. Zum generieren der C#-Klassen dafür benötigen wir noch den protoc Compiler. Die Installationsanleitung habe ich weiter oben unter [4] verwiesen. Alternativ kann man ihn auch per Chocolatey installieren – letzteres habe ich gemacht. Mit folgender Kommandozeile kann man die notwendigen C#-Klassen im gleichen Ordner wie die MyTestMessage.proto Datei generieren lassen. Wenn man protoc nicht installieren möchte, kann alternativ auch der Online Code-Generator unter [6] verwendet werden.

protoc MyTestMessage.proto --csharp_out=.

Nachfolgender Codeausschnitt zeigt, wie man die generierten C#-Klassen für die Serialisierung eines Objekts verwenden kann. Am Ende erhalten wir dabei eine Array aus Bytes, welche den serialisierten Inhalt des MyTestMessage Objekts enthält.

var myMessage = new MyTestMessage();
myMessage.FirstName = "Test FirstName";
myMessage.LastName = "Test LastName";
myMessage.Age = 8;
myMessage.Emails.Add("test@test.com");
myMessage.Emails.Add("test@test.de");

// Protobuf serialization
using var memStream = new MemoryStream(myMessage.CalculateSize());
myMessage.WriteTo(memStream);

var serializedBytes = memStream.ToArray();

Nachfolgender Codeausschnitt zeigt, wie man diese Daten wieder deserialisieren kann.

var deserialisierteTestMessage = new MyTestMessage();
deserialisierteTestMessage.MergeFrom(serializedBytes);

Protobuf in C# mit protobuf-net verwenden

An dieser Stelle möchte ich auch auf eine Alternative zu Google.Protobuf eingehen, und zwar protobuf-net [7]. Hierbei handelt es sich um eine komplett in C# geschriebene Implementierung des Protobuf Protokolls. Eine Besonderheit hier ist, dass man nicht mit der proto Datei startet, sondern mit der C# Klasse. Nachfolgender Codeausschnitt zeigt die Definition der Nachricht aus dem Beispiel oben.

[ProtoContract]
internal class MyTestMessage
{
    [ProtoMember(1)]
    public string FirstName { get; set; } = string.Empty;

    [ProtoMember(2)]
    public string LastName { get; set; } = string.Empty;

    [ProtoMember(3)]
    public int Age { get; set; }

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

Das Serialisieren und Deserialisieren von Objekten dieser Klasse wird wie folgt gemacht.

var myMessage = new MyTestMessage();
myMessage.FirstName = "Test FirstName";
myMessage.LastName = "Test LastName";
myMessage.Age = 8;
myMessage.Emails.Add("test@test.com");
myMessage.Emails.Add("test@test.de");

// Protobuf serialization
using var memStream = new MemoryStream();
Serializer.Serialize(memStream, myMessage);

var serializedBytes = memStream.ToArray();

// Protobuf deserialization
var myTestMessageDeserialized = Serializer.Deserialize<MyTestMessage>(serializedBytes.AsSpan());

Über die statische Methode Serializer.GetProto<T> kann protobuf-net zudem die proto Datei zur Klasse generieren. Letzteres wird dann benötigt, wenn man diese proto Datei etwa zum Generieren des Codes für eine andere Programmiersprache verwenden möchte.

Kompatibilität bei Änderungen der Nachrichten-Struktur

In diesem Kapitel möchte ich mich mit dem Thema Kompatibilität beschäftigen. Damit meine ich, wie das Format darauf reagiert, dass sich die Nachrichten-Typen ändern. Sagen wir mal, zum obigen Beispiel kommt noch ein Feld “Address” hinzu. Was passiert dann? Die gute Nachricht: Nichts. Alte Nachrichten können nach wie vor gelesen werden. Das neue Feld bleibt dabei lediglich auf seinem Default-Wert. Gleiches Gilt bei dem Löschen eines Felds aus der proto Datei.

Beim Umbenennen von Feldern spielt uns zudem eine Besonderheit von Protobuf in die Karten. Wie weiter oben schon erwähnt, identifiziert Protobuf Felder anhand einer Zahl, nicht anhand des Namens. Das sorgt dafür, dass wir die Felder beliebig umbenennen können. Nachfolgender Codeausschnitt zeigt eine Protobuf Datei, mit der Nachrichten aus den obigen Beispielen problemlos deserialisiert werden können. Dass die Feldnamen hier alle auf Deutsch übersetzt wurden, stört Protobuf nicht.

syntax="proto3";

option csharp_namespace = "HappyCoding.ProtobufSerialization.UsingGoogleProtobuf.Data";

message MyTestMessage_DifferentNames {
  string vorName = 1;
  string nachName = 2;
  int32 alter = 3;
  repeated string emails = 4;
}

Dennoch gibt es ein paar Themen, auf die man beim Erweitern von Nachrichten achten muss. Ändern von Datentypen einzelner Felder ist etwa nicht möglich. Bei neuen Feldern muss auch immer darauf geachtet werden, dass eindeutige Feldnummern innerhalb einer message verwendet werden. Weitere zu beachtende Themen sind in der Dokumentation von Google unter [8] zu finden.

Fazit

Wenn man gRPC verwendet, kommt man um Protobuf nicht herum. Verwendet man gRPC, so sollte man sich also ebenso tiefer mit Protobuf beschäftigen. Davon unabhängig sehe ich Protobuf aber auch so als sehr spannendes Format. Das gilt etwa für Netzwerkkommunikation mit TCP/IP, als auch für das Speichern von Daten in einer Datei. Protobuf sorgt für eine hohe Geschwindigkeit und wenig Platzverbrauch. Der wichtigste Nachteil, der für mich dabei übrig bleibt, ist das binäre Format. Anders als etwa Json oder Xml kann es nicht mit einfachen Texteditoren gelesen werden. Dafür behandelt es von Haus aus verschiedene Kompatibilitätsthemen. Mit neuen oder umbenannten Feldern hat das Format keine Probleme.

Downloads

Verweise

  1. Dokumentation zu Protobuf von Google
    https://protobuf.dev/
  2. Infos zu XML Schema auf Wikipedia
    https://de.wikipedia.org/wiki/XML_Schema
  3. Infos zu JSON Schema
    https://json-schema.org/
  4. Installationsanleitung für den Protobuf Compiler protoc
    https://grpc.io/docs/protoc-installation
  5. Das Nuget Paket Google.Protobuf
    https://www.nuget.org/packages/Google.Protobuf
  6. Online Code-Generator Tool für proto Dateien von Marc Gravell
    https://protogen.marcgravell.com
  7. GitHub Projekt zu protobuf-net
    https://github.com/protobuf-net/protobuf-net
  8. Proto Best Practices
    https://protobuf.dev/programming-guides/dos-donts

Ebenfalls interessant

  1. Yaml Dateien mit C# parsen
    https://www.rolandk.de/wp-posts/2022/11/yaml-dateien-mit-c-parsen
  2. Testautomatisierung beim Parsing von Dokumenten
    https://www.rolandk.de/wp-posts/2022/08/testautomatisierung-beim-parsing-von-dokumenten

Schreibe einen Kommentar

Diese Website verwendet Akismet, um Spam zu reduzieren. Erfahre mehr darüber, wie deine Kommentardaten verarbeitet werden.