Im Rahmen der Entwicklung einer Web-API mit ASP.NET Core für eine .NET MAUI-Anwendung wird zunächst ein neues Webservice-Projekt innerhalb der bestehenden Lösung „Chapter18“ erstellt. Dieses Projekt basiert auf der Vorlage ASP.NET Core Web API mit minimalen APIs, ohne Authentifizierung und ohne Docker-Unterstützung. Die Verwendung von HTTPS wird aktiviert, und OpenAPI-Unterstützung (Swagger) wird für die automatische Dokumentation freigeschaltet. Controller werden nicht genutzt, da die Endpunkte über Minimal APIs definiert werden.

Wichtig ist die Referenzierung des Northwind-Datenbankkontextes, der in einem separaten Klassenbibliotheksprojekt zur Verwaltung relationaler Daten mit SQL Server definiert wurde. Die Projektstruktur muss klar und ohne Zeilenumbrüche in Pfadangaben sein, um Kompilierungsfehler zu vermeiden. Die Kompilierung des Webservice-Projekts wird über die Kommandozeile mit „dotnet build“ verifiziert, um sicherzustellen, dass alle externen Entity-Modelle korrekt eingebunden sind.

Die Konfiguration des Webservices erfolgt über die Datei launchSettings.json, in der die Anwendung auf den Ports 5181 (HTTPS) und 5182 (HTTP) lauscht. Dies ermöglicht sowohl sichere als auch ungesicherte Zugriffe, wobei im Standardbetrieb HTTPS bevorzugt wird.

Der zentrale Programmcode in Program.cs ersetzt die ursprüngliche Wetterdienstlogik durch CRUD-Endpunkte für Kunden. Die Endpunkte implementieren die klassischen HTTP-Methoden GET, POST, PUT und DELETE und nutzen Entity Framework Core, um Datenbankoperationen asynchron auszuführen. Dabei werden Kundeninformationen abgerufen, neu angelegt, aktualisiert oder gelöscht. Jeder Endpunkt ist mit OpenAPI-Dokumentation versehen, die die erwarteten Antwortcodes (z.B. 200 OK, 201 Created, 204 No Content, 404 Not Found) angibt.

Die Swagger-Oberfläche dient als Test- und Dokumentationsplattform, mit der Entwickler direkt im Browser API-Requests ausführen und die JSON-Daten der Kunden abrufen können. Das Testen dieser Funktionalität erfolgt über GET-Anfragen, die die Rückgabe aller Kundendatensätze demonstrieren.

Für die Entwicklung und das Testen ist es oft notwendig, ungesicherte HTTP-Verbindungen zu erlauben. Hierzu wird die HTTPS-Umleitung im Webservice deaktiviert, um die Kommunikation auch über HTTP zu ermöglichen. Dies ist insbesondere für die lokale Entwicklung und Testphasen hilfreich, wenn beispielsweise Clients direkt auf das lokale Webservice zugreifen.

Bei der Verbindung von .NET MAUI-Apps mit lokalen Webservices gibt es wichtige Unterschiede zwischen den Zielplattformen. Während Windows- und iOS-Emulatoren problemlos auf localhost zugreifen können, ist bei Android-Emulatoren ein spezieller IP-Adressenaustausch erforderlich. Der Emulator verwendet die IP 10.0.2.2 als Proxy für 127.0.0.1, sodass lokale Webservice-Endpunkte über diese Adresse erreichbar sind.

Für iOS-Apps muss zusätzlich in der Info.plist des Projekts die Transport-Sicherheit konfiguriert werden, um ungesicherte HTTP-Verbindungen zu erlauben. Hierzu wird das NSAppTransportSecurity-Dictionary erweitert, wobei entweder NSAllowsArbitraryLoads (für uneingeschränkte erlaubte Klartextverbindungen) oder NSAllowsLocalNetworking (nur für lokale Netzwerke) aktiviert werden kann. Diese Einstellungen sind essenziell, um während der Entwicklung ohne HTTPS testen zu können, da iOS standardmäßig nur sichere Verbindungen zulässt.

Es ist wichtig zu verstehen, dass diese Konfigurationen bewusst Sicherheitsaspekte lockern, um Entwicklungs- und Testprozesse zu erleichtern. Im produktiven Umfeld sollten HTTPS-Verbindungen verpflichtend bleiben, um Datenintegrität und Vertraulichkeit zu gewährleisten. Zudem sollte die Architektur modular gestaltet sein, um Webservice, Datenbank und Client klar voneinander zu trennen und flexibel warten zu können.

Die enge Verzahnung von Webservice und mobilen Anwendungen im lokalen Entwicklungsnetzwerk erfordert eine genaue Kenntnis der Netzwerkstrukturen, insbesondere bei Emulatoren und verschiedenen Betriebssystemen. Die korrekte Zuordnung von Ports, Adressen und Sicherheitseinstellungen ist unerlässlich, um eine reibungslose Kommunikation zu gewährleisten.

Weiterhin ist es ratsam, neben der reinen API-Implementierung auch auf Monitoring und Logging zu achten, damit Fehlerzustände und Leistungsengpässe frühzeitig erkannt werden können. Die Nutzung von OpenAPI und Swagger stellt hierbei einen großen Vorteil dar, da sie neben der Dokumentation auch Testmöglichkeiten bieten und Entwicklern Transparenz über die angebotenen Schnittstellen verschaffen.

Wie man Vererbungshierarchien mit Entity Framework Core abbildet

In modernen Softwareanwendungen sind Datenbanken ein wesentlicher Bestandteil der Datenhaltung. Wenn es darum geht, relationale Daten zu verwalten, spielt Entity Framework Core eine bedeutende Rolle, da es eine einfache Möglichkeit bietet, Objekte aus der Programmiersprache in Datenbankstrukturen zu übertragen. Die Abbildung von Vererbungshierarchien in einer relationalen Datenbank ist eine der Herausforderungen, die Entwickler bei der Nutzung von EF Core meistern müssen. Besonders wenn unterschiedliche Vererbungsmuster wie Table-per-Hierarchy, Table-per-Type oder Table-per-Concrete-Type zur Anwendung kommen, können sich die Ansätze deutlich unterscheiden und erfordern spezifische Überlegungen.

Nehmen wir als Beispiel eine Vererbungshierarchie, die Informationen über Personen speichert, die sowohl Studenten als auch Mitarbeiter umfasst. Diese Klassenstruktur könnte so aussehen:

csharp
public abstract class Person { public int Id { get; set; } public string? Name { get; set; } } public class Student : Person {
public string? Subject { get; set; }
}
public class Employee : Person { public DateTime HireDate { get; set; } }

In EF Core gibt es mehrere Strategien, wie diese Klassenhierarchie auf eine relationale Datenbank abgebildet werden kann. Standardmäßig verwendet EF Core die Table-per-Hierarchy (TPH) Strategie, bei der alle abgeleiteten Klassen in einer einzigen Tabelle gespeichert werden. Diese Tabelle enthält eine zusätzliche Spalte, die als Discriminator fungiert, um zwischen den verschiedenen Typen der Hierarchie zu unterscheiden.

Table-per-Hierarchy (TPH)

Die TPH-Strategie nutzt eine einzige Tabelle für alle Klassen der Hierarchie und fügt einen sogenannten "Discriminator" hinzu, um zwischen den Typen zu unterscheiden. Diese Strategie ist einfach und leistungsfähig, da sie keine Joins erfordert und nur eine Tabelle benötigt. Der Nachteil dieser Strategie liegt in der Notwendigkeit, dass bestimmte Spalten für abgeleitete Klassen nullable sind, da nicht alle Eigenschaften in jeder Klasse vorhanden sind.

Ein Beispiel für eine TPH-Implementierung könnte folgendermaßen aussehen:

sql
CREATE TABLE [People] ( [Id] int NOT NULL IDENTITY, [Name] nvarchar(max) NOT NULL, [Discriminator] nvarchar(max) NOT NULL, [Subject] nvarchar(max) NULL, [HireDate] nvarchar(max) NULL, CONSTRAINT [PK_People] PRIMARY KEY ([Id]) );

Die Daten in dieser Tabelle könnten folgendermaßen aussehen:

IdNameDiscriminatorSubjectHireDate
1Roman RoyStudentHistoryNULL
2Kendall RoyEmployeeNULL02/04/2014
3Siobhan RoyEmployeeNULL12/09/2020

