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
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 GetToDosAsync();
Task GetToDoItemAsync(ToDoIdQuery query);
Task AddToDoItemAsync(ToDoData data);
Task UpdateToDoItemAsync(ToDoData data);
Task 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 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
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();
// ...
public class ToDoService : IToDoService
{
private readonly ToDoDbContext _dataContext;
public ToDoService(ToDoDbContext dataContext)
{
_dataContext = dataContext;
}
// { more code ... }
public Task GetTodoItemAsync(ToDoIdQuery query)
{
return _dataContext.ToDoDbItems.FirstOrDefaultAsync(item => item.Id == query.Id);
}
public Task GetToDosAsync()
{
return _dataContext.ToDoDbItems.ToArrayAsnyc();
}
// { more code ... }
}
public class TimesService : ITimesService
{
public IAsyncEnumerable SubscribeAsync(CallContext context = default)
=> SubscribeAsyncImpl(context.CancellationToken);
private async IAsyncEnumerable 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().EnableGrpcWeb();
app.MapGrpcService().EnableGrpcWeb();
app.MapControllers();
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();
});
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())
{
@foreach (var task in _toDoItems)
{
}
}
else
{
Es sind derzeit keine Aufgaben zu erledigen.
}
Add new Task
// Datei: index.razor.cs
public partial class Index
{
// Inject ToDoService via Dependency Injection
[Inject] private ToDoService ToDoService { get; set; }
private List _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.