In diesem Artikel

Wie in allen anderen browserbasierten Single-Page-Application (SPA) Frameworks, ist Blazor WebAssembly JSON-over-HTTP (über Web- oder REST-APIs) die bei weitem häufigste Methode, um Daten auszutauschen und serverseitige Vorgänge auszulösen. Der Client sendet eine HTTP-Anfrage mit JSON-Daten an eine URL, mitunter über unterschiedliche HTTP-Verben. Anschließend führt der Server eine Operation aus und antwortet mit einem HTTP-Statuscode und den resultierenden JSON-Daten. Warum sollte man das ändern? Nun, es gibt Gründe - vor allem wenn man in einem geschlossenen System ist und .NET sowohl im Frontend als auch im Backend einsetzt.

Web APIs funktionieren im Allgemeinen gut, es fallen jedoch Punkte auf, die eventuell Verbesserung bedürfen:

  • JSON ist ein sehr ausführliches Datenformat. Es ist nicht für die Bandbreite optimiert. Hier könnte je nach Anwendungsfall ein binärer Austausch besser sein.
  • Es gibt keinen sinnvollen Mechanismus, der garantiert, dass alle Details zu URLs, HTTP-Methoden, Statuscodes, Datenstrukturen (usw.) tatsächlich zwischen Server und Client konsistent sind.
  • In einem Umfeld, wo man sowohl die Client- als auch die Service-/API-Entwicklung kontrolliert, kann man durchaus mehr in Richtung streng typisierter Kommunikation gehen.

In diesem Artikel schauen wir uns eine andere Möglichkeit an, wie die Kommunikation zwischen Server und Clients realisiert werden kann. Anhand einer ToDo-Beispielanwendung betrachten wir die Implementierung und Kommunikation mit gRPC und gRPC-Web. Im Sinne einer kontrollierten Umgebung implementieren wir den Client als Blazor WebAssembly SPA und den Server und die APIs als ASP.NET-Core-Anwendung - jeweils mit .NET 5.

Was ist gRPC / gRPC-Web?

gRPC ist ein modernes leichtgewichtiges Kommunikationsprotokoll von Google das als Open-Source-RPC Framework bereitgestellt wird. Wie in vielen RPC-Systemen basiert gRPC auf der Idee, einen Service zu definieren und die Methoden anzugeben, die mit ihren Parametern und Rückgabetypen remote aufgerufen werden können. gRPC verwendet HTTP/2, um leistungsstarke und skalierbare APIs zu unterstützen. Je nach Art und Größe der Daten wird durch die Verwendung eines Binärformats anstelle von Text bei der Übertragung von Daten die Nutzlast so kompakt und effizient wie möglich gehalten. Auf der Serverseite implementiert der Server diese Schnittstelle und führt einen gRPC-Server aus, um Clientaufrufe zu verarbeiten. Auf der Clientseite verfügt der Code über einen Stub (in einigen Sprachen nur als Client bezeichnet), der dieselben Methoden wie der Server bereitstellt. Der Client kann dann mit Hilfe des Stub die Methoden aufrufen und die streng typisierten Parameter übergeben. Die Parameter werden dann je nach Kommunikationsart, welche wir in einem späteren Abschnitt näher betrachten, als Proto-Request(s) verpackt. Abschließend kümmert sich dann gRPC darum, die Anforderung(en) an den Server zu senden und die Proto-Response(s) des Servers zurückzugeben.

gRPC Kommunikation zwischen Server und Stub

gRPC & Protocol Buffers

Für die Zwecke einer SPA können wir uns gRPC als Alternative zu JSON-over-HTTP vorstellen. Dabei können wir die oben aufgeführten Schwachstellen adressieren:

  • gRPC mit Protocol Buffers ist für minimalen Netzwerkverkehr optimiert und sendet effizient binär serialisierte Nachrichten.
  • Da über typisierte Schnittstellen entwickelt wird, können wir beim Kompilieren garantieren, dass sich Server und Client darüber einig sind, welche Endpunkte vorhanden sind und welche Datenform gesendet und empfangen wird, ohne URLs, Statuscodes, usw. kennen und angeben zu müssen.