Die TPH-Strategie ist besonders effizient in Bezug auf Performance, da nur eine Tabelle abgefragt werden muss. Jedoch gibt es Einschränkungen, etwa wenn man sicherstellen möchte, dass alle Felder für abgeleitete Klassen nicht null sein dürfen, was zu Problemen führen kann, da Spalten für abgeleitete Typen nullable sein müssen.

Best Practice: Wenn der Discriminator viele unterschiedliche Werte enthält, kann das Hinzufügen eines Indexes auf diese Spalte die Performance weiter verbessern. Sollte es jedoch nur wenige unterschiedliche Werte geben, könnte ein Index die Gesamtperformance aufgrund der erhöhten Aktualisierungszeiten negativ beeinflussen.

Table-per-Type (TPT)

Im Gegensatz zur TPH-Strategie verwendet die TPT-Strategie für jede abgeleitete Klasse eine eigene Tabelle. Dies führt zu einer besseren Normalisierung und reduziert die Speicheranforderungen, da nur die spezifischen Spalten für jede Klasse gespeichert werden. Die Tabellenstruktur könnte folgendermaßen aussehen:

sql
CREATE TABLE [People] (
[Id] int NOT NULL IDENTITY, [Name] nvarchar(max) NOT NULL, CONSTRAINT [PK_People] PRIMARY KEY ([Id]) ); CREATE TABLE [Students] ( [Id] int NOT NULL, [Subject] nvarchar(max) NULL, CONSTRAINT [PK_Students] PRIMARY KEY ([Id]), CONSTRAINT [FK_Students_People] FOREIGN KEY ([Id]) REFERENCES [People] ([Id]) ); CREATE TABLE [Employees] ( [Id] int NOT NULL, [HireDate] nvarchar(max) NULL, CONSTRAINT [PK_Employees] PRIMARY KEY ([Id]), CONSTRAINT [FK_Employees_People] FOREIGN KEY ([Id]) REFERENCES [People] ([Id]) );

Daten in den Tabellen würden dann folgendermaßen aussehen:

People Tabelle:

IdName
1Roman Roy
2Kendall Roy
3Siobhan Roy

Students Tabelle:

IdSubject
1History

Employees Tabelle:

IdHireDate
202/04/2014
312/09/2020

Der Vorteil der TPT-Strategie ist die reduzierte Speichergröße durch die Normalisierung der Daten. Jede Tabelle enthält nur die relevanten Informationen für ihre jeweilige Klasse. Dies kann jedoch auch zu einer schlechteren Performance führen, wenn mehrere Tabellen gleichzeitig abgefragt werden müssen, da Joins erforderlich sind.

Table-per-Concrete-Type (TPC)

Die TPC-Strategie ist die neueste Ergänzung in EF Core (ab Version 7) und verfolgt einen Ansatz, bei dem für jede konkrete Klasse eine eigene Tabelle mit allen notwendigen Feldern erstellt wird. Diese Strategie vermeidet die Notwendigkeit von Joins und arbeitet effizienter bei der Abbildung komplexer Hierarchien. Allerdings hat auch dieser Ansatz seine Nachteile, insbesondere im Hinblick auf die Datenkonsistenz, da dieselben Felder möglicherweise in mehreren Tabellen vorhanden sind.

Die Wahl der richtigen Abbildungsstrategie hängt von den spezifischen Anforderungen der Anwendung ab. Es ist wichtig, die Vor- und Nachteile jeder Methode zu kennen, um die beste Lösung für die jeweilige Situation auszuwählen.

Wie misst man die Performance von Methoden präzise und welche Unterschiede zeigen sich bei synchroner Ausführung von Tasks?

Die präzise Messung der Performance einzelner Methoden ist ein zentraler Bestandteil jeder ernstzunehmenden Softwareoptimierung. Mit BenchmarkDotNet steht ein professionelles Werkzeug zur Verfügung, das es ermöglicht, systematisch und reproduzierbar Unterschiede im Verhalten von Codefragmenten zu analysieren – mit Fokus auf Geschwindigkeit, Konsistenz und statistischer Aussagekraft.

Ein typisches Setup beginnt mit dem Start eines Benchmarks über BenchmarkRunner.Run(). Dabei wird der Benchmark in der Release-Konfiguration ausgeführt, da Debug-Builds aufgrund zusätzlicher Laufzeitprüfungen verzerrte Werte liefern. In Visual Studio wird die Konfiguration im Toolbar auf „Release“ gesetzt und die Applikation ohne Debugger gestartet. In Visual Studio Code geschieht dies mit dotnet run --configuration Release.

Nach der Ausführung generiert BenchmarkDotNet eine Vielzahl von Ausgabedateien – darunter CSV-, Markdown- und HTML-Reports. Von besonderem Interesse ist jedoch die tabellarische Zusammenfassung der Messergebnisse, die Mittelwert, Standardabweichung, Konfidenzintervall und Verteilungsschiefe darstellt. Im konkreten Beispiel zeigt sich: Die Methode StringConcatenationTest benötigt im Mittel 412,990 ns, wohingegen StringBuilderTest mit durchschnittlich 275,082 ns signifikant schneller ist – ein Unterschied, der nicht nur statistisch signifikant ist, sondern sich auch durch die geringere Varianz und bessere Konsistenz bei StringBuilder manifestiert.

Insbesondere die Analyse der Ausreißer legt nahe, dass Stringverkettung nicht nur langsamer, sondern auch unvorhersehbarer ist: Während bei StringConcatenationTest insgesamt 14 Ausreißer registriert wurden, beschränken sich diese bei StringBuilderTest auf zwei marginale Werte. In performanzkritischen Szenarien ist dies ein nicht zu unterschätzender Faktor.

Im Anschluss an die Performanceanalyse wird ein weiteres fundamentales Konzept vorgestellt: das asynchrone Ausführen von Aufgaben. Doch bevor man die Parallelisierung nutzt, wird zunächst die synchrone Abarbeitung simuliert. Eine einfache Konsolenanwendung wird erstellt, in der drei Methoden sequenziell aufgerufen werden. Diese Methoden (MethodA, MethodB, MethodC) simulieren Rechenzeit durch gezielte Thread.Sleep()-Aufrufe von 3, 2 und 1 Sekunden.

Ziel dieser Übung ist es, die Zeit zu messen, die vergeht, wenn diese Aufgaben nacheinander – also auf einem einzelnen Thread – abgearbeitet werden. Dabei wird durch farblich hervorgehobene Konsolenausgaben Transparenz über die Thread-IDs, Prioritäten und weitere Metadaten geschaffen. Ein Stopwatch misst die verstrichene Zeit, und es zeigt sich, dass die gesamte Ausführungszeit erwartungsgemäß der Summe der Einzelzeiten entspricht – also rund 6 Sekunden.

Wichtig bei diesem Experiment ist weniger das konkrete Ergebnis als vielmehr das Verständnis für die Limitierungen synchroner Ausführung: Jede Methode blockiert den Thread vollständig, es findet keine Überlappung statt, und das System bleibt unterausgelastet – insbesondere auf modernen Mehrkernprozessoren.

Was aus diesen Beobachtungen zu verstehen ist, geht über die reinen Zahlen hinaus. Die Fähigkeit, präzise Messdaten zu interpretieren, verlangt ein Bewusstsein für statistische Maßzahlen wie Konfidenzintervalle und Standardabweichungen – nicht nur den Mittelwert. Eine kleinere Streuung bedeutet nicht nur bessere Performance, sondern auch höhere Vorhersagbarkeit, was im realen Einsatz eine entscheidende Rolle spielt. Auch ist es essenziell zu erkennen, dass Benchmarks im Kontext des Zielsystems stehen: Prozessorarchitektur, Garbage Collection, JIT-Optimierungen und Hintergrundprozesse können Einfluss auf die Messergebnisse nehmen.

Zudem sollte der Leser verstehen, dass rein sequentielle Programmierung nicht mehr dem heutigen Paradigma entspricht. Moderne Systeme verlangen Parallelität, nicht als Selbstzweck, sondern als notwendige Antwort auf Hardwareentwicklungen. Die vorgestellte synchrone Ausführung bildet daher lediglich die Grundlage – die kommenden Schritte führen folgerichtig zur Analyse asynchroner Task-Verarbeitung, bei der sich neue Herausforderungen wie Thread-Sicherheit und Synchronisationsmechanismen ergeben.

