Cancellation Tokens in C#

Rasheed K Mozaffar - Jun 11 - - Dev Community

Hi There! πŸ‘‹πŸ»

It's been a while since I wrote something about C#, or wrote anything in general, but now I have an interesting concept that I'm sure you've stumbled upon or seen somewhere in a C# code base.
If you used Entity Framework, HttpClient or anything that have methods which can do asynchronous work, it's almost guaranteed an overload accepting a CancellationToken was present there.

So what are cancellation tokens? Why are they used? What's the benefit of them? Buckle up, because that's what we are going to answer in this post!

Defining Cancellation Tokens πŸͺ™

A cancellation token in C# is a readonly struct, which is defined as the following according to Microsoft documentation:

πŸ”” Propagates notification that operations should be canceled.

What that means is, a cancellation token is an object used to tell if an operation should be cancelled before it's done executing.
This mechanism is definitely something you must be aware of, and should have an idea of how to use it and when.

If you're not convinced of its importance, I want to demonstrate a real example that should be sufficient to hook you and make you a massive proponent of using cancellation tokens whenever possible.

Imagine you visit a website like Amazon, you navigate to a certain product category, and once you do, Amazon begins loading a bunch of products from that category, but imagine you pressed on a wrong category, and you immediately navigated back. Assuming Amazon doesn't use the concept of task cancellation in general, the website will still continue to process the request even though you navigated back, the task was just not cancelled, and the database query will still run and return data that will never be used.

This in essence, is wasted application resources. The app had to perform a potentially demanding database query, to return the retrieved data nowhere. This not only applies to data retrieval, it could be any other operation, like a network call to a remote API, or an IO operation.

The origin of CancellationToken in C# πŸ’‘

Now that you're likely convinced that you SHOULD incorporate cancellation tokens in your asynchronous C# code, let's get into the C# specifics regarding this concept.

In C#, to get a cancellation token, we need to create an instance of CancellationTokenSource, and through that source object, we can obtain the associated cancellation token by using the Token property on that object. The following code demonstrates the creation of a CancellationToken in a C# console app:




CancellationTokenSource cts = new();

CancellationToken cancellationToken = cts.Token;



Enter fullscreen mode Exit fullscreen mode

A little bit about the CancellationTokenSource, it's the object that will signal to a cancellation token that it should be cancelled, it could be immediate cancellation by calling a cancellation token's Cancel method, or CancelAfter to specify when the token should switch to the cancelled state.

The source class, has 4 different constructors:

  1. CancellationTokenSource()
  2. CancellationTokenSource(int millisecondsDelay)
  3. CancellationTokenSource(TimeSpan delay)
  4. CancellationTokenSource(TimeSpan delay, TimeProvider timeProvider)

To sum them up, all the ones that do have a delay, will create an instance that will signal cancellation on the instance's cancellation token after the provided delay in the constructor.

A basic coding sample

We've talked theory, but now I want to show you a basic code snippet, that should sort of illustrate the concept code-wise.




CancellationTokenSource cts = new(3000);
CancellationToken cancellationToken = cts.Token;

Task lazyCountingTask = Task.Run(() => LazyCounterFunction(cancellationToken), cancellationToken);

try
{
    await lazyCountingTask;
}
catch (OperationCanceledException ex)
{
    Console.WriteLine("Cancellation Was Requested! Lazy counter defeated");
}

static async Task LazyCounterFunction(CancellationToken cancellationToken)
{
    int counter = 0;
    while (true)
    {
        if (cancellationToken.IsCancellationRequested)
        {
            Console.WriteLine("WE'VE BEEN COMMANDED TO CANCEL!!!");
            cancellationToken.ThrowIfCancellationRequested();
        }

        Console.WriteLine($"Counting... Currently At: {counter++}");
        await Task.Delay(1000);
    }
}



Enter fullscreen mode Exit fullscreen mode

The code provided creates a counter function, which increments a counter value every second starting from 0. Inside the while loop, we check the IsCancellationRequested property on the cancellation token instance, so that when cancellation is triggered, we log a message to the console, and then call ThrowIfCancellationRequested() on that token. This method will throw an OperationCancelledException, which the calling code can catch to execute some logic in case the task was cancelled.

Inside Main, we create a task then await it inside a try catch block. However, when I instantiated the cancellation token source, I passed 3000 as an argument to it, which is a delay in milliseconds, which should simulate a process taking 3 seconds before getting cancelled.

If you run the code, you should see the following output:
Output of the sample cancellation token code

Real Use Cases of Cancellation Tokens βš™οΈ

Now with the sample counter program out of the way, let's investigate some real world use cases that highlight the benefit of cancellation tokens.




[HttpGet("get-users")]
public async Task<IActionResult> GetUsersAsync(CancellationToken cancellationToken)
{
    try
    {
        var users = await UsersRepo.GetAllUsersAsync(cancellationToken);

        return Ok(users);
    }
    catch (OperationCanceledException ex)
    {
        return Ok("Cancelled loading users query");
    }
}



Enter fullscreen mode Exit fullscreen mode

Starting off with this demo API endpoint, this endpoint is supposed to load all the users available in the database and return them.

⚠️ Please note that this is an incomplete implementation, in such a scenario, you need to handle other types of exceptions, add pagination, filtering and more.

In our endpoint parameters, we added a cancellation token parameter, now when you write a service using HttpClient to call this endpoint for instance, you can pass a cancellation token which as we said earlier, you can get from a cancellation token source object and maybe in the user interface relying on that http service, you could either add a cancel button, or check for when the user navigates from the page, if the task was still undone, you can call the Cancel method such that the task is cancelled properly.

Cancellation Tokens With Entity Framework Core πŸ—‚οΈ

When using the Async variants of Entity Framework Core LINQ methods, you'll always find an overload accepting a cancellation token, and now since you learned how to use this powerful tool, you should always pass one when possible.

Let's investigate this piece of code:




public class EventsManagementService
{
    private readonly ApplicationDbContext _dbContext;

    public EventsManagementService(ApplicationDbContext dbContext)
    {
        _dbContext = dbContext;
    }

    public async Task<List<LogStore>> GetEventsAsync(CancellationToken cancellationToken)
    {
        // Use AsNoTracking() to avoid change tracking overhead
        var events = await _dbContext.LogStore.AsNoTracking().ToListAsync(cancellationToken);
        return events;
    }
}



Enter fullscreen mode Exit fullscreen mode

In this events management service class, we have a method that retrieves a list of LogStore objects, the method accepts a cancellation token, which is passed down to the ToListAsync method. This method of EF Core asynchronously creates a list from an IQueryable by enumerating the results from the query. This way, when cancellation is requested, this method will not continue to create the list thus saving our application from burned resources.

Conclusion

In this post, we've looked at the concept of task cancellation in general, and the way it works in C#. I didn't want to dive much in the theory, but instead show you actual code so that you see the concept in use. Now equipped with this new knowledge, you can smoothly obtain a cancellation token, set a cancellation delay, and actually cancel an async operation.
I hope you learned something new from this post!

Thanks for Reading!

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .