Using CancellationToken Without Polluting Your Code
One of the strengths of ASP.NET Core is how easily it integrates cooperative cancellation through the CancellationToken mechanism. The framework offers two main ways to access the cancellation token tied to the request: via HttpContext.RequestAborted, or by accepting a CancellationToken as a parameter in your controller action. Either approach works fine, but when you want to propagate cancellation deeper into your services without cluttering every method signature, things start to look messy.
Passing the token manually through every service call can quickly become tedious. It introduces noise, especially when cancellation is used opportunistically rather than being central to the business logic. Ideally, we want to access the current request’s cancellation token anywhere in the request pipeline without having to pass it explicitly.
In this post, I’ll walk through a simple solution using AsyncLocal<T> that lets you store the cancellation token once — and access it wherever you need it.
The Problem with Explicit Token Passing
Suppose you’re building an API that fetches weather data:
[HttpGet]
public async Task<IActionResult> Get(CancellationToken cancellationToken)
{
var forecast = await _weatherService.GetForecastAsync(cancellationToken);
return Ok(forecast);
}
This is clean enough on its own. But now imagine that _weatherService.GetForecastAsync() calls several internal helpers, which in turn query the database, perform I/O, or call other APIs — and all of them need the token. If each of those methods requires a CancellationToken, you’ll find yourself passing the token through every layer, even when the business logic doesn’t care about cancellation directly. The clutter adds up quickly. And it’s not just production code that suffers — your unit tests will also need to provide tokens everywhere, even for trivial scenarios, making them more verbose and harder to follow.
A Cleaner Approach with AsyncLocal
Instead of threading the token explicitly, we can use AsyncLocal<T> to store it in a context-local variable. This is a perfect match for ASP.NET Core’s asynchronous request handling model, where a single logical context flows through the entire request pipeline.
First, we define a simple accessor class that uses AsyncLocal<CancellationToken> under the hood:
public interface ICancellationTokenAccessor
{
CancellationToken Token { get; set; }
}
public class CancellationTokenAccessor : ICancellationTokenAccessor
{
private static readonly AsyncLocal<CancellationToken> _token = new();
public CancellationToken Token
{
get => _token.Value;
set => _token.Value = value;
}
}
We then register this accessor as a singleton:
builder.Services.AddSingleton<ICancellationTokenAccessor, CancellationTokenAccessor>();
Although it’s a singleton, AsyncLocal ensures each request context sees its own value.
Next, we write a small middleware component to extract the token and set it at the beginning of the pipeline:
public class CancellationTokenMiddleware
{
private readonly RequestDelegate _next;
public CancellationTokenMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext context, ICancellationTokenAccessor accessor)
{
accessor.Token = context.RequestAborted;
await _next(context);
}
}
Register it in Program.cs with:
app.UseMiddleware<CancellationTokenMiddleware>();
Now, in any service that’s part of the request scope, we can inject the ICancellationTokenAccessor and retrieve the token whenever we need it:
public class WeatherService : IWeatherService
{
private readonly ICancellationTokenAccessor _tokenAccessor;
public WeatherService(ICancellationTokenAccessor tokenAccessor)
{
_tokenAccessor = tokenAccessor;
}
public async Task<IEnumerable<Forecast>> GetForecastAsync()
{
var token = _tokenAccessor.Token;
return await FetchForecastAsync(token);
}
}
When This Makes Sense – and When It Doesn’t
This approach is particularly useful in large applications where service methods are layered deeply and the cancellation token is only occasionally needed. Instead of threading the token through every call, you centralize its management and keep your interfaces focused on business concerns.
However, this does introduce a form of ambient context, which can obscure where the token is coming from. For developers unfamiliar with the pattern, it may not be immediately clear how cancellation works without some explanation. But this is a trade-off – one that favors reducing boilerplate and improving code readability, especially in services that are already deeply nested.
It’s also worth noting that AsyncLocal flows only within the logical execution context. If you queue work on a background thread or store the token for later use outside the scope of the request, you risk unexpected behavior or outright bugs. This pattern is safe for anything that runs asynchronously within the same request, but avoid using it for anything that lives beyond that boundary.
Final Thoughts
Passing CancellationToken through every method is the standard, and in many cases, the correct approach. But for request-scoped flows where you want cancellation support without constantly threading tokens through unrelated parts of the code, this pattern provides a cleaner alternative. It’s a small investment that can lead to more maintainable code, and fewer method signatures that look like they’ve been to baggage claim.

RSS - Posts