Wie man Apps und Dienste mit .NET erstellt: Ein Leitfaden zur effektiven Nutzung von Visual Studio, C# und .NET 7

In diesem Buch begegnen Sie verschiedenen Textstilen, die unterschiedliche Arten von Informationen kennzeichnen. Diese Stile dienen dazu, das Verständnis für technische Begriffe und Prozesse zu erleichtern. Ein häufig verwendeter Stil ist der CodeInText, der Codewörter im Text hervorhebt, sowie Datenbanktabellennamen, Ordnernamen, Dateinamen, Dateiendungen, Pfadangaben, Dummy-URLs, Benutzereingaben und Twitter-Handles. Ein Beispiel: „Die Ordner Controllers, Models und Views enthalten ASP.NET Core-Klassen und die .cshtml-Dateien zur Ausführung auf dem Server.“

Besondere Aufmerksamkeit verdienen auch Codeblöcke. Ein Beispiel für einen solchen Codeblock könnte wie folgt aussehen:

csharp
// Elemente an Indexpositionen speichern names[0] = "Kate"; names[1] = "Jack"; names[2] = "Rebecca"; names[3] = "Tom";

Wenn wir ein bestimmtes Element innerhalb eines Codeblocks hervorheben möchten, erfolgt dies durch eine visuelle Markierung der entsprechenden Zeilen oder Einträge, um die Lesbarkeit zu steigern.

Ein weiterer häufig verwendeter Stil ist Fettgedruckt. Dieser Stil wird genutzt, um neue Begriffe oder besonders wichtige Wörter zu kennzeichnen, die beispielsweise in Menüs oder Dialogfeldern zu sehen sind. Ein Beispiel: „Durch Klicken auf die Schaltfläche Next gelangen Sie zum nächsten Bildschirm.“

Darüber hinaus gibt es einen Best Practices-Stil, der bewährte Methoden zum Programmieren von Experten hervorhebt. Diese Hinweise sollen den Leser dabei unterstützen, effizienter und effektiver zu arbeiten.

In Bezug auf die Entwicklung von Apps und Diensten mit .NET ist es von zentraler Bedeutung, die verschiedenen Versionen von .NET zu unterscheiden. Der Begriff „modernes .NET“ bezieht sich auf .NET 7 und seine Vorgänger wie .NET 5 und .NET 6, die alle aus .NET Core hervorgegangen sind. Im Gegensatz dazu bezeichnet „Legacy .NET“ ältere Plattformen und Standards wie .NET Framework, Mono, Xamarin und .NET Standard. Diese Unterscheidung ist wichtig, da modernes .NET eine Vereinigung der älteren Plattformen darstellt, wodurch es eine größere Flexibilität und bessere Integration bietet.

Im Verlauf des Buches werden Sie auch lernen, wie Sie Ihre Entwicklungsumgebung richtig einrichten, sowohl in Visual Studio 2022 als auch in Visual Studio Code. Es wird erklärt, wie Sie Code-Analyzer nutzen können, um die Qualität Ihres Codes zu verbessern, und es werden neue Features in C# 8 bis C# 11 sowie in den .NET-Versionen bis hin zu .NET 7 behandelt.

Ein besonders hilfreiches Werkzeug wird das GitHub-Repository dieses Buches sein. Hier finden Sie Lösungen zu allen Code-Aufgaben und können diese leicht mit Ihrem eigenen Code vergleichen. Das Repository lässt sich auch direkt in Visual Studio Code verwenden, was eine nahtlose Integration der Aufgaben und Beispiele ermöglicht.

Ein zusätzliches Augenmerk sollte auf den verschiedenen Möglichkeiten gelegt werden, Daten zu speichern und zu verwalten. In den folgenden Kapiteln wird erklärt, wie Sie SQL Server und Azure Cosmos DB nutzen können, um Daten sowohl lokal als auch in der Cloud zu speichern. Diese Grundlagen sind essentiell, da Datenmanagement die Grundlage für viele Anwendungen und Dienste darstellt.

In den späteren Kapiteln erfahren Sie, wie Sie spezialisierte Bibliotheken verwenden, die Ihnen helfen, mit Datums- und Zeitangaben umzugehen, Daten zu verschlüsseln oder zu hashen und Ihre Apps mit Authentifizierungs- und Autorisierungstechniken abzusichern. Diese Techniken sind für die Erstellung sicherer und performanter Anwendungen unerlässlich.

Ein weiterer wichtiger Bestandteil dieses Buches sind die modernen Servicetechnologien. Sie lernen, wie Sie Web-APIs mit ASP.NET Core, OData, GraphQL, gRPC und SignalR aufbauen. Diese Technologien sind unverzichtbar, wenn es darum geht, skalierbare und performante Web-Dienste zu entwickeln. Die Nutzung von Azure Functions zur Serverless-Architektur wird ebenfalls ausführlich behandelt.

Zuletzt wird auch auf die Gestaltung von Benutzeroberflächen eingegangen. Sie lernen, wie Sie mit Technologien wie Blazor und .NET MAUI plattformübergreifende Anwendungen entwickeln können, die auf Web-, Desktop- und mobilen Geräten ausgeführt werden können.

Es ist jedoch wichtig zu verstehen, dass das Erlernen und Anwenden dieser Technologien ein iterativer Prozess ist. Komplexe Themen lassen sich am besten durch Nachahmung und Wiederholung erlernen. Der kontinuierliche Umgang mit den vorgestellten Technologien, das Erstellen von Projekten und das Experimentieren mit verschiedenen Features wird dazu beitragen, Ihre Fähigkeiten zu vertiefen.

Insgesamt bietet dieses Buch eine fundierte und praxisorientierte Einführung in die Entwicklung von Apps und Diensten mit .NET 7. Die behandelten Themen sind darauf ausgelegt, Sie auf die Entwicklung realer Anwendungen vorzubereiten und Ihnen die nötigen Werkzeuge zu geben, um erfolgreich in der modernen Softwareentwicklung zu arbeiten.

Wie man gRPC-Metadaten und Deadlines für verbesserte Zuverlässigkeit nutzt

In modernen Microservice-Architekturen spielt die effiziente Kommunikation zwischen Client und Service eine zentrale Rolle. Bei der Verwendung von gRPC, einem leistungsstarken Framework für Remote Procedure Calls, werden nicht nur Daten zwischen den Systemen übertragen, sondern auch wichtige Metadaten, die für die Verwaltung und das Debugging von Anfragen unerlässlich sind. Zusätzlich wird empfohlen, für gRPC-Anfragen Deadlines zu setzen, um die Ressourcennutzung zu optimieren und die Reaktionszeiten zu kontrollieren. Im Folgenden wird erläutert, wie man gRPC-Metadaten extrahiert und Deadlines für Anfragen setzt.

Die gRPC-Kommunikation basiert auf der Definition von Anfragen und Antworten, die in einem sogenannten Vertrag festgelegt sind. Doch nicht nur diese Nachrichten sind entscheidend für die Interaktion zwischen Client und Server – auch Metadaten, die als Header und Trailer mit den Nachrichten übermittelt werden, spielen eine wesentliche Rolle. Diese Metadaten bestehen aus einfachen Schlüssel-Wert-Paaren, die bei jeder Anfrage mitgesendet werden können. Um diese Metadaten zu extrahieren, kann der Client das AsyncUnaryCall-Objekt nutzen, das bei gRPC-Aufrufen generiert wird. Ein Beispiel für das Abrufen und Ausgeben der Metadaten wird wie folgt gezeigt:

Im Code von HomeController.cs muss der Aufruf des gRPC-Services zunächst auskommentiert und durch Code ersetzt werden, der das AsyncUnaryCall-Objekt abruft. Daraufhin können die Header der Antwort mit der Methode ResponseHeadersAsync ausgelesen und im Log ausgegeben werden. Ein Beispiel für diesen Vorgang könnte folgendermaßen aussehen:

csharp
AsyncUnaryCall<ShipperReply> shipperCall = shipperClient.GetShipperAsync(new ShipperRequest { ShipperId = id });
Metadata metadata = await shipperCall.ResponseHeadersAsync; foreach (Metadata.Entry entry in metadata) { _logger.LogCritical($"Key: {entry.Key}, Value: {entry.Value}"); }

