WCF-Alternativen (Teil 3) – Eine Anleitung zur Migration von WCF zu gRPC

In diesem Blogpost der Artikelserie zu den Alternativen der Windows Communication Foundation (WCF) werden die Besonderheiten und Herausforderungen einer WCF-Migration als Vorbereitung zu einer späteren Portierung der Anwendung auf .NET Core beschrieben.

Nachdem im letzten Artikel ASP.NET Core Web API als Alternative vorgestellt wurde, wird in diesem Beitrag auf gRPC als eine weitere Möglichkeit eingegangen. Auch hier soll Schritt für Schritt ein mögliches Vorgehen bei der Migration von WCF zu gRPC beschrieben werden.

Vorgehen bei der Migration

In der Regel existiert ein separates WCF-Projekt in der Solution. Da eine direkte Umstellung nicht möglich ist, kann dieses Projekt zunächst unverändert in der Solution verbleiben.

Zunächst sollte ein neues Class-Library-Projekt für Shared-Objekte zwischen Server und Client angelegt werden. In dieses Projekt werden die ServiceContract Interfaces sowie die DataContract-Klassen aus dem WCF-Projekt kopiert und die WCF-spezifischen Attribute wie „ServiceContract“, „OperationContract“, „DataContract“, „DataMember“ usw. entfernt.

Client-Projekt

Im WCF Service konsumierenden Projekt wird als erstes die WCF-Service-Referenz entfernt. Auch können die WCF-spezifischen Attribute wie „CallbackBehavior“ o. Ä. entfernt werden.

Eine neue Referenz zum zuvor angelegten Class-Library-Projekt für die Shared-Objekte wird hinzugefügt. Als nächstes kann im Client-Projekt eine leere Implementierung des jetzt im Class-Library-Projekt abgelegten ServiceContract Interfaces erstellt werden. Die „alte“ Initialisierung des WCF Service wird jetzt auf die noch leere Implementierung des ServiceContract geändert.

Abschließend müssen noch die Usings für die zuvor verwendeten DataContract-Klassen aus dem WCF Service auf das neue Class-Library-Projekt geändert werden. Damit sollte sich das Client-Projekt wieder kompilieren lassen. Um das Projekt auch wieder starten zu können, ist der Teil <system.serviceModel> aus der *.config zu entfernen.

Erstellung der Schnittstellenbeschreibung mit Protocol Buffers

Bei gRPC wird die Schnittstelle mit der Protocol Buffer Language in *.proto-Dateien beschrieben. Am besten wird die *.proto-Datei im neu angelegten Class-Library-Projekt hinzugefügt. Um später daraus Server- und Client-Klassen generieren zu können, müssen zusätzlich die NuGet-Pakete „Google.Protobuf“, „Grpc.Core“ und „Grpc.Tools“ hinzugefügt werden.

Nach Anlage der *.proto-Datei muss diese in der *.csproj-Datei im Knoten „ItemGroup“ durch folgende Zeile bekannt gemacht werden.

<Project Sdk="Microsoft.NET.Sdk">
 
  <ItemGroup>
    <Protobuf Include="ProtoZeitService.proto" GrpcServices="Both" />
 
  </ItemGroup>
 
</Project>

Definition der *.proto in *.config Datei


Aufbau der *.proto Datei

Nachfolgend ist ein Beispiel zu sehen, wie eine WCF-Service-Beschreibung in eine *.proto-Datei überführt werden kann.

Die [ServiceContract]-Attribute werden zu „Service“ und aus [OperationContract] werden „rpc“-Aufrufe. Die als [DataContract] gekennzeichneten Klassen werden zu „message“-Objekten.

[ServiceContract]
public interface IDataInputService
{
    [OperationContract]
    int CreateUser(User user);
 
    [OperationContract]
    int Login(User user);
 
    [OperationContract]
    List<Time> GetTimes(int userId);
 
    [OperationContract]
    void AddTime(Time time, int userId);
 
    [OperationContract]
    List<string> Projects();
}
 
[DataContract]
public class User
{
    [DataMember]
    public string Name { get; set; }
 
    [DataMember]
    public string Passwort { get; set; }
}
 
[DataContract]
public class Time
{
    [DataMember]
    public DateTime Start { get; set; }
 
    [DataMember]
    public DateTime End { get; set; }
 
    [DataMember]
    public string Project { get; set; }
 
    [DataMember]
    public int uId { get; set; }
 
    [DataMember]
    public int Id { get; set; }
}

Beispiel eines zu migrierenden WCF ServiceContract und DataContract


syntax = "proto3";
 
import "google/protobuf/timestamp.proto";
import "google/protobuf/Empty.proto";
 
option csharp_namespace = "DataInputt.ZeitService.Api";
 
service DataInputService {
    rpc CreateUser (UserDto) returns (UserResponse) {}
    rpc Login (UserDto) returns (UserResponse) {}
    rpc GetTimes (GetTimesRequest) returns (TimeCollection) {}
    rpc AddTime (AddTimeRequest) returns (google.protobuf.Empty) {}
    rpc Projects (google.protobuf.Empty) returns (ProjectCollection) {}
}
 
 
message UserDto {
    string name = 1;
    string passwort = 2;
}
 