Die hausgemachte Vorgehensweise bei gRPC dreht sich immer um eine plattformunabhängige Schnittstellenbeschreibung. Diese wird mit Protocol Buffers (auch als protobuf bekannt) realisiert. Um einen gRPC-Dienst zu erstellen, wird zuerst eine .proto-Datei erstellt, die eine sprachunabhängige Beschreibung einer Reihe von RPC-Diensten und ihrer Datenformen ist.

Hier ein kleines Beispiel einer solchen .proto-Datei:

syntax = "proto3";
option csharp_namespace = "ToDoGrpcService";
import "google/protobuf/empty.proto";

package ToDo;

service ToDoService {
    rpc GetToDosAsync(google.protobuf.Empty) returns (ToDoItems);
    rpc GetToDoItemAsync(ToDoIdQuery) returns (ToDoData);
    rpc AddToDoItemAsync(ToDoData) returns (ToDoRequestResponse);
    rpc UpdateToDoItemAsync(ToDoPutQuery) returns (ToDoRequestResponse);
    rpc DeleteToDoItemAsync(ToDoIdQuery) returns (ToDoRequestResponse);
}

message ToDoData {
    int32 Id = 4;
    string Title = 1;
    string Description = 2;
    bool Status = 3;
}

message ToDoIdQuery {
    int32 Id = 1;
}

message ToDoItems {
    repeated ToDoData ToDoItemList = 1;
}

message ToDoPutQuery {
    ToDoData ToDoDataItem = 1;
    int32 Id = 2; 
}

message ToDoRequestResponse {
    string StatusMessage = 1;
    bool Status = 2;
    int32 StatusCode = 3;
}

Sieht eigentlich recht vertraut aus. Da wir uns in diesem Artikel dem Code-First Ansatz widmen, werden wir nicht näher auf die Details der .proto-Datei eingehen. Weitere Informationen über den Aufbau von proto-Dateien finden sich hier.

gRPC-Web

Derzeit ist es jedoch leider nicht möglich, gRPC direkt von Browser-basierten Anwendungen aus zu verwenden. Denn der Browser besitzt keine APIs, um die Anforderungen der HTTP/2-gRPC-Spezifikation vollständig zu implementieren.

Die Lösung für dieses Problem ist gRPC-Web. Die Grundidee bei gRPC-Web besteht darin, dass der Browser eine HTTP/1.1- oder HTTP/2-Anfrage mit Fetch oder XHR sendet und auf der Serverseite es einen kleinen Proxy vor den gRPC-Backend-Diensten gibt, um die Requests und Responses während der Kommunikation dann in natives gRPC zu übersetzen. So bringt gRPC-Web viele der Funktionen von gRPC, wie kleine binäre Nachrichten und schnittstellenbasierte APIs, in moderne Browser-Apps wie SPAs.

In diesem Beitrag schauen wir uns an, wie ein gRPC-Web-Endpunkt in einem ASP.NET Core Server implementiert und in einer Blazor WebAssembly-Anwendung verwendet wird.

Demoanwendung

Bei der Beispielanwendung handelt es sich um eine einfache ToDo-Anwendung, wie man sie bei dem ein oder anderen Tutorial schon einmal gesehen haben könnte. Der gesamte Source Code zur Beispielanwendung findet sich hier.

Version Information

  • .NET SDK: 5.0.201
  • ASP.NET Core: 5.0.4
  • ASP.NET Core Blazor WebAssembly: 5.0.4
  • MudBlazor: 5.0.5

Die Anwendung besteht aus drei Projekten:

  • ASP.NET Core API
  • Shared Library
  • Blazor WebAssembly Client

VS Solution Explorer

Betrachten wir den Solution Explorer etwas genauer, sehen wir, dass sowohl die API als auch der Client die Bibliothek GrpcToDo.Shared nutzten. Im weiteren Verlauf des Artikels, werden wir sehen, dass dies einen großen Vorteil haben kann, da wichtige Elemente zur Kommunikation zwischen Server und Client, wie z.B. Models oder Service-Schnittstellen, von beiden Projekten genutzt werden können und nicht selbst implementiert werden müssen.

Kommunikationsmuster in gRPC

Es gibt bei gRPC prinzipiell vier verschiedene Arten der Kommunikation:

  • Unär: Aufrufe, bei denen der Client eine einzelne Anforderung an den Server sendet und eine einzelne Antwort zurückerhält, wie wir es auch von einem normalen Funktionsaufruf kennen.
  • Server-Streaming: Hier sendet der Client einen Request an den Server. Der Server antwortet mit einem Stream, auf welchem so lange Nachrichten an den Client gesendet werden, bis der Server den Stream beendet. Natürlich kann der Stream auch durch einen Verbindungsabbruch durch den Client beendet werden.
  • Client-Streaming: Hier schreibt der Client eine Folge von Nachrichten und sendet diese unter Verwendung eines bereitgestellten Streams an den Server. Sobald der Client die Nachrichten fertig geschrieben hat, wartet er darauf, dass der Server sie liest und seine Antwort zurückgibt.
  • Duplex: Bei dieser Art der Kommunikation werden zwei unabhängige Streams zwischen Client und Server geöffnet. Sowohl der Client als auch der Server können Nachrichten senden und empfangen. Der Server muss hier nicht direkt bei jeder Nachricht eine Antwort an den Client senden, sondern kann dies zum Beispiel auch erst nachdem er alle Nachrichten erhalten hat.

In diesem Artikel werden wir uns jedoch nur die unäre und die Server-Streaming-Kommunikation näher anschauen. Weitere Informationen zu den anderen Kommunikationsarten finden sich hier.

Definieren der Service-Schnittstellen

Es gibt zwei grundlegende Möglichkeiten, gRPC-Dienste einer Anwendung hinzuzufügen. Auf der einen Seite gibt es den Ansatz von Contract-First, auf der anderen Seite den Code-First Ansatz.

Contract-First

Beim Contract-First Ansatz werden im ersten Schritt plattform- und programmiersprachenunabhängige .proto-Dateien erstellt. Diese definieren die message-Objekte und die service-Objekte. Ein Beispiel haben wir bereits weiter oben gesehen. Die message-Objekte definieren das Model, die zur Kommunikation zwischen Server und Client genutzt werden. Die service-Objekte definieren die Methoden, die zur Kommunikation genutzt werden können. Aus diesen plattformunabhängigen Beschreibungen werden dann im Fall von C# über einen MSBuild Task C#-Klassen generiert. Je nach Konfiguration des Tasks kann man Client Code, Server Code oder Code für beide Seiten generieren.

Doch warum sollte man das tun? Warum ein Umweg? Wenn man beide Seiten der Kommunikation voll kontrolliert dann könnte man doch idealerweise einfach die Schnittstellen mit .NET Code definieren, oder? Denn Cross-Plattform und Sprachunabhängigkeit ist hier nicht notwendig oder gewünscht.

Code-First

Genau hier kommt der Code-First Ansatz ins Spiel. Mit ihm ist es möglich, service-Objekte und message-Objekte in C# Code zu definieren, anstelle von .proto-Dateien. Dies bietet den großen Vorteil, dass Klassen und Methoden über eine Shared Library als DLL geteilt werden können. Der ASP.NET Server Code nutzt die gleiche DLL wie der Blazor WebAssembly Client. Dadurch entsteht ein weiterer praktischer Vorteil: mit den streng typisierten .NET-Klassen hat sowohl der Client- als auch der Server-Entwickler volle IntelliSense und Typsicherheit während der Entwicklungszeit.

Code-First gRPC in .NET

Da wir durch den Code-First Ansatz, wie wir im letzten Abschnitt schon sehen konnten, jede Menge an Vorteile in der Implementierung von gRPC gewinnen können, werde ich mich im weiteren Verlauf des Artikels ausschließlich auf den Code-First-Ansatz beziehen und nicht weiter auf den Contract-First-Ansatz eingehen. Bei weiterem Interesse zum Contract-First-Ansatz, finden sich hier weitere Informationen dazu.

Um den Code-First-Ansatz nutzen zu können, benötigen wir in allen Projekten eine Open-Source-Bibliothek: protobuf-net.Grpc-Paket (Code). Ist das Paket installiert, können wir uns im ersten Schritt die Shared Library der Demoanwendung genauer anschauen. Hier werden die Models und Services implementiert.

Zusätzlich werden noch die folgenden Pakete zum Erstellen von Models bzw. Services benötigt:

  • protobuf-net.Grpc
  • System.ServiceModel.Primitives

Um Data- oder Service-Contracts zu definieren, erstellen wir eine Klasse. In unserem Beispiel verwenden wir hierfür die Klasse ToDoData.

[DataContract]
public class ToDoData
{
    [DataMember(Order = 1)]
    public int Id { get; set; }
    
    [DataMember(Order = 2)]
    public string Title { get; set; }
    
    [DataMember(Order = 3)]
    public string Description { get; set; }
    
    [DataMember(Order = 4)]
    public bool Status { get; set; }
}

Wie wir im obigen Code Sample sehen, wird der Klasse das Attribut DataContract hinzugefügt.

Hinweis: Wer aus der WCF-Welt kommt, dem könnte das ein oder andere hier sicherlich bekannt vorkommen. Ähnlich wie in WCF können die DataContract-Klassen mit dem Attribut DataContract versehen werden. Aber auch die Properties können ein Attribut nutzen, welches in der WCF-Welt sehr bekannt ist: DataMember. Doch nicht nur DataContract und DataMember werden genutzt, sondern auch das ServiceContract-Attribut findet sich im Code-First-Ansatz wieder.

Die einzelnen Properties werden mit dem DataMember-Attribut versehen. Das DataMember-Attribut hat den Parameter Order, das zur Identifikation in der binären Nachricht genutzt wird.

Um die Properties eines DataContracts bei der Serialisierung bzw. Deserialisierung identifizieren zu können, nutzt Protobuf die Order. Daher ist es wichtig, dass die Order im Nachhinein nicht mehr geändert wird. Mehr Informationen und Details zur Order finden sich hier.

Ein Data Contract kann beliebig komplex sein, einschließlich Listen, Arrays usw. Wichtig ist nur, dass das Objekt in einer Baumstruktur aufgebaut ist und keine Graphen abbildet.

Request-Response-Kommunikation: der ToDoService

Nachdem wir wissen, wie wir einen Data Contract erstellen können, widmen wir uns dem Service Contract. Service Contracts sind Schnittstellen, die mit dem ServiceContract-Attribut versehen werden. Schauen wir uns mal die Klasse ToDoService aus der Beispielanwendung näher an.

[ServiceContract]
public interface IToDoService
{
    Task<ToDoData[]> GetToDosAsync();
    Task<ToDoData> GetToDoItemAsync(ToDoIdQuery query);
    Task<ToDoRequestResponse> AddToDoItemAsync(ToDoData data);
    Task<ToDoRequestResponse> UpdateToDoItemAsync(ToDoData data);
    Task<ToDoRequestResponse> DeleteToDoItemAsync(ToDoIdQuery query);
}

Im obigen Code sehen wir den ToDoService, welcher im Code-First-Ansatz erstellt wurde. Doch im Vergleich zum Service mit dem Contract-First-Ansatz, können im Code-First-Ansatz Listen bzw. Arrays direkt zurückgegeben werden. Dies können wir in der Methode GetToDosAsync sehen, welche im Contract-First-Ansatz das Objekt ToDoItems returniert. Dieses Objekt wiederum beinhaltet eine Liste von ToDoData. Hier im Beispiel gibt die Methode direkt ein Array des Typen ToDoData zurück.

