ncellation Token für bessere Performance in Blazo
r mit C#.
Bei Webanwendungen und überall dort, wo HTTP-Requests gesendet werden, wird asynchrone Programmierung eingesetzt. Diese bietet zahlreiche Vorteile, wie eine flüssige Benutzeroberfläche, verbesserte Performance und parallele Zugriffe auf WebAPIs. Wenn man mit C# asynchron programmiert, stößt man früher oder später auf die Parameter asynchroner Funktionen, insbesondere auf das Cancellation Token. Die Idee dahinter ist einfach: Wenn eine asynchrone Methode ein Cancellation Token übergeben bekommt, kann die Ausführung der Methode vorzeitig abgebrochen werden.
Ein einfaches Szenario ist ein Suchfeld mit einer großen Datenmenge. Der Benutzer gibt eine Zeichenfolge in das Suchfeld ein, und bei jedem neuen Buchstaben wird die Suche erneut durchgeführt. Ohne Cancellation Token wird jede Suche vollständig ausgeführt. Tippt ein Benutzer ein Wort, werden Suchergebnisse der einzelnen Buchstabenfragmente angezeigt, bevor das endgültige Suchergebnis vorliegt. Bei einfachen Suchanfragen wird zumindest die Wartezeit nicht maßgeblich beeinträchtigt. Wenn jedoch komplexe Berechnungen erforderlich sind, würden diese für jeden Buchstaben durchgeführt werden. In solchen Fällen kann ein Cancellation Token zu einer deutlichen Performance-Verbesserung führen.
Die Anwendung von Cancellation Tokens möchte ich an einem kleinen Beispiel verdeutlichen. Das Projekt ist hier zu finden:
GitHub: CancellationToken C# Blazor Demo
In diesem Szenario wurde eine Blazor-Webanwendung mit WebAPI entwickelt. Als Beispiel dient die Standard-‘Weather Forecast‘-Seite der Blazor-Vorlage. In der WebAPI wurde ein Controller hinzugefügt, der nach einer Verzögerung zufällige Wetterdaten zurückgibt. Der Zugriff auf die WebAPI erfolgt über einen Proxy (WeatherForecastProxy). Controller und Proxy implementieren beide das zugehörige Interface (IWeatherForecastExchange).
Dieser Programmfluss wurde zweimal implementiert: einmal ohne und einmal mit Cancellation Token. So können beide Szenarien, sowohl auf der Quellcode- als auch auf der Anwenderseite, miteinander verglichen werden.
Auf der Demo-‘Weather Forecast‘-Seite wurde ein Texteingabefeld hinzugefügt, welches auf das oninput
-Blazor-Event reagiert. Wenn der Eingabetext geändert wird, soll von der API ein neuer zufälliger Wetterdatensatz abgerufen werden. In der Komponente Weather.razor
(ohne Cancellation Token) wird über den Proxy jeweils ein Request gesendet, und entsprechend ändern sich die Daten im Frontend.
Um mit Cancellation Tokens zu arbeiten, benötigen wir in C# ein Objekt vom Typ CancellationTokenSource. In der textChanged
-Methode der Komponente WeatherWithCancellation.razor
wird der Token des vorherigen Aufrufs abgebrochen und eine neue CancellationTokenSource erstellt, deren Token an den Proxy übergeben wird.
1@code {
2 private List<WeatherForecastDTO>? forecasts;
3
4 private CancellationTokenSource _cts = new CancellationTokenSource();
5
6 protected override async Task OnInitializedAsync()
7 {
8 forecasts = await weatherForecastExchange.GetAll(DateTime.Now, _cts.Token);
9 }
10
11 private async Task textChanged(ChangeEventArgs e)
12 {
13 _cts.Cancel();
14 _cts = new CancellationTokenSource();
15 try
16 {
17 forecasts = await weatherForecastExchange.GetAll(DateTime.Now, _cts.Token);
18 }
19 catch (TaskCanceledException ex)
20 {
21 Console.WriteLine($"Task Canceled: {ex.Message}");
22 }
23 catch (Exception ex)
24 {
25 Console.WriteLine(ex.Message);
26 }
27 }
28}
Damit auf ein Cancellation Token reagiert werden kann, muss es in jede aufgerufene asynchrone Methode weitergegeben werden. Dementsprechend wird im Proxy und auch im Controller das Cancellation Token an die Service-Klasse, welche die Wetterdaten ermittelt, weitergereicht.
1public async Task<List<WeatherForecastDTO>> GetAll(DateTime startDate, System.Threading.CancellationToken cancellationToken)
2{
3 try
4 {
5 var result = await httpClient.GetFromJsonAsync<List<WeatherForecastDTO>>("/api/WeatherForecast", cancellationToken);
6 if (result != null)
7 {
8 return result;
9 }
10 return new List<WeatherForecastDTO>();
11 }
12 catch (Exception ex)
13 {
14 await Console.Out.WriteLineAsync(ex.Message);
15 throw;
16 }
17}
1[HttpGet]
2public async Task<List<WeatherForecastDTO>> GetAll(DateTime startDate, System.Threading.CancellationToken cancellationToken)
3{
4 try
5 {
6
7 await Task.Delay(1000, cancellationToken);
8 return Enumerable.Range(1, 5).Select(index => new WeatherForecastDTO
9 {
10 Date = startDate.AddDays(index),
11 TemperatureC = Random.Shared.Next(-20, 55),
12 Summary = Summaries[Random.Shared.Next(Summaries.Length)]
13 }).ToList();
14 }
15 catch (Exception ex)
16 {
17 throw;
18 }
19}
Um die Funktionsweise des Cancellation Tokens besser nachvollziehen zu können, habe ich die Fehler protokolliert. Wird ein Cancellation Token abgebrochen, so wird eine TaskCanceledException ausgelöst. Auf diese kann sowohl auf der API- als auch auf der Frontend-Seite reagiert werden. Tippt man bei diesem Testprojekt auf der Weather Cancellation-Seite in das Suchfeld, sieht man in der Konsole, dass dieser Fehler protokolliert wird.
Im Vergleich zur Seite ohne Cancellation Token sieht man hier, dass bei schnellem Tippen in das Suchfeld die Daten nur einmalig aktualisiert werden und die angezeigten Daten nicht hinter dem Eingabetempo des Benutzers zurückbleiben.