message TimeDto {
    google.protobuf.Timestamp start = 1;
    google.protobuf.Timestamp end = 2;
    string project = 3;
    int32 uid = 4;
    int32 id = 5;
}
 
message UserResponse {
    int32 id = 1;
}
 
message GetTimesRequest {
    int32 userId = 1;
}
 
message TimeCollection {
    repeated TimeDto times = 1;
}
 
message AddTimeRequest {
    TimeDto time = 1;
    int32 userId = 2;
}
 
message ProjectCollection {
    repeated string projects = 1;
}

Beispiel der erstellten gRPC *.proto-Datei


Bei der Erstellung der *.proto-Datei sollten folgende Punkte berücksichtigt werden.

Angabe Namespace

Damit die generierte Server- und Client-Implementierung den korrekten Namespace erhält, sollte dieser in der *.proto-Datei angegeben werden.

Definition Übergabe/Rückgabe Parameter

Bei gRPC-Schnittstellen sind nur Aufrufe mit einem einzigen Paramater zugelassen. Wird im WCF Service mit mehreren Übergabeparametern gearbeitet, müssen diese in einem neuen Message-Objekt zusammengefasst werden.

Jeder Aufruf einer gRPC-Schnittstelle muss auch einen Rückgabewert haben. Gab es im WCF Service void-Methoden, so müssen diese bei gRPC jetzt den speziellen Typ „google.protobuf.Empty“ zurückgeben.

Des Weiteren darf für die Übergabe und Rückgabe kein einzelner primitiver Datentyp (int, bool, string) verwendet werden. Soll für die Rückgabe nur ein int oder string verwendet werden, so muss auch dafür ein extra Message-Objekt erstellt werden.

Rufen sich im WCF Service Methoden gegenseitig auf, war das bei der Verwendung eines primitiven Datentyps sehr einfach. Soll das auch in der gRPC-Schnittstelle möglich sein, ist darauf zu achten, dass die betreffenden Methoden dieselben Message-Objekte verwenden. So kann ein unnötiges Mapping vermieden werden.

Bezeichnung der Message-Objekte

Bei der Bezeichnung der Message-Objekte sollten diese besser nicht 1:1 von den DataContract-Klassen des WCF Service übernommen werden. Der Grund dafür ist, dass die später aus der Definition generierten C#-Klassen teilweise andere Datentypen verwenden und für die Verwendung erst gemappt werden müssen. Um diese dann besser von den DataContract-Klassen zu trennen, empfiehlt sich hier eine differenzierte Bezeichnung.

Außerdem müssen die Properties innerhalb der Message-Objekte fortlaufend nummeriert werden.

Datentypen in Message-Objekten

Aus den Message-Objekten der *.proto-Datei werden automatisch C#-Klassen generiert. Hier sollte man sich bewusst sein, dass nicht immer die Standard-C#-Datentypen für die Generierung verwendet werden.

So wird aus dem in der *.proto-Datei angegebenen Typ google.protobuf.Timestamp in der C#-Klasse der Typ Google.Protobuf.WellKnownTypes.Timestamp, welcher bei Verwendung immer erst in ein DateTime umgewandelt werden muss.

Wird in der *.proto-Datei „repeated“ angegeben, wird daraus kein List<T> sondern ein Google.Protobuf.Collections.RepeatedField<T>, welches auch entsprechend gemappt werden muss.

Auch andere Typen wie z. B. Dictionary<K, V> haben in der *.proto-Datei und der später generierten C#-Klasse jeweils andere Typen. Der C#-Typ „decimal“ wird aktuell aufgrund fehlender Rundungsgenauigkeit von der *.proto-Datei noch gar nicht unterstützt. Als Workaround wird empfohlen, sich ein eigenes Decimal-Message-Objekt zu erstellen, welches Vor- und Nachkommastellen als separate int-Werte abbildet.

Anlage des gRPC-Server-Projekts

Das gRPC-Server-Projekt kann als einfache Konsolenanwendung erstellt werden und sollte einen Verweis auf das zuvor neu angelegte Class-Library-Projekt mit der *.proto-Datei haben.

Um den Server zu starten, sind nur wenige Zeilen Code nötig:

static void Main(string[] args)
{
    const int port = 9000;
    const string host = "0.0.0.0";

    Grpc.Core.Server server = new Grpc.Core.Server
    {
        Services = { DataInputt.ZeitService.Api.ZeitService.BindService(new ZeitService()) },
        Ports = { new Grpc.Core.ServerPort(host, port, Grpc.Core.ServerCredentials.Insecure) }
    };
    server.Start();

    Console.WriteLine($"Starting server {host}:{port}");
    Console.WriteLine("Press any key to stop...");
    Console.ReadKey();
}

Beispiel für den Start eines gRPC Server


Es müssen lediglich Host und Port definiert sowie dem im Class-Library-Projekt durch die *.proto-Datei generierten Service eine Implementierung zugewiesen werden. Dabei sollte die Implementierung im gRPC-Server-Projekt liegen.

Implementierung des gRPC Service

