Managing TaskCancellationTokens in a central service in ASP.NET

16/09/2024

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:

  1. 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.
  2. 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.

Create your own Validationattribute in ASP.NET Core

In this small blog post, I will show you how to create your own Validation attribute in ASP.NET Core to tailor-made your validation rules.

ASP.NET Core - Why async await is useful

Did you ever wonder why you "should" use async and await in your ASP.NET Core applications? Most probably, you heard something about performance. And there is some truth to it, but not in the way you might think.

So let's discuss this with smaller examples.

Building a Minimal ASP.NET Core clone

In this article, we will build a minimal version of what ASP.NET Core does - yes, you read right. We will create a very simplistic clone of ASP.NET Core to discuss how the whole thing works. Beginning with a simple console application, we will add the necessary components to make it work as a web server. Bonus points for our own middleware pipeline and dependency injection.

An error has occurred. This application may no longer respond until reloaded. Reload x