ASP.NET Core

CancellationTokens: vom abgebrochenen Request bis zum SQL Server

Schliesst ein Benutzer den Tab, wartet niemand mehr auf die Antwort. Wir zeigen, wie ASP.NET Core den Abbruch erkennt, wie die Cancellation durch alle Schichten bis zum SQL Server wandert und wie Architekturtests das Muster absichern.

01
Browser
Tab geschlossen, weiternavigiert, Timeout: die Verbindung ist weg
02
ASP.NET Core
Kestrel löst HttpContext.RequestAborted aus
03
EF Core + SqlClient
Der Token wird an die laufende Abfrage durchgereicht
04
SQL Server
Ein Attention-Signal stoppt die teure Query
Warum es wichtig ist

Auf diese Antwort wartet niemand mehr

Jeder abgebrochene HTTP-Request ohne Cancellation arbeitet für einen Client weiter, der längst weg ist: Die Action rechnet weiter, die Datenbank kaut an einer teuren Query, und der Connection Pool hält eine Verbindung besetzt. Unter Last macht genau dieses Muster aus einem Schluckauf einen Ausfall, denn ungeduldige Benutzer, die die Seite neu laden, verdoppeln die Arbeit, statt sie abzubrechen.

.NET hat darauf eine erstklassige Antwort: den CancellationToken. ASP.NET Core erstellt ihn bei jedem Request automatisch. Der eigene Code muss ihn nur annehmen und weiterreichen, und zwar jedes Mal.

Die Kette

Ein Abbruch wandert durch alle Schichten

Die komplette Kette funktioniert ohne eigenes Plumbing. Jede Schicht muss den Token nur an die nächste weitergeben:

Ausprobieren: einen Request mitten im Flug abbrechen

Starten Sie einen simulierten Request, dessen SQL-Query einige Sekunden dauert, und schliessen Sie den Tab, bevor sie fertig ist. Wechseln Sie den Modus, um zu sehen, was sich ohne weitergereichten CancellationToken ändert.

  1. Browser
    wartet
  2. ASP.NET Core (Kestrel)
    wartet
  3. Controller Action
    wartet
  4. EF Core + SqlClient
    wartet
  5. SQL Server
    wartet

Tipp: Die Query dauert rund neun Sekunden. Brechen Sie auf halbem Weg ab und beobachten Sie, wie weit die Cancellation kommt.

1. Der Browser bricht ab

Der Benutzer schliesst den Tab, navigiert weiter oder der Client läuft in einen Timeout. Auf der Leitung heisst das schlicht: Die Verbindung wird geschlossen.

2. ASP.NET Core merkt es

Kestrel erkennt die geschlossene Verbindung und cancelt HttpContext.RequestAborted. Deklariert die Action einen CancellationToken-Parameter, liefert das Model Binding genau diesen Token.

3. EF Core und SqlClient reichen weiter

ToListAsync(cancellationToken) und Co. geben den Token durch EF Core an Microsoft.Data.SqlClient weiter, und dort passiert der eigentliche Trick.

4. SQL Server stoppt die Query

SqlClient schickt ein Attention-Signal an den SQL Server, der die womöglich teure Query abbricht und die Ressourcen freigibt. Die Action endet mit einer OperationCanceledException, die ASP.NET Core als das behandelt, was sie ist: kein Ereignis.

Controller-Action mit CancellationToken
[HttpGet("reports/{year:int}")]
public async Task<IActionResult> GetYearlyReport(
    int year,
    CancellationToken cancellationToken)
{
    // Model binding fills this token from HttpContext.RequestAborted.
    // Passing it on means SQL Server receives an attention signal and
    // stops the query the moment the client disconnects.
    var report = await dbContext.Orders
        .Where(o => o.CreatedAt.Year == year)
        .GroupBy(o => o.CustomerId)
        .Select(g => new { CustomerId = g.Key, Total = g.Sum(o => o.Total) })
        .ToListAsync(cancellationToken);

    return Ok(report);
}
Absichern

Architekturtests statt Code-Review-Disziplin

Das Muster funktioniert nur, wenn jede Action mitmacht, und genau das geht in Code Reviews an einem vollen Freitag unter. Ein Architekturtest macht aus der Konvention einen fehlschlagenden Build: Mit NetArchTest.Rules (pure Reflection geht genauso) sammeln wir alle Controller-Actions ein und prüfen, dass sie einen CancellationToken akzeptieren.

Der Test nennt jede fehlbare Action in seiner Fehlermeldung, die Korrektur ist damit eine Sache von Sekunden. Analyzer wie CA2016 ergänzen den Test und warnen, wenn ein Token zwar existiert, aber nicht weitergereicht wird.

Architekturtest mit NetArchTest.Rules
[Fact]
public void Controller_actions_accept_a_CancellationToken()
{
    var controllers = Types.InAssembly(typeof(OrdersController).Assembly)
        .That()
        .HaveNameEndingWith("Controller")
        .And().Inherit(typeof(ControllerBase))
        .GetTypes();

    var offenders = controllers
        .SelectMany(c => c.GetMethods(BindingFlags.Public | BindingFlags.Instance))
        .Where(m => m.GetCustomAttributes<HttpMethodAttribute>().Any())
        .Where(m => m.GetParameters()
            .All(p => p.ParameterType != typeof(CancellationToken)))
        .Select(m => $"{m.DeclaringType!.Name}.{m.Name}")
        .ToList();

    offenders.ShouldBeEmpty(
        $"These actions do not accept a CancellationToken: " +
        $"{string.Join(", ", offenders)}");
}

Das Wichtigste

Den Token in jeder Action annehmen, an jeden async Call weiterreichen und die Konvention von einem Architekturtest bewachen lassen. Das Ergebnis: Abgebrochene Requests verbrauchen weder Server noch Datenbank noch Geduld.

Kontakt

Lassen Sie uns gemeinsam etwas Grossartiges aufbauen

Haben Sie eine Frage oder möchten Sie ein Projekt besprechen? Wir freuen uns, von Ihnen zu hören. Füllen Sie das untenstehende Formular aus und wir melden uns so schnell wie möglich bei Ihnen.

0 / 2000
oder

Sie können uns auch direkt erreichen unter [email protected]