Wenn wir uns eine der Methoden genauer anschauen, sehen wir, dass es eine unäre Verbindung zwischen dem Server und dem Client ist. Es ist deshalb eine unäre Verbindung, da nur der Client mit dem Server kommuniziert, der dann eine Antwort an den Client liefert: Der Client sendet mit beispielsweise GetToDosAsync eine Eingabe an den Server und der Server antwortet mit einem ToDoData[]-Objekt als Ausgabe.

Streaming von Daten: der TimeService

Der zweite Service in der Beispielanwendung ist der ITimeService. Dieser stellt die Methode SubscribeAsync bereit, der immer die aktuelle Zeit zurückgibt.

[ServiceContract]
public interface ITimeService
{
    IAsyncEnumerable<TimeResult> SubscribeAsync(CallContext context = default);
}

Hierbei handelt es sich um ein Server-Streaming Event, das bedeutet der Client sendet einmalig eine Eingabe an den Server und der Server antwortet mit einer Folge von Ausgaben.

Nachdem wir uns exemplarisch angeschaut haben, wie Data Contracts und Service Contracts erstellt werden, schauen wir uns im nächsten Schritt an, wie wir diese implementieren und dem Server hinzufügen können.

gRPC in ASP.NET Core

Im Server sind mehrere Schritte zu implementieren, um dem Client über den Server eine gRPC-Schnittstelle bereitzustellen.

Server-Konfiguration

In der Startup.cs fügen wir der ServiceCollection den Code-First-Ansatz von gRPC hinzu.

public void ConfigureServices(IServiceCollection services)
{
    // ...
    services.AddGrpc();
    services.AddCodeFirstGrpc(config => { config.ResponseCompressionLevel = CompressionLevel.Optimal; });
}

Mit der Methode AddGrpc wird gRPC dem Server grundlegend hinzugefügt. Die andere Methode AddCodeFirstGrpc fügt Code-First-Unterstützung hinzu, um direkt mit den Data- und Service Contracts arbeiten zu können. In der Konfiguration für AddCodeFirstGrpc setzen wir den ResponseCompressionLevel. Dies bewirkt, dass die vom Server gesendete Nachrichten komprimiert werden. Hierfür haben wir mehrere Möglichkeiten:

  • Optimal: Die Komprimierung soll optimal ausgeführt werden, auch wenn der Vorgang unter Umständen eine längere Zeit in Anspruch nehmen kann.
  • Fastest: Die Komprimierung soll so schnell wie möglich beendet werden, auch wenn die resultierende Datei nicht optimal komprimiert wird.
  • NoCompression: Bei der Nachricht sollte keine Komprimierung erfolgen.

Wie weiter oben bereits eruiert, müssen wir zusätzlich noch gRPC-Web aktivieren. Hierzu fügen wir in der Configure-Methode den Aufruf app.UseGrpcWeb() hinzu.

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    //...
    app.UseGrpcWeb();
    // ...
}

Service-Implementierungen

ToDoService

Nach der Konfiguration von gRPC auf dem Server können wir unseren ersten Service implementieren. Dazu nehmen wir zuerst das Interface IToDoService.

public class ToDoService : IToDoService
{
    private readonly ToDoDbContext _dataContext;

    public ToDoService(ToDoDbContext dataContext)
    {
        _dataContext = dataContext;
    }

    // { more code ... }
    
    public Task<ToDoData> GetTodoItemAsync(ToDoIdQuery query)
    {
        return _dataContext.ToDoDbItems.FirstOrDefaultAsync(item => item.Id == query.Id);
    }

    public Task<ToDoData[]> GetToDosAsync()
    {
        return _dataContext.ToDoDbItems.ToArrayAsnyc();
    }
    
    // { more code ... }
}

Im Beispiel der ToDo-App greifen wir einfach mit Entity Framework Core auf die Datenbank zu und geben entweder eine Liste oder ein bestimmtes ToDo-Item zurück. gRPC übernimmt für uns das Se- und Deserialisieren der Requests und Responses über die Service und Data Contracts.

TimeService

