The Importance of the Cancellation Token

Have you ever wondered what happens when you cancel a request? Well, unfortunately, in many cases, nothing happens, but this shouldn't be the norm, at least not in C#. Find out in this post what actually happens and what should happen instead.

 

 

 

1 - What is the CancellationToken?

 

Whenever we develop APIs, among other pieces of software, we see there's a type called CancellationToken. Unfortunately, it often gets ignored. And this doesn't just happen in hobby code, but also in enterprise code, which is a problem.

 

The CancellationToken is a mechanism or structure in .NET that allows us to cancel the current process; technically, what it does is check whether the process should be interrupted.

This process should be async/await, and what we do is, at every async call, we pass our cancellation token and the async/await itself will check whether it should be interrupted or not.

 

This interruption can be triggered automatically in code (after X seconds), or in the case of an API or a web app, the user can manually cancel the operation.

When you load a webpage and see the `X` to stop loading, that triggers the CancellationToken, or even when you close the tab.

Obviously, if it were a code client, you could replicate that experience as well.

 

 

2 - How does the CancellationToken work?

 

When we talk about APIs, every request comes linked to a CancellationToken, which is generated (well, the framework generates it), and we can access it inside each of our endpoints.

From our endpoint, we have to pass it to each of our asynchronous processes or functions happening in the process.

 

Under the hood, what's happening is a pattern described in this Microsoft blog:

ct explanation

What happens is that (or actually, .NET does it for us) we instantiate a CancellationTokenSource object, which, among other elements, contains the token that gets copied across the different asynchronous methods.

 

It's important to mention it's for asynchronous methods because it's the async/await structure that contains the logic to know if something needs to be canceled or not.

Another feature of CancellationTokenSource is the .Cancel element, which can cancel a token, and it gets executed when the user hits cancel or closes the tab.

 

Then in the code, if the user cancels, the OperationCancelledException exception is thrown.

 

 

3 - Cancellation Token Implementation in Code

 

The code we're about to look at is part of my distributed systems course and the code is on GitHub.

 

In this case, let's look at the use case of creating a product. The logic is as follows:

 public async Task<Result<CreateOrderResponse>> Execute(CreateOrderRequest createOrder,
        CancellationToken cancellationToken = default(CancellationToken))
{
    return await CreateOrder(createOrder)
        .Async()
        //On a real scenario:
        //validate orders
        .Bind(x=> ValidateFraudCheck(x, cancellationToken))
        .Bind(x => SaveOrder(x, cancellationToken))
        .Then(x => MapToOrderResponse(x, cancellationToken)
            .Bind(or => PublishDomainEvent(or, cancellationToken)))
        .Map(x => new CreateOrderResponse(x.Id, $"order/getorderstatus/{x.Id}"));
}
  1. We create the order
  2. We perform validations
  3. We save the order to the database
  4. We create the domain event
  5. Response to the user

 

 

In our current case, we don't have an implementation for what would be the fraud check,

private async Task<Result<OrderDetails>> ValidateFraudCheck(OrderDetails orderDetails,
        CancellationToken cancellationToken)
{
    //Validate fraud check
    return orderDetails;
}

But for those who haven't worked with point of sale systems, you would send the data about what is being purchased, name, address, etc., and that service evaluates whether the payment can be fraudulent or not; if so, it returns an error.

That call would be over HTTP and, as such, would carry a cancellation token.

 

 

The same applies to saving the information to the database.

private async Task<Result<OrderDetails>> SaveOrder(OrderDetails orderDetails, CancellationToken cancellationToken)
{
    await _orderRepository.Save(orderDetails, cancellationToken);
    return orderDetails;
}

 

 

The same applies to publishing a domain event.

private async Task<Result<Guid>> PublishDomainEvent(OrderResponse orderResponse,
        CancellationToken cancellationToken)
{
    await _domainMessagePublisher.Publish(orderResponse, routingKey: "order", cancellationToken: cancellationToken);
    return orderResponse.OrderId;
}

But what we've just seen is the use case; how does the CancellationToken get here? The answer is simple: from the controller.

 

In .NET or C#, the controller automatically provides it when you add it as one of the parameters.

[HttpPost("create")]
[ProducesResponseType(typeof(ResultDto<CreateOrderResponse>), (int)HttpStatusCode.Created)]
public async Task<IActionResult> CreateOrder(CreateOrderRequest createOrderRequest,
    CancellationToken cancellationToken = default(CancellationToken))
{
    return await _createOrderService.Execute(createOrderRequest, cancellationToken)
        .UseSuccessHttpStatusCode(HttpStatusCode.Created)
        .ToActionResult();
}

 

 

3.1 - Do I have to use CancellationToken everywhere?

 

In this example we've seen how the CancellationToken goes everywhere, but is this the best practice?

 

Probably not, and let's see why. There are a few things to keep in mind when using a cancellation token: when you cancel the token, you cancel the process, which means nothing beyond the cancellation point will work; it's basically a regular exception.

So if you cancel midway, you end up in an invalid state for your application.

 

 

If we go back to the earlier case, we're cancelling at every step, and that's wrong, because once we validate the event and save, the rest of the operation should be completed. So in our code, we should modify it so that only validation and saving operations get the cancellation token.

 public async Task<Result<CreateOrderResponse>> Execute(CreateOrderRequest createOrder,
        CancellationToken cancellationToken = default(CancellationToken))
{
    return await CreateOrder(createOrder)
        .Async()
        //On a real scenario:
        //validate orders
        .Bind(x=> ValidateFraudCheck(x, cancellationToken))
        .Bind(x => SaveOrder(x)) 
        .Then(x => MapToOrderResponse(x) 
            .Bind(or => PublishDomainEvent(or))) 
        .Map(x => new CreateOrderResponse(x.Id, $"order/getorderstatus/{x.Id}"));
}

 

This is simple. If the token is canceled during the save execution, we should ignore it since we need to ensure the entire process completes once we get to that point.

On the other hand, if the cancellation occurs during validation, the exception will be thrown before saving, which isn't a problem since no data in the system has been modified.

 

 

3.2 - Basic implementation of cancellation token in .NET

 

If you came to this post looking for CancellationToken info but have never seen any of my posts before, you might not understand the previous parts, so here’s a simple example that you can easily try out:

app.MapGet("/cancellation-example", async (CancellationToken ct) =>
{
   foreach (var i in Enumerable.Range(0,1000))
   {
       Console.WriteLine($"iteration: {i}");
       await Task.Delay(i * 1000, ct); //this simulates a call to  a service
       Console.WriteLine("Completed;");
   }
});

 

When you make the request in your browser, you'll see the console shows the info, and right when you cancel, the OperationCanceledException will be thrown, which means no other line of code will be executed. But whether the process actually ends or not depends on where you're calling it from and whether the CancellationToken is properly implemented.

 

 

4 - Where should I use cancellation token?

 

As we saw in the previous section, it's not always necessary to use the cancellation token; in fact, it's intended for a very specific use case: for operations that take a long time to complete.

 

For example, you have a process that queries a service or database that returns a thousand records, then you have to individually query each one and then do further lookups. It's a bit chaotic, but imagine a structure like this:

call sequence

We're making lots of calls at different levels to gather all the information; that means it's a slow process that might take a minute, two, or, I've seen some "reports" that take up to 10 minutes. That's where we want the cancellation token; we want the system to stop gathering information if the user closes the window or cancels the process.

 

  • Note: There are alternative solutions for this, but as an example it serves our purpose.

 

The CancellationToken is not meant for simple CRUDs that will take very little time.

 

 

4.1 - What if I'm forced to pass a CancellationToken?

 

It's not very common, but you might find yourself in a method where you're forced to pass a CancellationToken, but either you don't have one or you simply don't want the operation to be cancelable.

 

In that case, you can pass CancellationToken.None, and nothing will be canceled after that.

 

 

4.2 - Does the CancellationToken cancel external services?

 

The short answer is no: we should assume that the CancellationToken is for our code only.

 

The most common example is when we make a database query, as we can pass the CancellationToken. When we do, we'll cancel the invocation from Entity Framework Core, but nothing guarantees it'll be canceled at the database or dependency level.

 

For example, SQL Server supports CancellationToken, although its functionality is not always guaranteed.

 

This post was translated from Spanish. You can see the original one here.
If there is any problem you can add a comment bellow or contact me in the website's contact form

Uso del bloqueador de anuncios adblock

Hola!

Primero de todo bienvenido a la web de NetMentor donde podrás aprender programación en C# y .NET desde un nivel de principiante hasta más avanzado.


Yo entiendo que utilices un bloqueador de anuncios como AdBlock, Ublock o el propio navegador Brave. Pero te tengo que pedir por favor que desactives el bloqueador para esta web.


Intento personalmente no poner mucha publicidad, la justa para pagar el servidor y por supuesto que no sea intrusiva; Si pese a ello piensas que es intrusiva siempre me puedes escribir por privado o por Twitter a @NetMentorTW.


Si ya lo has desactivado, por favor recarga la página.


Un saludo y muchas gracias por tu colaboración

© copyright 2025 NetMentor | Todos los derechos reservados | RSS Feed

Buy me a coffee Invitame a un café