DEV Community

Cover image for Using cancellation tokens on ASP.NET Core MVC actions
João Antunes
João Antunes

Posted on

Using cancellation tokens on ASP.NET Core MVC actions

Today I'm back for a smaller post, on a topic that has already some amount of posts about, but I think is not spread enough and it's not being used as much as it could, and probably should - using cancellation tokens with ASP.NET Core MVC actions.

If you want to follow along with the code, it's here. If you prefer to watch a video walk-through, skip to the end of the post.

A scenario

Imagine we create a Web API to power our frontend. We have a search box with autocomplete, have the usual debounce logic to avoid a request on every keystroke, but even with this logic in place, sometimes it takes longer for the user to complete the train of thought. When this happens, the client side application makes a new request and disregards the previous ones. On the server side, if we're not expecting this to happen, the application will just complete the requested operation and issue a response as if nothing happened, having completed a workload for nothing, misusing the server's resources.

A solution

Fortunately, there's a stupid simple solution for this problem. On our controller's action methods, we can receive a CancellationToken instance and pass it around our asynchronous methods. When the client cancels a request, this token will be signaled and our methods are notified and can act accordingly.

Some samples

For the samples, I'm making something simpler then the autocomplete scenario I talked above, but I think it's more than enough to take the point across.



[Route("thing")]
public async Task<IActionResult> GetAThingAsync(CancellationToken ct)
{
    try
    {
        await _httpClient.GetAsync("http://httpstat.us/204?sleep=5000", ct);
        _logger.LogInformation("Task completed!");
    }
    catch (TaskCanceledException)
    {
        _logger.LogInformation("Task canceled!");

    }
    return NoContent();
}


Enter fullscreen mode Exit fullscreen mode

So here we have a stupid simple action, that has no client supplied arguments, only a CancellationToken. This CancellationToken is injected by the framework and will be signaled for cancellation by the framework. A case in which it is signaled - when the client cancels the request. In the demo code I'm just invoking an external endpoint that will take 5 seconds to complete, passing the token to the GetAsync method. I'm wrapping it all up in try catch and expecting a TaskCanceledException, as it's the one thrown when the operation is canceled due to cancellation token being signaled.

Below I added a gif with a quick show of a request being canceled.

Cancellation tokens in action

Here's another quick sample:



[Route("anotherthing")]
public async Task<IActionResult> GetAnotherThingAsync(CancellationToken ct)
{
    try
    {
        for(var i = 0; i < 5; ++i)
        {
            ct.ThrowIfCancellationRequested();
            //do stuff...
            await Task.Delay(1000, ct);
        }
        _logger.LogInformation("Process completed!");
    }
    catch (Exception ex) when (ex is TaskCanceledException || ex is OperationCanceledException)
    {
        _logger.LogInformation("Process canceled!");

    }
    return NoContent();
}


Enter fullscreen mode Exit fullscreen mode

In this case, besides passing the CancellationToken along to other asynchronous methods - in this case a Task.Delay but the result of the cancellation is similar to the HttpClient.GetAsync - I'm checking the token to see if it was signaled for cancellation. So, imagine the Task.Delay method didn't accept a CancellationToken as an argument, on the next loop iteration the code would check for a cancellation request and would throw an exception (in this case an OperationCanceledException).

This may be useful in scenarios we're not doing async work that nevertheless takes a while to complete and would benefit from being cancellable along the way.

Video walk-through

If you're more in a mood for video walk-through rather the reading, be my guest :)

Hope this was useful.
Thanks for reading/watching, cyaz!

PS: originally posted here

Top comments (7)

Collapse
 
tan_truongthe_82018bd181 profile image
tan truong the

i have a question. It is mandatory to pass token from controller to service and then to repository, it there any way shorter because i want to use them in repo only ? I found a suggestion that use addScope(). It will be injected to repo instead, do you have any suggestion or any solution for this ?

Collapse
 
joaofbantunes profile image
João Antunes

Hey!

(sorry for the late reply, didn't get a notification for this comment 😬)

While it's not mandatory (as you said, there are workarounds with dependency injection and stuff), it is the recommended approach. Cancellation is considered to be cooperative, that is, the various components cooperate by passing the token around to allow for cancellation. Sometimes though (not that often I guess, but happens) you might not want to pass it in a specific scenario, for example if you already did some work and would be better to try and finish it instead of canceling in the last step. If you don't follow the cooperative approach, this can't be done.

Even if you don't have scenarios where you don't want to pass the cancellation token, I'd still follow the cooperative approach. For one, because it's the idiomatic way to do it in C#, but also because the alternatives feel a bit heavyweight just to avoid passing an extra parameter around.

Collapse
 
loferreiranuno profile image
loferreiranuno • Edited

Just a question.

How do we handler parameters on the Method?

I've tryed set something such


Task Method(string tag, CancellationToken ctx) and I'm getting an 415 Code.

Collapse
 
joaofbantunes profile image
João Antunes

Hi,

I'm not really sure of what may cause this (but I have a vague idea of such problem in the past), so I'll drop a couple of ideas that may be able to help you investigate.

  • Depending on if it's a GET, POST, etc, try to use an attribute to indicate from where the parameter comes from, e.g.: [FromRoute], [FromQuery], [FromBody].
  • Instead of the first idea, you could instead use the [ApiController] attribute, which will use some defaults to resolve the parameters, e.g. simple types come from route or query, complex ones come from body.
Collapse
 
ronaldoperes profile image
Ronaldo Peres

Hi,

This will be very helpful.

Thanks!!

Collapse
 
joaofbantunes profile image
João Antunes

Glad to know!

Collapse
 
joaofbantunes profile image
João Antunes

Glad to know 🙂