public class TimesService : ITimesService
{
    public IAsyncEnumerable<TimeResult> SubscribeAsync(CallContext context = default)
        => SubscribeAsyncImpl(context.CancellationToken);

    private async IAsyncEnumerable<TimeResult> SubscribeAsyncImpl(
        [EnumeratorCancellation] CancellationToken cancel)
    {
        while (!cancel.IsCancellationRequested)
        {
            try
            {
                await Task.Delay(TimeSpan.FromSeconds(1), cancel);
            }
            catch (OperationCanceledException)
            {
                break;
            }

            yield return new TimeResult {Message = $"Loading: {DateTime.Now}"};
        }
    }
}

Im ITimesService, in dem wir einen Server-Streaming-Aufruf haben, gibt es zusätzlich noch den Parameter CallContext in der Methode SubscribeAsync. Mit CallContext greifen wir auf aktuellen Kontext des Aufrufs zu. In unserem Fall sind wir am CancellationToken interessiert, sodass wir ein Signal erhalten, wenn die Verbindung zum Client unterbrochen wird oder der Client den Aufruf beendet.

Route registrieren

Im letzten Schritt müssen wir nur noch in der Startup.cs die Routen registrieren, unter denen die gRPC-Services erreichbar sind. Im folgenden Code registrieren wir sowohl den TimeService als auch den ToDoService. Dafür nutzen wir die Methode MapGrpcService<T> und aktivieren zusätzlich gRPC-Web mit der Methode EnableGrpcWeb().

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    // ...

    app.UseGrpcWeb();

    app.UseEndpoints(endpoints =>
    {
        // Endpunkte für den gRPC Service hinzufügen
        endpoints.MapGrpcService<TimeService>().EnableGrpcWeb();
        endpoints.MapGrpcService<ToDoService>().EnableGrpcWeb();
        endpoints.MapControllers();
    });
}

Nachdem wir alle Schritte vollzogen haben, ist der Server vorbereitet für die Kommunikation mit dem Client. Im nächsten Kapitel schauen wir uns an, wie wir aus einem Blazor WebAssembly Client über gRPC-Web mit einem gRPC-Server kommunizieren.

gRPC & gRPC-Web in Blazor WebAssembly

Das Hinzufügen von gRPC-Unterstützung zu einer Blazor WebAssembly-App umfasst eine Reihe von Schritten. Da es noch keine Projektvorlage gibt, müssen diese Schritte manuell erfolgen. Die gute Nachricht ist jedoch, dass dieses Setup nur einmal durchgeführt werden muss. Danach können weitere gRPC-Endpunkte recht einfach implementiert werden.

Wie auch in der Shared Library, müssen im Client zuerst die notwendigen Pakete der .csproj Datei hinzugefügt werden:

  • protobuf-net.Grpc
  • Grpc.Net.Client
  • Grpc.Net.Client.Web

gRPC-Web konfigurieren

Für gRPC-Web in Blazor WebAssembly benötigen wir einen eigenen HttpClient, der einen GrpcWebHandler nutzt, um Aufrufe gegen den Server zu machen. Danach wird ein neuer GrpcChannel erzeugt, der die Serveradresse entgegennimmt. Zum Schluss wird mithilfe des Channels ein Service vom Typ IToDoService erstellt.

public class Program
{
    public static async Task Main(string[] args)
    {
        // ...

        builder.Services.AddScoped(services =>
        {
            // Erstelle neuen HttpClient mit GrpcWebHandler
            var httpClient = new HttpClient(new GrpcWebHandler(GrpcWebMode.GrpcWeb, new HttpClientHandler()));
            
            // Konfiguriere die Server URL
            var channel = GrpcChannel.ForAddress("https://localhost:5001",
                new GrpcChannelOptions {HttpClient = httpClient});
            
            // Erstelle einen gRPC Service, der zur Kommunkation genutzt wird
            return channel.CreateGrpcService<IToDoService>();
        });

        await builder.Build().RunAsync();
    }
}

Kommunikation mit den gRPC-Services

ToDoService

