gRPC Code-First mit ASP.NET Core 7 und Blazor WebAssembly

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.

In diesem Artikel:

Version Information

  • .NET SDK: 7.0.202
  • ASP.NET Core: 7.0.4
  • ASP.NET Core Blazor WebAssembly: 7.0.4
  • MudBlazor: 6.2.0

Der vollständige Beispiel-Code für diesen Artikel findet sich hier.

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 7.

Was ist gRPC?

gRPC ist ein modernes leichtgewichtiges Kommunikationsprotokoll von Google das als Open-Source-RPC Framework bereitgestellt wird. gRPC ist ein modernes, leichtgewichtiges Kommunikationsprotokoll für Remote Procedure Calls (RPC), das von Google als Open-Source-RPC Framework bereitgestellt wird. Es verwendet ein eigenes Protobuf-Datenformat (Protocol Buffers) für die Serialisierung von Nachrichten, welches sowohl auf möglichst kleine Datenpakete zur Bandbreitenoptimierung als auch auf sehr effiziente Serialisierung für möglichst schnelle Aufrufe ausgelegt ist. 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.

Der eingangs erwähnte definierte Service mit seinen Methoden, Parametern und Rückgabetypen definiert die Schnittstelle zwischen Server und Client. Es ist vorgesehen, diese Methoden und Datentypen in .proto Dateien mit einer speziellen Sprache zu definieren. Üblicherweise werden aus diesen .proto-Dateien dann die Server- und Client Implementierungen sowie optimierter Serialisierungcode für die eigene Programmiersprache generiert. 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 & 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. In diesem Artikel gehen wir jedoch davon aus, das wir in einem geschlossenen System sind und der Server sowie alle Clients die auf ihn Zugreifen ausschließlich .NET-basierte Anwendungen sein werden. Für diesen Fall gibt es Bibliotheken die uns erlauben, unsere Schnittstellen und Datentypen ohne .proto Files direkt in C# Code zu implementieren. Dies wird als Code-First Ansatz bezeichnet. Daher werden wir hier auch nicht näher auf die Details der .proto-Dateien eingehen, weitere Informationen dazu finden sich hier.

Service-Schnittstellen definieren mit Contract-First

Damit gRPC Dienste der Anwendung hinzugefügt werden können, müssen Service Schnittstellen erstellt werden. Diese können mit Contract-First Ansatz generiert werden. Hier 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. 

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.

Was ist 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 der Browser Fetch-API und XHR (XMLHTTPRequest) 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.

Die Anwendung besteht aus drei Projekten:

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

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.

Code-First

Service-Schnittstellen definieren mit Code-First

Im obigen Abschnitt haben wir gesehen wie man mit dem Contract-First Ansatz, Service-Schnittstellen generieren kann. 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. 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. Hierbei müssen alle Typen, sowohl einfachste primitive Typen als auch jeder in einem komplexen Typen verwendete verschachtelte Typ, in einer DataContract-Klasse mit DataMember annotiert sein. 

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 Programm.cs fügen wir der ServiceCollection den Code-First-Ansatz von gRPC hinzu.

				
					builder.Services.AddGrpc();
builder.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.

				
					//...
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().

				
					
// ...

app.UseGrpcWeb();

// Endpunkte für den gRPC Service hinzufügen
app.MapGrpcService<TimeService>().EnableGrpcWeb();
app.MapGrpcService<ToDoService>().EnableGrpcWeb();
app.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.

				
					// ...

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("{WebApi URL}",
        new GrpcChannelOptions {HttpClient = httpClient});
            
    // Erstelle einen gRPC Service, der zur Kommunkation genutzt wird
    return channel.CreateGrpcService<IToDoService>();
});

var app = 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>
}

<MudButton Class="btn-add" 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.

 

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.

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.

Mehr Artikel zu Blazor, ASP.NET Core, gRPC
Kostenloser
Newsletter

Aktuelle Artikel, Screencasts, Webinare und Interviews unserer Experten für Sie

Verpassen Sie keine Inhalte zu Angular, .NET Core, Blazor, Azure und Kubernetes und melden Sie sich zu unserem kostenlosen monatlichen Dev-Newsletter an.

Newsletter Anmeldung
Diese Artikel könnten Sie interessieren
Database Access with Sessions
.NET
KP-round

Data Access in .NET Native AOT with Sessions

.NET 8 brings Native AOT to ASP.NET Core, but many frameworks and libraries rely on unbound reflection internally and thus cannot support this scenario yet. This is true for ORMs, too: EF Core and Dapper will only bring full support for Native AOT in later releases. In this post, we will implement a database access layer with Sessions using the Humble Object pattern to get a similar developer experience. We will use Npgsql as a plain ADO.NET provider targeting PostgreSQL.
15.11.2023
Old computer with native code
.NET
KP-round

Native AOT with ASP.NET Core – Overview

Originally introduced in .NET 7, Native AOT can be used with ASP.NET Core in the upcoming .NET 8 release. In this post, we look at the benefits and drawbacks from a general perspective and perform measurements to quantify the improvements on different platforms.
02.11.2023
ASP.NET Core
favicon

Architektur-Modernisierung: Migration von WCF zu gRPC mit ASP.NET Core – ein pragmatischer Ansatz

Viele Projekte mit verteilten Anwendungen in der .NET-Welt basieren noch auf der Windows Communication Foundation (WCF). Doch wie kommt man weg von der "Altlast" und wie stellt man seinen Code auf sowohl moderne als auch zukunftssichere Beine? Eine mögliche Lösung ist gRPC.

13.04.2023
Blazor
favicon

Blazor WebAssembly in .NET 7: UI-Performance-Optimierung auf Komponentenebene

Stockende UI, keine Reaktion nach dem Klick auf einen Button oder einer Eingabe in einem Feld - dies sind nur wenige Beispiele alltäglicher Probleme, die der Nutzung von Client-Anwendungen im Allgemeinen, und bei Webanwendungen im Speziellen, immer wieder auftreten können. In diesem Artikel schauen wir uns an, wie wir komponentenbasierte UIs in Blazor WebAssembly optimieren können, um dadurch eine für die Benutzer zufriedenstellende Geschwindigkeit und ein flüssiges UI zu bekommen.
29.03.2023
Blazor
sg

Understanding and Controlling the Blazor WebAssembly Startup Process

There are a lot of things going on in the background, when a Blazor WebAssembly application is being started. In some cases you might want to take a bit more control over that process. One example might be the wish to display a loading screen for applications that take some time for initial preparation, or when users are on a slow internet connection. However, in order to control something, we need to understand what is happening first. This article takes you down the rabbit hole of how a Blazor WASM application starts up.
07.03.2023
.NET
cl-neu

Adding Superpowers to your Blazor WebAssembly App with Project Fugu APIs

Blazor WebAssembly is a powerful framework for building web applications that run on the client-side. With Project Fugu APIs, you can extend the capabilities of these apps to access new device features and provide an enhanced user experience. In this article, learn about the benefits of using Project Fugu APIs, the wrapper packages that are available for Blazor WebAssembly, and how to use them in your application.

Whether you're a seasoned Blazor developer or just getting started, this article will help you add superpowers to your Blazor WebAssembly app.
28.02.2023