Imagine a user starts downloading a large report from your ASP.NET Core API. A few seconds later, they close the browser or cancel the request. The client is no longer waiting for the result, but the server may still be querying the database, calling another API, or processing data.
If this happens frequently, your application can waste CPU time, database connections, and network resources on work nobody needs anymore.
This is where CancellationToken becomes useful. It allows an operation to receive a cancellation request and stop when it is safe to do so.
However, adding a cancellation token to a method does not automatically stop every operation inside it. Your code must pass the token to supported APIs and handle cancellation correctly.
In this article, we will learn how to use CancellationToken in ASP.NET Core, propagate cancellation through database and HTTP calls, and avoid common mistakes in production applications.
What Is CancellationToken in C#?
CancellationToken is a .NET type that allows one part of an application to notify another part that an operation should stop.
It does not forcibly terminate a running method. Instead, it provides a cooperative cancellation mechanism. The operation must check the token or call an API that observes it.
Consider this example:
public async Task ProcessAsync(
CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
await Task.Delay(
TimeSpan.FromSeconds(10),
cancellationToken);
Console.WriteLine("Processing completed.");
}If cancellation is requested while Task.Delay is running, the delay can stop early by throwing an OperationCanceledException.
The important point is that the token must reach the operation that can actually respond to cancellation.
How ASP.NET Core Handles Request Cancellation
ASP.NET Core exposes HttpContext.RequestAborted, a cancellation token that indicates when the current HTTP request has been aborted.
In controller actions, ASP.NET Core can also bind a CancellationToken parameter to the request-aborted token.
For example:
[HttpGet("report")]
public async Task<IActionResult> GetReport(
CancellationToken cancellationToken)
{
var report = await GenerateReportAsync(
cancellationToken);
return Ok(report);
}When the request is aborted, the token can signal that the operation should stop.
This is particularly useful for long-running database queries, external HTTP requests, file operations, and data processing.
However, cancellation is not guaranteed to happen immediately. Some operations may already have completed, and some dependencies may not support cancellation at every stage.
Step 1: Pass CancellationToken to Entity Framework Core
Database operations can consume resources even after a client stops waiting for the result.
Entity Framework Core provides asynchronous methods that accept a cancellation token, including ToListAsync, FirstOrDefaultAsync, and SaveChangesAsync.
Consider this example:
[HttpGet("orders")]
public async Task<IActionResult> GetOrders(
CancellationToken cancellationToken)
{
var orders = await _context.Orders
.AsNoTracking()
.OrderByDescending(o => o.CreatedAt)
.Take(100)
.Select(o => new
{
o.Id,
o.CreatedAt,
o.TotalAmount
})
.ToListAsync(cancellationToken);
return Ok(orders);
}The cancellation token is passed to ToListAsync. If the request is cancelled, EF Core can pass the cancellation request to the database provider.
Whether the underlying database operation stops immediately depends on the provider and database driver.
Without the token, the query may continue running even when the request is no longer useful. The exact behavior depends on the operation and provider, so test cancellation with your actual database setup.
Step 2: Cancel External HTTP Requests
ASP.NET Core applications often call external services to retrieve prices, customer details, payment status, or other information.
Use HttpClient methods that accept a cancellation token so an abandoned request does not unnecessarily wait for the external service.
public sealed class PricingService
{
private readonly HttpClient _httpClient;
public PricingService(HttpClient httpClient)
{
_httpClient = httpClient;
}
public async Task<string> GetPricingAsync(
CancellationToken cancellationToken)
{
using var response = await _httpClient.GetAsync(
"api/pricing",
cancellationToken);
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync(
cancellationToken);
}
}Register the service with IHttpClientFactory:
builder.Services.AddHttpClient<PricingService>(client =>
{
client.BaseAddress =
new Uri("https://pricing.example.com/");
client.Timeout = TimeSpan.FromSeconds(15);
});The timeout is an additional safeguard. It does not replace request cancellation.
The external service may still have received and started processing the request before cancellation reaches it. Cancellation primarily helps your application stop waiting and release resources when the underlying operation supports it.
For critical operations, such as a payment request, do not assume that cancelling the HTTP request reverses a transaction that the external service has already completed.
Step 3: Pass Cancellation Through Every Layer
A common mistake is accepting a cancellation token in the controller but not passing it to the service or repository.
Consider a service method:
public async Task<List<Product>> GetProductsAsync(
CancellationToken cancellationToken)
{
return await _context.Products
.AsNoTracking()
.ToListAsync(cancellationToken);
}The controller should pass its token to the service:
[HttpGet("products")]
public async Task<IActionResult> GetProducts(
CancellationToken cancellationToken)
{
var products = await _productService
.GetProductsAsync(cancellationToken);
return Ok(products);
}
Join the conversation! Your thoughts help the community grow.