Um den gRPC-Service zu nutzen, können wir uns via Dependency Injection eine Instanz, beispielsweise in einer unserer Razor-Komponenten, geben lassen. Im folgenden Code-Beispiel verwenden wir den Service, um die aktuelle Liste von ToDos abzurufen und per Datenbindung in Razor anzuzeigen.

// Datei: index.razor

@page "/"

@if (_toDoItems != null && _toDoItems.Any())
{
    <div class="todos">
        @foreach (var task in _toDoItems)
        {
            <ToDoItem @key="task.Id" Item="@task" ToDoItemChanged="@RefreshAsync"></ToDoItem>
        }
    </div>
}
else
{
    <p class="info">Es sind derzeit keine Aufgaben zu erledigen.</p>
}

<MatButton class="btn-add" Raised="true" OnClick="@(e => { dialogIsOpen = true; })">Add new Task</MatButton>

<ToDoItemEditor DialogIsOpen="@dialogIsOpen" DialogClosed="@(async (e) => { await CloseDialog(e); })"></ToDoItemEditor>
// Datei: index.razor.cs

public partial class Index
{
    // Inject ToDoService via Dependency Injection
    [Inject] private ToDoService ToDoService { get; set; }

    private List<ToDoData> _toDoItems;
    private bool _dialogIsOpen;

    protected override async Task OnInitializedAsync()
    {
        await GetToDoListAsync();
    }

    private async Task GetToDoListAsync()
    {
        _toDoItems = await ToDoService.GetToDoListAsync();
    }

    private async Task CloseDialog(bool refresh)
    {
        _dialogIsOpen = false;
        if (refresh)
        {
            await RefreshAsync();
        }
    }

    private async Task RefreshAsync()
    {
        _toDoItems = await ToDoService.GetToDoListAsync();
        StateHasChanged();
    }
}

Betrachten wir die Anwendung zur Laufzeit in den DevTools des Chrome Browsers, können wir uns die Route anschauen, die der Client aufruft, um die Daten vom Server zu erhalten.

Dev Tools > gRPC Request URL

Im obigen Beispielbild sehen wir, dass ein HTTP-POST Request an den Server gesendet wird. Die URL spiegelt den Namespace des Services wider, in dem der IToDoService implementiert wurde. Beim Methodennamen wird der Präfix Async abgeschnitten. Daher ist im obigen Beispiel in der URL nur GetToDos zu sehen.

In der zweiten Hälfte des Beispiels können wir gut erkennen, dass die Daten nicht wie üblich im Text Format sondern als Binär-Daten vom Server an den Client gesendet werden.

Insgesamt fühlt sich gRPC-Web somit auf der Netzwerkebene eher so an wie WCF damals (und SOAP) als Web oder REST APIs.

TimeService

Als Nächstes werfen wir einen Blick auf den ITimeService von der Clientseite aus, der uns die aktuelle Zeit zurückgibt. Hier liegt der Unterschied darin, dass der Aufruf an den Server gestartet wird, dann aber so lange offen bleibt, bis einer von beiden die Verbindung beendet.

Auch wie im letzten Beispiel muss hierfür zuerst eine Instanz des Services per DI angefordert werden.

@inject ITimeService TimeService

Danach können wir uns mit der Methode SubscribeAsync registrieren und so die aktuelle Zeit erhalten. Hierfür nutzen wir eine asynchrone foreach-Schleife, die so lange offenbleibt, bis der Aufruf SubscribeAsync vom Server beendet wird oder ein Fehler geworfen wird.

private async Task StartTimeAsync()
{
    _cts = new CancellationTokenSource();
    var options = new CallOptions(cancellationToken: _cts.Token);

    try
    {
        await foreach (var time in TimeService.SubscribeAsync(new CallContext(options)))
        {
            _time = time.Time;
            StateHasChanged();
        }
    }
    catch (RpcException)
    {
    }
    catch (OperationCanceledException)
    {
    }
}

Hinweis: Um aktuelle Daten in der UI anzuzeigen bzw. zu aktualisieren muss die Methode StateHasChanged()aufgerufen werden. Dies führt dazu, dass die Komponente neu gerendert wird.

