en / de
Expertisen
Methoden
Dienstleistungen
Referenzen
Jobs & Karriere
Firma
Technologie-Trends TechCast WebCast TechBlog News Events Academy

Tutorial: Protocol Buffers in ASP.NET Core

1.  Einleitung

Protocol Buffers (Protobuf) ist eine Methode zur Serialisierung strukturierter Daten. Protobuf umfasst eine Schnittstellenbeschreibungssprache, um die Strukturen der Daten zu definieren und Compiler zur Generierung der Strukturen und (De-)Serialisierung. Es gibt mittlerweile Protobuf Compiler für C#, C, C++, Go, Objective-C, Java, Python, Swift und Ruby.

Google hat Protobuf ursprünglich für die interne Verwendung entwickelt und im Juli 2008 veröffentlicht. Es gibt mittlerweile drei Versionen, die nicht miteinander kompatible sind: Google intern (<2008), proto2 und proto3.

Die Entwurfsziele für Protobuf sind Einfachheit und Leistung. Protobuf wurde so konzipiert, dass es kleiner und schneller ist als XML und JSON. XML und JSON sind nicht für den Datenaustausch zwischen zwei verschiedenen Plattformen optimiert.

Protocol Buffers bietet viele Vorteile:

Protobuf wird in gRPC verwendet.

2.  Wie es funktioniert…

Warum ist Protobuf so schnell und kompakt?

  1. Kontext ist getrennt von den Daten
  2. Binäres Transportformat (statt Text wie bei XML und JSON)

 

2.1  Kontext getrennt von den Daten

In XML und JSON enthält jeder Eintrag den Kontext und die Daten.
Beispiel XML:

<Person>
  <FirstName>Peter</FirstName>
  <LastName>Muster</LastName>
</Person>

Beispiel JSON:

{
  “firstName”: “Peter”,
  “lastName”: “Muster”
}

Bei Protobuf werden die Datenstrukturen (‘message’) in einer Konfigurationsdatei definiert (*.proto). Der Kontext ist in den Datenstrukturen definiert:

Message Person{ 
  String first_name = 1; 
  String last_name = 2; 
}

Aus dieser Datei werden durch den Compiler die dazugehörigen Klassen generiert und die Methoden zur Serialisierung und Deserialisierung.

2.2  Binäres Transportformat

Eine detaillierte Beschreibung der binären Kodierung würde den Ramen dieses Blogs sprengen, dazu wird zur originalen Quelle verwiesen: https://developers.google.com/protocol-buffers/docs/encoding.

Jedes Feld hat folgendes Format:

{field_number << 3 | field_type} + {length of data} + {data}

Nehmen wir als Beispiel die minimale Struktur Person:

message Person{
  string name = 1; 
}

Es gibt eine Instanz mit folgendem Inhalt:

person.name = “Erik Stroeken”

Die binäre Darstellung für ‘name’ sieht dann so aus:

0a 0d 45 72 69 6b 20 53 74 72 6f 65 65 6e => {10} + {13} + {Erik Stroeken}

Die Felder werden wie folgt interpretiert:

{field_number}: 10 => 0000 1010 >> 3 => 0000 0001 => 1
{field_type}: 10 => 0000 1010 => 2
{length of data}: 13
{data}: ‘Erik Stroeken’

3.  Protobuf Messages

Die Strukturen der Protobuf Messages werden in einer separaten Textdatei (*.proto) definiert und dann in die Sprache kompiliert, in der die Nachrichten verwendet werden.
Die Namen der Messages sollten CamelCase sein (z.B. ‹message SongServerRequest›).
Jede Nachricht hat vier Felder:

  1. Rule
  2. Type
  3. Name
  4. Tag

In der ‘proto2’ Version von Protobuf war es noch möglich Standardwerte für jedes Feld zu definieren. Das geht nicht mehr in ‘proto3’: der default Wert ist der Standardwert vom Typ (0 oder string.Empty).

3.1  Message Feld ‘Rule’

message Customer {
int32 id = 1;
string username = 2;
repeated google.protobuf.Any details = 3;
}

Für Rule gibt es nur zwei Möglichkeiten:

Bei wiederholten Feldern (z.B.’repeated string keys = 1′) werden am besten pluralisierte Namen verwenden.
‘repeated’ Felder sind read-only. Man kann Elemente hinzufügen oder löschen, aber nicht die ganze Kollektion ersetzen.
Der Protobuf-Compiler für C# übersetzt ‘repeated’ in den Typ RepeatedField<T>. Dieser Typ ist wie List<T> mit ein paar extra Methoden wie Add() ausgestattet, welche eine Sammlung entgegennimmt zur Verwendung in Collection-Initialisatoren.

3.2  Message Feld ‘Type’

message Customer {
int32 id = 1;
string username = 2;
repeated google.protobuf.Any details = 3;
}

Es gibt folgende Typen in Protobuf:

  1. Scalar Typ
  2. Enumeration
  3. Message Typ
  4. Typ ‘one of’
  5. Typ ‘map’
  6. Typ ‘Any’

 

3.2.1  Scalar Typ

Mögliche Werte:

Die Typen mit den Präfixen ‘s’ oder ‘sfixed’ sind nur Varianten zur Optimierung.

3.2.2  Enumerator

Enumeratortypnamen sind alle CamelCase, Enumeratorwertnamen sind alle UPPER_CASE. Der nach 0 benannte Enumeratorwert sollte mit nnn_UNDEFINED = 0; enden. Ein Enumerator kann innerhalb einer anderen Message definiert werden. Der Scope ist dann nur die Message.

message Customer {
  enum CustomerType{
    UNDEFINED = 0;
    REGULAR = 1;
    MEMBER = 2;
    SPONSOR =3;
  }
  CustomerType customer_type = 1
}

Google-Richtlinien bevorzugen globale Aufzählungstypen mit dem Typ Namen als Präfix in jedem Wertefeld.

enum PhoneType {
  PHONETYPE_UNDEFINED = 0;
  PHONETYPE_MOBILE = 1;
  PHONETYPE_HOME = 2;
  PHONETYPE_WORK = 3;
}

enum Gender {
  GENDER_UNDEFINED = 0;
  GENDER_MALE = 1;
  GENDER_FEMALE = 2;
}

Der Protobuf-Compiler für C # generiert die folgenden C # enum Typen:

public enum PhoneType {
  [pbr::OriginalName("PHONETYPE_UNDEFINED")] Undefined = 0,
  [pbr::OriginalName("PHONETYPE_MOBILE")] Mobile = 1,
  [pbr::OriginalName("PHONETYPE_HOME")] Home = 2,
  [pbr::OriginalName("PHONETYPE_WORK")] Work = 3,
}

public enum Gender {
  [pbr::OriginalName("GENDER_UNDEFINED")] Undefined = 0,
  [pbr::OriginalName("GENDER_MALE")] Male = 1,
  [pbr::OriginalName("GENDER_FEMALE")] Female = 2,
}

3.3.3  Message Typ

Diese Felder definieren verschachtelte Messages mit dem Scope des Eltern Message.

message Customer {
 message Address {
    …
 }
 Repeated Address addresses = 1
}

3.3.4  Type ‘one of’

‘one of’ ist wie ‘union’ in Sprachen wie Pascal, in denen nur ein Feld einen Wert haben kann. ‘one of’ wird aus Gründen der Effizienz eingesetzt:

message Customer {
  one of access_type  {
    string email = 1;
    string username = 2;
  }
}

3.3.5  Type ‘map’

‘map’ ist ein einfaches Dictionary mit int oder string Typen.

message Customer {
 map<string, string> email_addresses = 1;
}

3.3.6  Type ‘Any’

‘Any’ verhält sich wie Variant in Basic oder var in C#.

message Customer {
  int32 id = 1;
  string username = 2;
  repeated google.protobuf.Any details = 3;
}

3.4  Feld ‘Name’

message Customer {
int32 id = 1;
string username = 2;
google.protobuf.Any repeated details = 3;
}

Es gelten folgende Namenskonventionen:

Feldnamen werden für jede Sprache im richtigen Stil kompiliert.

3.5  Feld ‘Tag’

message Customer {
int32 id = 1;
string username = 2;
google.protobuf.Any repeated details = 3;
}

Numerische Platzhalter des Feldes:

3.6  GUID ist nicht unterstützt

Implementieren als string oder wie beschrieben in https://github.com/protocolbuffers/protobuf/issues/2224.

3.7  Grosse Inhalte (z.B. Bilder, Dateien)

Grosse Inhalte bis 1 kB kann man als byte Array definieren. Grössere Inhalte sollten gestreamed werden (‘chuncking’). Streaming wird in gRPC in beide Richtungen unterstützt. In der unteren Message wird ‘imageChunk’ gestreamed.

message PersonImageMessage
{
  int32 personId = 1;
  ImageType imageType = 2;
  bytes imageChunk = 3;
}

4.  Die ‘proto’-Datei

Proto-Dateien werden am Besten in einem Verzeichnis ‘Protos’ gespeichert. Enumeratoren werden gemäss Richtlinien von Google global definiert und sind im Beispiel unten ausgelagert in der Datei ‘enums.proto’.

Noser Engineering AG Protobuf für ASP.NET Tutorial

Die Datei ‘enums.proto’:

syntax = "proto3";
option csharp_namespace = "Addressbook.Services";

enum Gender {
  GENDER_UNDEFINED = 0;
  GENDER_MALE = 1;
  GENDER_FEMALE = 2;
}

enum PhoneType {
	PHONETYPE_UNDEFINED = 0;
	PHONETYPE_MOBILE = 1;
	PHONETYPE_HOME = 2;
	PHONETYPE_WORK = 3;
}

Die ‘option csharp_namespace’ definiert den Namensraum für die generierten Klassen. Es ersetzt das mehr generische ‘package package_name’.

Hier ist der Inhalt für ‘addressBook.proto’:

syntax = "proto3";
import "enums.proto";
import "google/protobuf/timestamp.proto";
option csharp_namespace = "Addressbook.Services";

message Person {
  int32 id = 1;
  string name = 2;
  int32 age = 3;
  string email = 4;
  Gender gender = 5;
  message PhoneNumber {
    string number = 1;
    PhoneType phoneType = 2;
  }
  repeated PhoneNumber phone_numbers = 6;
  google.protobuf.Timestamp last_updated = 7;
}

message AddressBook {
  repeated Person persons = 1;
}

message AddressBookMessage {
  AddressBook addressBook = 1;
}

Mit ‘import’ werden andere Proto-Dateien importiert.

4.  Versionierung

4.1  Reservierte Tags

Im unteren Beispiel wird das Feld ‘name’ aufgeteilt in ‘first_name’ und ‘last_name’. Danach wird ein neues Feld ‘email’ hinzugefügt mit dem Tag 2. Wenn sich jetzt ein neuer Client mit einem alten Server verbindet, wird das Feld ‘email’ mit dem Inhalt vom Feld ‘name’ abgefüllt.

message Foo {
int32 id = 1;
string name = 2;
}
message Foo {
int32 id = 1;
string email = 2;
string first_name = 3;
string last_name = 4;
}

Nach der Revision soll das Schlüsselwort ‘reserved’ verwendet werden, um dem Entwickler und Compiler mitzuteilen, dass 2 nicht mehr verwendet werden darf.

message Foo {
int32 id = 1;
string name = 2;
}
message Foo {
int32 id = 1;
string email = 5;
string first_name = 3;
string last_name = 4;
reserved 2;
}

Gültige Definitionen:

4.2  Reservierte Felder

Gleicher UseCase wie oben, aber jetzt wird der Namen wiederverwendet.

message Foo {
int32 id = 1;
string full_name = 5;
}
message Foo {
int32 id = 1;
string full_name = 5;
string first_name = 3;
string last_name = 4;
}

Aus der Protobuf-Sicht gibt es kein Problem, aber wenn der Inhalt zu JSON serialisiert wird, wird ein Konflikt entstehen wenn ein neuer Client sich mit einem alten Server verbindet.

message Foo {
int32 id = 1;
string whole = 2;
}
message Foo {
int32 id = 1;
string whole_name = 5;
string first_name = 3;
string last_name = 4;
reserved «full_name»;
}

Gültige Definitionen:

5. Kompilieren

Es gibt zwei Wege um die proto-Datei zu kompilieren:

  1. Über Visual Studio 2019
  2. Manuell mit dem Tool von Google

 

5.1 Visual Studio 2019

Ab .NET Core 3.0 ist gRPC und Protobuf integriert. Die Kompilierung der proto-Dateien geschieht daher automatisch bei jeder Build-Aktion.

  1. Installiere Visual Studio 2019 Community, Professional oder Ultimate.
  2. Installiere .NET Core 3.1 (nicht .NET Framework 4.8).
  3. Kreiere die proto-Dateien im Projekt wie oben beschrieben.
  4. Installiere NuGet Package ‘Grpc.AspNetCore (v2.25.0)’.
  5. Selektiere die proto-Dateien und setzt die Build Aktion auf ‘Protobuf compiler’.

Nach dem Kompilieren befinden sich die cs-Dateien in ‘object\Debug\netcoreapp3.0\’:

Noser Engineering AG, Protobuf asp.net core tutorial

5.2 Manuell Kompilieren (nicht empfohlen)

Installiere NuGet Paketen ‘Google.Protobuf’ und ‘Google.Protobuf.Tools’.
Noser Engineering AG Protobuf Asp.NET Core Tutorial
Die NuGet-Pakete warden hier installiert:

C:\Users\[User]\.nuget\packages\google.protobuf\3.10.1
C:\Users\[User]\.nuget\packages\google.protobuf.tools\3.10.1

Entweder setzte die Path Variable zu:

C:\Users\[User]\.nuget\packages\google.protobuf.tools\3.10.1\tools\windows_x64

Oder kopiere ‘protoc.exe’ und das Unterverzeichnis ‘google’ in die Solution:

C:\Users\[User]\.nuget\packages\google.protobuf.tools\3.10.1\tools\windows_x64\protoc.exe
C:\Users\[User]\.nuget\packages\google.protobuf.tools\3.10.1\tools\windows_x64\google

Führe dann folgender Befehl aus (achtung: –csharp hat zwei ‹-‹ Zeichen):

protoc –csharp_out=ProtocolBuffers ProtocolBuffers\addressbook.proto

6.  (De-)Serialisierung

Die generierte Klasse ‘Person’ kann jetzt instanziiert und abgefüllt werden.

Person person = new Person
{
    Name = "Jan Muster",
    Age = 50,
    Gender = Gender.Male,
    Email = "[email protected]",
    PhoneNumbers =
    {
        new PhoneNumber {Number = "00 41 76 417 12 87", PhoneType = PhoneType.Mobile},
        new PhoneNumber {Number = "00 41 44 741 22 16", PhoneType = PhoneType.Home},
        new PhoneNumber {Number = "00 41 41 455 45 45", PhoneType = PhoneType.Work}
    }
};

Zur Serialisierung und Deserialisierung gib es statische und extension Methoden.

6.1  (De-)Serialization von\zu byte array

// Serializing person to byte array and back.
byte[] personByteArray = person.ToByteArray();
Person personFromByteArray = Person.Parser.ParseFrom(personByteArray);

6.2  (De-)Serialization von\zu file

// Writing person to disk and reading it again.
using (FileStream output = File.Create("person.dat"))
{
    person.WriteTo(output);
}
using (FileStream input = File.OpenRead("person.dat"))
{
    Person personToDisk = Person.Parser.ParseFrom(input);
}

6.3  (De-)Serialization von\zu JSON

// Serializing person to JSON and back.
string jsonMessage = Google.Protobuf.JsonFormatter.Default.Format((IMessage)person);
IMessage message = (IMessage)Activator.CreateInstance(typeof(Person));
Person personFromJSON = (Person)Google.Protobuf.JsonParser.Default.Parse(jsonMessage,
 message.Descriptor);

6.4  (De-)Serialization von\zu XML

Macht keinen Sinn und gibt es auch nicht.

Fazit

Protobuf ist schnell zu lernen und dank der Integration in Visual Studio 2019 einfach anzuwenden. Protocol Buffer ist die Technologie, die verwendet wird für die Serialisierung und Deserialisierung von DTOs in gRPC. In den nächsten Blogs wird gRPC und ASP.NET Core ausführlich erklärt.

Kommentare

2 Antworten zu “Tutorial: Protocol Buffers in ASP.NET Core”

  1. […] Verträge werden mit Protocol Buffers definiert. Eine Methode hat keinen Parameter (google.protobuf.Empty) oder einen Parameter vom Typ […]

  2. […] Blog «Tutorial: Protocol Buffers in ASP.NET Core» wird die Anwendung von Protocol Buffers in .NET und Visual Studio erklärt. Protocol Buffers […]

Schreiben Sie einen Kommentar

Ihre E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert

Newsletter - aktuelle Angebote, exklusive Tipps und spannende Neuigkeiten

 Jetzt anmelden
NACH OBEN
Zur Webcast Übersicht