Der Client sendet die Anfrage und erhält dann eine Antwort, die sowohl die Geschäftsdaten des Shippers als auch die Metadaten enthält. Dies ermöglicht eine detaillierte Überwachung der Kommunikation zwischen den Systemen und kann beim Debugging sowie bei der Analyse von Anfragen äußerst nützlich sein.

Darüber hinaus können sogenannte „Trailers“ als Metadaten am Ende der Antwort abgerufen werden. Diese Funktion ist nützlich, um zusätzliche Informationen zu erhalten, die nach Abschluss der Hauptnachricht übermittelt werden. Der Trailer kann über die Methode GetTrailers() abgerufen werden, die ein weiteres Metadaten-Dictionary enthält.

Neben den Metadaten ist die Definition einer Deadline für eine gRPC-Anfrage eine empfohlene Praxis, die die Zuverlässigkeit und Ressourcenkontrolle der Microservices verbessert. Eine Deadline gibt dem Service eine zeitliche Grenze, innerhalb derer eine Antwort erwartet wird. Wird diese Zeit überschritten, kann der Client die Anfrage abbrechen, bevor der Server seine Antwort vollständig verarbeitet hat. Dies schützt den Server vor übermäßiger Last und verhindert, dass Ressourcen unnötig verbraucht werden. Gleichzeitig sorgt es dafür, dass der Client nicht unbegrenzt auf eine Antwort warten muss.

Ein Beispiel zur Implementierung einer Deadline sieht wie folgt aus:

  1. In der Methode GetShipper auf der Server-Seite wird eine künstliche Verzögerung von 5 Sekunden eingeführt, um eine Situation zu simulieren, in der die Anfrage möglicherweise zu lange dauert:

csharp
public override async Task<ShipperReply> GetShipper(ShipperRequest request, ServerCallContext context) { _logger.LogCritical("This request has a deadline of {0:T}. It is now {1:T}.", context.Deadline, DateTime.UtcNow); await Task.Delay(TimeSpan.FromSeconds(5)); return ToShipperReply(await db.Shippers.FindAsync(request.ShipperId)); }
  1. Auf der Client-Seite wird beim Aufruf der GetShipperAsync-Methode eine Deadline von 3 Sekunden gesetzt:

csharp
AsyncUnaryCall<ShipperReply> shipperCall = shipperClient.GetShipperAsync(
new ShipperRequest { ShipperId = id }, deadline: DateTime.UtcNow.AddSeconds(3));

Falls die Anfrage die festgelegte Zeit überschreitet, wird eine RpcException geworfen, die im Client abgefangen wird:

csharp
catch (RpcException rpcex) when (rpcex.StatusCode == global::Grpc.Core.StatusCode.DeadlineExceeded) { _logger.LogWarning("Northwind.Grpc.Service deadline exceeded."); ViewData["exception"] = rpcex.Message; }

Durch das Setzen von Deadlines wird der Client in die Lage versetzt, auf nicht reagierende Services schnell zu reagieren, anstatt ewig auf eine Antwort zu warten. Dies verbessert nicht nur die Benutzererfahrung, sondern trägt auch dazu bei, dass die Systemressourcen effizienter genutzt werden.

Zusätzlich zum Setzen einer Deadline für den gRPC-Aufruf sollte auch die Log-Konfiguration für sowohl den Service als auch den Client angepasst werden, damit aussagekräftigere Informationen im Falle von Fehlern oder Verzögerungen erfasst werden. Dies kann über die Änderung des Log-Levels in der appsettings.Development.json-Datei erfolgen, um sicherzustellen, dass auch weniger schwere Ereignisse, wie etwa Warnungen, im Log erscheinen.

Wichtig ist zu beachten, dass Deadlines in gRPC keine universelle Lösung für alle Leistungsprobleme darstellen. Es ist eine gezielte Maßnahme, die in Verbindung mit anderen Performance-Optimierungen, wie etwa Lastverteilung und effizienten Datenbankabfragen, eingesetzt werden sollte. Die Wahl der richtigen Deadline hängt vom jeweiligen Anwendungsfall ab und sollte so gewählt werden, dass sie eine Balance zwischen Benutzerfreundlichkeit und Ressourcenschonung bietet.