Nobody is waiting for that response anymore
Every aborted HTTP request without cancellation keeps doing work for a client that is long gone: the action method keeps executing, the database keeps grinding through an expensive query, and the connection pool keeps a connection busy. Under load, exactly this pattern turns a hiccup into an outage, because impatient users who refresh the page double the work instead of cancelling it.
.NET has a first-class answer: the CancellationToken. ASP.NET Core creates it for you on every request. The only thing your code has to do is accept it and pass it on, every single time.
One abort travels through every layer
The complete chain works without any custom plumbing. Each layer only has to hand the token to the next one:
Try it: abort a request mid-flight
Start a simulated request whose SQL query takes a few seconds, then close the tab before it finishes. Switch the mode to see what changes when the CancellationToken is not forwarded.
- Browserwaiting
- ASP.NET Core (Kestrel)waiting
- Controller Actionwaiting
- EF Core + SqlClientwaiting
- SQL Serverwaiting
Tip: the query takes about nine seconds. Abort halfway through and watch how far the cancellation reaches.
1. The browser aborts the request
The user closes the tab, navigates away or the client hits a timeout. On the wire this simply means the connection is closed.
2. ASP.NET Core notices
Kestrel detects the closed connection and cancels HttpContext.RequestAborted. Declare a CancellationToken parameter in your action and model binding hands you exactly this token.
3. EF Core and SqlClient forward it
ToListAsync(cancellationToken) and friends pass the token through EF Core into Microsoft.Data.SqlClient, which is where the real magic happens.
4. SQL Server stops the query
SqlClient sends an attention signal to SQL Server, which aborts the possibly expensive query and frees its resources. Your action ends with an OperationCanceledException, which ASP.NET Core treats as the non-event it is.
[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);
}Architecture tests instead of code review discipline
The pattern only works when every action participates, and that is exactly what code reviews forget on a busy Friday. An architecture test turns the convention into a failing build: with NetArchTest.Rules (plain reflection works too) we collect every controller action and assert that it accepts a CancellationToken.
The test names each offending action in its failure message, so the fix is a ten second job. Analyzers like CA2016 complement the test by warning when a token exists but is not forwarded.
[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)}");
}Key takeaway
Accept the token in every action, pass it to every async call, and let an architecture test guard the convention. The result: aborted requests stop consuming your servers, your database and your patience.
More .NET services
Get In Touch
Let's Build Something Great Together
Have a question or want to discuss a project? We'd love to hear from you. Fill out the form below and we'll get back to you as soon as possible.
You can also reach us directly at [email protected]