Schauen wir uns das Ganze auch mal in den DevTools des Chrome Browsers an.

Dev Tools > gRPC Request URL

Dev Tools > gRPC running HTTP Request

Hier sehen wir im ersten Bild, dass auch hier die URL dem Namespace des aufgerufenen Services entspricht. Im zweiten Bild sehen wir, dass der Aufruf offenbleibt und weitere Daten über die Response liefern kann. Dies geschieht so lange bis entweder der Server keine Nachrichten mehr sendet oder der Client bzw. Server die Verbindung abbricht. In diesem Fall wird jede Sekunde die neue Uhrzeit vom Server an den Client gesendet und der Call wird erst dann beendet, wenn der Client die Verbindung durch den CancellationToken beendet.

Hinweis: Nutzt man bei Blazor ein Streaming-Event ist es wichtig, das Interface IDisposable zu implementieren, da beim Navigieren auf eine andere Seite im Client der Call nicht geschlossen wird, sondern offenbleibt. Im Beispiel haben wir hierfür einen CancellationToken erstellt, die in der Methode Dispose, die Cancel-Methode des Tokens aufruft, um die Verbindung zum Server zu beenden.

public void Dispose()
{
    StopTime();
}

private void StopTime()
{
    _cts?.Cancel();
    _cts = null;
    _time = "";
}

Das ist es! Jetzt haben wir eine eigenständige Blazor WebAssembly-Anwendung, die einen externen gRPC-Webdienst verwendet. Die vollständige Demo findet sich auf GitHub.

Fazit

ASP.NET Core bietet seit Version 3.0 gRPC-Unterstützung. Doch wie wir in diesem Artikel gesehen haben ist gRPC alleine nicht das ganze Highlight. Kombiniert man den Code-First Ansatz mit einer .NET-Anwendung wie Blazor WebAssembly, wird die ganze Geschichte erst richtig rund.

Durch den Code-First Ansatz sind keine .proto-Dateien mehr nötig, denn DataContracts und ServiceContracts können dadurch in üblichen C# Klassen geschrieben werden. Wird das ganze System/Projekt in .NET entwickelt, können die Models und Service Schnittstellen in einer Shared Library implementiert werden, sodass Server und Client diese nutzen können und es keiner eigenen Implementierung bedarf. Wichtig hierbei ist es aber, dass sowohl Server als auch Client immer die gleiche Version der Shared Library nutzen oder die Shared Library rückwärtskompatibel ist.

Wie wir in unserer Beispielanwendung sehen können eröffnet dies die Möglichkeiten, künftig in .NET Projekten nicht nur JSON-over-HTTP einzusetzen, sondern auch die Kommunikation zwischen Server und Client mit gRPC-Web und dem Code-First-Ansatz zu implementieren.

Wenn Sie auch weitere Artikel, Webinare und Screencasts unserer Experten nicht verpassen möchten, melden Sie sich hier zu unserem kostenlosen, monatlichen Dev-Newsletter an.

Related Articles

 | Patrick Jahr

Version Information .NET SDK 5.0.104 ASP.NET Core Blazor WebAssembly: 5.0.4 MudBlazor: 5.0.4 Der gesamte Source Code zur Beispielanwendung findet sich in diesem GitHub Repository. Exception Handling als Aufgabenstellung Der richtige Umgang mit Fehlern ist für die Erstellung einer…

Read article
 | Pawel Gerr

In general, you can divide template engines into two types. The relatively simple ones are using template strings with placeholders to be replaced by some concrete values. The other template engines can do everything the simple ones can but additionally provide means for control…

Read article
 | Patrick Jahr

Seit der Version Blazor WebAssembly 3.2.0 enthält Blazor umfangreiche Unterstützung für clientseitige Authentifizierung, wodurch die Implementierung von OpenID Connect und OAuth2 in Single-Page-Applications (SPAs) deutlich vereinfacht wird. In diesem Artikel sehen wir uns an, wie…

Read article