In async
code it is very common to pass a CancellationToken
to the method that is being awaited. This allows the caller to cancel the operation if it is no longer needed. But this will lead to some ceremonial code that is repeated in every method. In this article I will show you how to manage CancellationToken
in a central service in ASP.NET.
The "normal" way
Imagine we have a service as such:
class SomeService
{
async Task DoSomething(CancellationToken token)
{
try
{
await Task.Delay(100_000, token);
}
catch (TaskCanceledException)
{
Console.WriteLine("Task was canceled");
}
}
}
That works quite nicely! To pass it down in a controller (here Minimal API) you have to do the following:
app.MapGet("/service", async (SomeService service, CancellationToken token) =>
{
await service.DoSomething(token);
return Results.Ok();
});
That is somewhat nice, but leads to two issues:
- You have to pass the
CancellationToken
to every method that is being awaited down the whole call chain. That includes methods that are just passing them down without actually using them. - You have to setup the method signature on every API call.
But we can also manage all of that in a central service.
Middleware to the rescue
We can leverage middlewares to put the CancellationToken
into a central service which then can be used by every method that needs it. Here is how you can do it:
builder.Services.AddScoped<TokenService>();
builder.Services.AddScoped<ITokenRetriever>(c => c.GetRequiredService<TokenService>());
app.Use((context, func) =>
{
var tokenService = context.RequestServices.GetRequiredService<TokenService>();
tokenService.Token = context.RequestAborted;
return func();
});
interface ITokenRetriever
{
CancellationToken Token { get; }
}
public class TokenService : ITokenRetriever
{
public CancellationToken Token { get; set; }
}
The usage would be like this:
class SomeService
{
private readonly ITokenRetriever _tokenRetriever;
public SomeService(ITokenRetriever tokenRetriever)
{
_tokenRetriever = tokenRetriever;
}
public async Task DoSomething()
{
try
{
await Task.Delay(100_000, _tokenRetriever.Token);
}
catch (TaskCanceledException)
{
Console.WriteLine("Task was canceled");
}
}
}
The API call would look like this:
app.MapGet("/service", async (SomeService service) =>
{
await service.DoSomething();
return Results.Ok();
});
This way you don't have to pass the CancellationToken
down the call chain and you don't have to setup the method signature on every API call. BUT: You are creating dependencies to the ITokenRetriever
in every service that needs the CancellationToken
. So choose wisely if you want to use this approach. If you see that you are just passing down the token a lot without the really cancelling something your service controls, the shown approach might be a good fit for you. Otherwise you should stick to the "normal" way, which is also more known.