Die Implementierung des gRPC Service erfolgt durch das Erben der im Class-Library-Projekt durch die *.proto-Datei generierten ServiceBase. Die einzelnen Serviceaufrufe können dann durch ein override implementiert werden.

public class ZeitService : DataInputt.ZeitService.Api.ZeitService.ZeitServiceBase
{
    public override Task<UserResponse> CreateUser(UserDto request, ServerCallContext context)
    {
         
    }
 
    public override Task<UserResponse> Login(UserDto request, ServerCallContext context)
    {
         
    }
 
    public override Task<TimeCollection> GetTimes(GetTimesRequest request, ServerCallContext context)
    {
         
    }
 
    public override Task<Empty> AddTime(AddTimeRequest request, ServerCallContext context)
    {
         
    }
 
    public override Task<ProjectCollection> Projects(Empty request, ServerCallContext context)
    {
         
    }   
}

Beispiel für die Server-Implementierung eines gRPC Service


Wird für die Service-Implementierung der „alte“ WCF Code verwendet, kann ein Mapping der Parameter nötig sein, wenn die Datentypen nicht zu den „alten“ DataContract-Klassen passen.

Wichtig ist auch zu wissen, dass sich der Lifecycle der Service-Implementierung über die gesamte Laufzeit des gRPC Service erstreckt (Singelton). Anders als bei einem Web API Controller wird nicht für jeden Request eine neue Instanz der Service-Implementierung erstellt. Somit bleibt der Zustand des gRPC Service zwischen den Aufrufen erhalten. Klassenvariablen und im Konstruktor erstellte oder injizierte Ressourcen sollten daher besser vermieden werden, da deren Zustand sonst zwischen den Aufrufen ggf. nicht sichergestellt werden kann.

Implementierung des gRPC Clients

Für die Implementierung des gRPC Clients kann die im konsumierenden Projekt angelegte leere Implementierung des ServiceContract Interfaces genutzt werden. Hier muss zunächst eine Verbindung zum gRPC Server hergestellt werden.

const int port = 9000;
string host = Environment.MachineName;
 
var channel = new Channel(host, port, ChannelCredentials.Insecure);
var grpcClient = new ZeitService.Api.ZeitService.ZeitServiceClient(channel);

Beispiel eines Clients zum Herstellen der Verbindung zum gRPC Server


Auch hier kommt eine aus der im Class-Library-Projekt durch die *.proto-Datei generierte Client-Klasse zum Einsatz. Durch diese werden die in der *.proto-Datei definierten Aufrufe bereitgestellt.

Jetzt muss die leere Implementierung des ServiceContract Interfaces durch die entsprechenden Aufrufe der gRPC-Client-Klasse ergänzt werden. Auch hier kann es nötig sein, die vom gRPC Service verwendeten Über- und Rückgabeparameter auf die bisherigen DataContract-Klassen zu mappen.

Durch die Verwendung und Implementierung des „alten“ WCF Service Interface muss im konsumierenden Projekt nichts weiter angepasst und geändert werden.

Bidirektionale Kommunikation

Das Konzept der bidirektionalen Kommunikation in gRPC unterscheidet sich stark von WCF Duplex Services.

Bei WCF kann der Server über Callback Interfaces sehr einfach verschiedene Methoden auf Client-Seite aufrufen. Hingegen wird bei gRPC vom Client ausgehend eine Server-Methode angesprochen, welche als Stream Daten an den Client zurückliefert.

Dazu muss die gRPC-Server-Methode so implementiert werden, dass diese nicht beendet und somit die Verbindung aufrechterhalten wird. Anschließend kann zum Beispiel durch Events die Übertragung von Daten an den Client ausgelöst werden.

Auch beim Client muss nach dem Aufruf der Server-Methode die Verbindung aufrechterhalten und auf den Empfang neuer Daten reagiert werden.

Somit sind für eine Umstellung auf gRPC Streaming sehr rundlegende und konzeptionelle Anpassungen nötig.

Nicht betrachtet wurden Querschnittsfunktionen wie Authentifizierung, Autorisierung, Logging und Fehlerbehandlung der gRPC-Aufrufe – diese Punkte sollten im Einzelfall geprüft und ggf. angepasst werden.

Fazit

Die Umstellung von WCF auf gRPC ist verglichen mit ASP.NET Core Web API deutlich aufwendiger und mit mehr Code-Anpassungen verbunden. Zunächst muss eine *.proto-Datei erstellt werden. Durch die Vorgabe, dass jeder Serviceaufruf eine Rückgabe haben muss und max. ein Übergabeparamater zulässig ist, sind teilweise Anpassungen an den Methodensignaturen nötig. Da in den generierten Klassen teilweise keine .NET-Standard-Typen verwendet werden, muss jede Server- und Client-Methode mit dem entsprechenden Mapping-Code ergänzt werden.

Bei der Verwendung von gRPC sollte auch unbedingt berücksichtigt werden, dass sich der Lifecycle der Serviceinstanz über die gesamte Ausführungszeit des gRPC Servers erstreckt (Singelton).

Dieser Beitrag wurde verfasst von: