Als ich mich mit dem Thema gRPC beschäftigt habe, war das Thema ein Monat alt gewesen. Ich dachte als Blog-Schreiber, dass das Thema für ein Artikel schon veraltet ist.
Erst 3 Monate später erschienen erste Artikel, die mich persönlich enttäuscht haben. Die meisten Artikel bezogen sich lediglich auf das Beispiel, dass quasi in Visual Studio per Default-Template schon vorgefertigt zu finden ist, den GreeterService.
Ein weiteres gutes Beispiel zu gRPC hat Hannes Preishuber in Verbindung mit Blazor verfasst:
http://blog.ppedv.de/post/gRPC-und-Blazor
Woanders bezahlt man Geld und man bekommt nur einen GreeterService!
In meinem ersten Artikel habe ich gRPC vorstellt, welche Vorteile es mit sich bringt und in welchen Szenarien gRPC gut einsetzbar ist.
In diesem Artikel möchte ich ein paar praktische Beispiele darbieten und wir bauen unseren eigenen gRPC – Service. Nebenbei möchte ich auch ein paar Raffinessen der Proto-File darstellen.
Let’s go!
In meinem Beispiel verwende ich Visual Studio 2019 mit .NET Core 3.1.
1.) Wir erstellen ein neues Projekt (File->New Project)
2.) Wählen das Template „gRPC Service“ mit aus. Danach auf Next.
3.) Für unsere Service verwenden wir als Projektnamen: EmployeeService und die Solution nenne ich GrpcSamples. Danach auf Create.
4.) In nächsten Dialog überprüfen oder passen wir die .NET Core 3.1 an. Danach auf Create
Nachdem wir die Vorlage erstellt haben, sehen wir im Solution-Explorer (Menü->View->SolutionExplorer), den viel zu oft illustrierten GreeterService.
Ich muss erwähnen, dass ich mit Begeisterung im deutschen Sprachraum mir einige Artikel gekauft habe und wurde mit dem einfachen GreeterService-Beispiel abgespeist. Das Beispiel ist Gut, um eine Einfache Unäre Kommunikation darzustellen, aber zeigt kaum die Stärken von gRPC. Des Weiteren sind auch weitere benutzerdefinierte Beispiele sehr unterlichtet. Hier möchte ich eine saubere Einstiegslinie aufzeigen!
Code First mit Proto!
Wer einen WCF – Services schon entwickelt hat, wird sich sofort in dem Code First Ansatz in gRPC zurechtfinden.
Zuerst beschreiben wir die Schnittstelle, die sich Proto-File nennt und sind in unserem Projekt im Verzeichnis Protos zu finden.
Unser Code-First Ansatz ist nicht wie in WCF ein C#-Interface mit seinen Methodenbeschreibungen, sondern gRPC hat seine eigene IDL-Language mit dazu gepackt. Der Name lautet, wie zu erahnen, Proto3.
In der greet.proto – File stehen alle Definitionen von Service und seinen RPC-Methode sowie die Message-Strukturen, die wir für unsere Request und Response verwenden.
In unserem Beispiel möchte ich die Fähigkeit aufzeigen, dass wir unsere Proto-File Definition aufteilen können und in jeweils eigene Proto-Files aufsplitten.
Für unser Beispiel benötigen wir 4 Proto-Files.
1.) EmployeeModel -> wir definieren hier unseren eigentlichen Employee-Datensatz in Form einer Proto-Message.
2.) EmployeeRequests -> Hier werden die Anfrage-Messages definiert und verwenden z.B. bei bei der Insert-Methode das EmployeeModel um einen Datensatz mit zu übertragen. Man kann in Proto die Message in eine Composition bringen.
3.) EmployeeResponse -> Wie bei EmployeeRequest, wird in EmployeeResponse die Response-Messages definiert und verwenden je nach UseCase auch das EmployeeModel.
4.) Employee -> Unsere Service Definition mit seinen ganzen RCP – Methoden. Für die Methoden Parameter stehen unser EmployeeRequest bereit und für die Antworten die EmployeeResponse.
Ein kleiner Nachteil bringt das keyword „messages“ mit sich. In der Praxis stellt das message – Schlüsselword, die Beschreibung von Request-Message und Response-Messages, allerdings möchte man nicht jedes Mal, wenn wir einen Datensatz übertragen, den Datensatz an sich auflisten. Das wäre ein redundanter Code. Hierfür kann man das keyword messages auch dafür verwenden um einen Datensatz zu deklarieren.
Also haben wir für messages zwei Beschreibungsfelder. In diesem Fall splitten wir die Proto-File in EmployeeModel.proto (für Datensätze) und in die EmployeeRequest/EmployeeResponse auf. Mit dem Zusatz, dass wir die EmployeeModel-Message als Datentypen in unseren EmployeeRequest/EmployeeResponse verwenden.
Lasst uns die Proto-File erstellen:
Um unsere erste eigene Proto-Datei zu erstellen führen sie folgende Schritte aus:
1.) klicken Sie bitte mit der rechten Maustaste auf den Ordner Protos-> Add -> New Item
2.) Wählen sie folgendes Template aus
3.) Benennt das Protocol Buffer File nach „Employee.proto“.
4.) Danach klicken Sie auf „Add“
5.) Erstellen Sie eine neue Proto-Datei mit dem Namen: EmployeeModel.proto (Schritt 1-4).
6.) Erstellen Sie eine neue Proto-Datei mit dem Namen: EmployeeRequest.proto (Schritt 1-4).
7.) Erstellen Sie eine neue Proto-Datei mit dem Namen: EmployeeResponse.proto (Schritt 1-4)
Das Proto-Verzeichnis wird dann folgendermaßen aussehen:
Zuerst öffnen wir die EmployeeModel.proto – Datei
Was wir bei allen Proto-Dateien in Zeile 1 sehen, ist die Festlegung des Syntax auf Proto3.
In Zeile 3 wird angegeben in welchen Namespace sich später die generierte C#-Klasse befindet.
Um eine Datenstruktur für unseren Employee anzulegen schreibt ihr folgenden Code:
Mithilfe des Keywords „message“ wird eine Nachricht definiert. In Proto ist es allerdings auch möglich Datensätze zu definieren.
Wichtig vor allem ist, dass man die Felder mit einem Index verzieht.
Bei einfachen Datentypen, wie bool / int32 oder string, erhöht sich der Index immer um den Wert 1. Man benötigt den Index, damit man bei der Serialisierung und Deserialisierung ein Mapping zu den Datentypen hat.
Im nächsten Schritt erweitern wir unser EmployeeModel mit einem Datentyp, den jeder C# Entwickler aus seiner Programmiersprache her kennt. Das Enum!
In Proto3 kann man mithilfe des Schlüsselwortes „enum“ eigene Datentypen erstellen. In unserem Beispiel werden wir eine Aufzählung von Akademische Titel darstellen. Wichtig ist bei den Proto3-Enums, dass man immer einen Default-Status mit in das Enum einbaut. Siehe folgender Code
In unserem Beispiel haben wir das enum Title erstellt. Der Default-Wert ist in diesem Fall, Nothing mit dem Indexwert 0.
Diesen Datentyp, können wir in unser EmployeeModel implementieren. Siehe folgender Code:
Ich habe bewusst den Enum Datentyp Title an der zweiten Position unseren EmployeeModels gesetzt, um zu demonstrieren, dass sich auch bei einem Enum der Indexwert sich nur um den Wert 1 erhöht.
Nun wechseln wir in die EmployeRequest.proto – Datei und implementieren folgenden SourceCode
Mithilfe von „import“ können wir unsere EmployeeModel.proto verfügbar machen und können auf das EmployeeModel zugreifen.
Unsere CreateEmployeeRequest dient lediglich als Wrapper.
In der EmployeeResponse.proto – Datei schaut der Proto-Code
Die CreateEmployeeResponse ist sehr schlicht gehalten und gibt mit einem einfachen bool an, ob der Datensatz auf der Serverseite auch erfolgreich angelegt wurde.
Zuletzt wechseln wir in die Employee.proto – Datei um unseren Service zu schreiben:
Zuerst importieren wir unsere vorgefertigten Messages aus den Dateien EmployeeModel.proto (Als RückgabeTyp unseren GetEmployees RPC-Methode), EmployeeRequest.proto und EmployeeResponse (beide werden für unsere CreateEmployee RPC-Methode benötigt).
In Zeile 13 beginnt unsere eigentliche Service-Definition. Hierzu möchte ich sagen, dass wir hier noch nicht den Namen EmployeeService einsetzten, weil das später zu Namenskonflikten führt. Daher den Service-Suffix im Namen weglassen.
In Zeile 14 wird mit rpc die CreateEmployee -Methode definiert und erwartet ein CreateEmployeeRequest als Parameter und gibt eine CreateEmployeeResponse an den Client zurück.
Die Create-Employee – Methode erhält eine einfache Message und gibt eine einfache Message zurück. In diesem Fall spricht man auch von einer unären RPC-Methode.
In Zeile 15: Die GetEmployees benötigt keinen Parameter und gibt lediglich eine Liste an EmployeeModels zurück.
In vielen Beispielen wird void als eine leere message nachgebaut. Google bietet allerdings void als Definition in Proto an. Man muss lediglich in Zeile wie in Zeile 11: die empty.proto importieren.
Da wir eine Liste an den Client zurückgeben, sprechen wir hier von serverseitigem Streaming.
Jetzt kommt protocol buffer ins Spiel!
Bevor wir unsere Projektmappe „rebuilden“, müssen wir unser Proto-Property Einstellungen anpassen:
1.) Rechtsklich auf eine Proto-Datei
2.) Wähle Properties aus.
3.) Setze die Einstellungen wir im Screenshot
4.) Wende diese Einstellung auch bei den anderen erstellen Proto-Dateien an.
In diesem Fall sagen wir bei Build Action, dass wir den Protocolbuf Compiler beim Kompilierungsvorgang für unsere Proto-Datei verwenden.
Die Einstellung gRPC StubClasses sagt aus, welche Art von C# generiert werden soll. Mit Server only werden nur die Service – Klassen generiert. Spielend einfach!
Wenn wir jetzt unsere EmployeeService-Projektmappe kompilieren, fängt der Protocol Buffer Compiler an die Proto-Dateien auszuwerten und generiert auf der Serverseite unseren gRPC Service in C#-Code. Protocol Buffer bietet dieses Feature auch für jede andere beliebige Programmiersprache an.
Weiter zur Service-Implementierung
Als ich meinen ersten gRPC-Service geschrieben habe, wunderte ich mich, warum ich keine generierten Files in meiner Projekt-Mappe wiedergefunden habe.
Der Protobuf-Compiler hat die C# Files in folgendes Verzeichnis gepackt: EmployeeService\obj\Debug\netcoreapp3.1.
Dass die erstellen Klassen nicht in der Projektmappe ausfindig sind, ist auch gut so, weil die Klassen sich immer wieder auf neue neu generieren, wenn man die Projektmappe neu kompiliert.
In unserem Beispiel erstellen wir im Projekt-Ordner Service eine neue Klasse mit dem Namen EmployeeService.
Im nächsten Schritt erben wir von dem eigentlich generierten Code der uns die Protocol Buffer Engine generiert hat.
Wie leiten EmployeeService von der Klasse Employee.EmployeeBase ab. Zusätzlich wird noch ein weiteres wird noch der Namespace EmployeeService.Protos mit using hinzugefügt.
Damit wir eine Datenbank simulieren, verwenden wir eine Liste von unseren EmployeModel als DB-Mock.
Als nächstes rufen wir public override und bekommen diese Methoden zum überschreiben angeboten:
Wir sehen nun die in Proto-Abgebildeten RPC-Methoden, als angebotene virtual – Methoden. Diese überschreiben wir sehr gerne.
Die zweite Methode GetEmployees streamen wir an den Client eine Liste aller Employees.
Damit wir mithilfe von IServerStreamWriter.WriteAnsync eine Liste an den Client streamen können, müssen wir unsere überrschrieben GetEmployees-RPC Methode als async umdefinieren.
An dieser Stelle ist auch zu erwähnen, dass der Empty Datentyp in der Parameterliste der RPC-Methode in unserem C# Code aus dem Namespace Google.Protobuf.WellKnownTypes stammt.
Die letzte Codezeile müssen wir in der Startup.cs schreiben.
Hier registrieren wir unseren Service als Request-Endpunkt in der Configure-Methode.
Danach sollte unserer Service lauffähig sein.
Lass uns den Client schreiben!
Zuerst erstellen wir in unserer Solution ein neues Projekt und wähle eine Konsolen-Anwendung.
Bennen diese nach EmployeeClient und klicken auf Create.
Bevor wir unsere erste Zeile Code schreiben klicken wir im SolutionExplorer mit einem Rechtsklick auf unser EmployeeClient-Projekteintrag und wählen Manage NuGet Packages aus.
Hier installieren wir folgende Packages nach fester Reihenfolge
Grpc.Net.Client:
Grpc.Tools
Google.Protobuf
Danach klicken mit einem Rechtsklick auf unser EmployeeClient-Projekteintrag und erstellen einen Ordner mit dem Namen Protos.
Der Service ist ja Serverseitig beschrieben, aber der Client weiß nichts von seinen Methoden.
Daher werden Proto-Files auch auf Client-Seite eingesetzt und haben eben den benötigten Ordner für unseren folgenden Schritt angelegt.
Wir kopieren unsere erstellen Proto-Dateien vom EmployeeService-Projekt zu unserem EmployeeClient.
Wir markieren im EmployeeClient alle unsere Employee-Proto Dateien.
Danach Rechtsklick->Properties.
Hier müssen wir den Proto-Files angeben, das die gRPC-Stub Klassen auf Client-Seite generiert werden.
Danach können wir unseren ersten zwei Zeilen Code in der Programm.cs schreiben.
Wir bauen zuerst einen Channel auf und übergeben diesen an unseren EmployeeClient.
Wir ersten uns ein CreateEmployeeRequest-Objekt und instanziieren und initiieren das EmployeeModel-Objekt.
Beim eigentlichen Aufruf werden zwei CreateEmployeeAsync und CreateEmployee angeboten. Für den asynchronen Aufruf muss noch die Main-Methode mit async Task verändert werden.
Vor unserem ersten Test müssen folgendes tun.
1.) In der SolutionExplorer mit einem Rechtsklick auf die Solution
2.) Set Startup Projects… auswählen
In der folgenden Dialogbox müssen wir folgendes tun:
1.) Wähle Multiple startup projects aus
2.) Setze den Service in der Startreihenfolge an erster Stelle
3.) Wähle bei beiden Projekten den Wert von Action auf Start!
Das ist auch unser Stichwort. Wir können jetzt unsere erste Methode testen
Wer nicht mit dem Debugger die Kommunikation mitverfolgen möchte kann auch in der Service-Methode die Request-Anfrage erleben und es wird der HTTP – Code ausgegeben.
Auf der Clientseite kann mit mithilfe des Debuggers sich das CreateEmployeeResponse – Objekt anschauen und sehen, dass wir die Success-Property auf True gesetzt und an den Client übermittelt haben.
Unser zweite Methode die wir implementieren ist eine Auflistung aller Employees.
Dafür schreiben wir folgenden Code:
Wenn man die zweite Methode testet merkt man, dass unsere Mock-Liste nicht 5-Einträge groß ist.
Bei einer Datenbankanbindung sollte dieser Effekt nicht auftreten.
Hier nochmal die Komplette Übersicht zu unserer Program.cs