DEV Community

Rasheed K Mozaffar
Rasheed K Mozaffar

Posted on • Updated on

Cancellation Tokens in C#

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!

Top comments (15)

Collapse
 
parentelement profile image
Dustin Horne (parentelement)

Good primer. I read this as someone who already knows and uses cancellationTokens (the way it was intended, not just default value and ignoring). One thing I'd recommend adding to this is an example and using linked sources through CancellationTokenSource.CreateLinkedTokenSource(...). This is where cancellation tokens get really powerful because you then get your own token that you can use and cancellation can be reacted to wether it's the original token or yours. I use this technique in my ParentElement.ReProcess package (source is on GitHub).

Collapse
 
rasheedmozaffar profile image
Rasheed K Mozaffar

Tysm for mentioning that!
I was thinking of making a sequel to this post where I delve into more details and code samples, so I'll definitely come back to your recommendation!

Collapse
 
pinaki_basu_27d0350c46cdf profile image
Pinaki Basu

learned something which could be useful!

Collapse
 
fredrikskne profile image
Fredrik Karlsson

Why are you mixing Thread.Sleep and async/await?

Collapse
 
rasheedmozaffar profile image
Rasheed K Mozaffar

Thanks for pointing that out!

Collapse
 
krakoss profile image
krakoss

this is very useful information

Collapse
 
rumendimov profile image
Rumen Dimov • Edited

I have used CacellationTokens when running BackgroundService tasks. But I never thought about using them with EF core retrieving data as shown in the example.

Awesome content!

Collapse
 
chung_ontsui_c76da0625d0 profile image
Chung On Tsui

Good article on a topic probably not considered often enough, good job πŸ‘πŸ»

Collapse
 
rasheedmozaffar profile image
Rasheed K Mozaffar

Thank you! Glad you liked it

Collapse
 
adn_ng_5d75dafc2f4b7f531f profile image
Adn Ng

thank you

Collapse
 
rasheedmozaffar profile image
Rasheed K Mozaffar

Glad that was useful 😁

Collapse
 
softwaredeveloping profile image
FrontEndWebDeveloping

Hey Rasheed! How is Flow going? Have you finished your second version?

Collapse
 
rasheedmozaffar profile image
Rasheed K Mozaffar

Hey mate!
Uhh unfortunately i couldn't proceed with the development, I got swamped with some freelance work, college assignments and I'm preparing to start my new job next month, so I had to stop working on Flow, i dunno if I'll get back to it later or not, but I'm sure once everything settles, I'll find some time to continue working on it!

Collapse
 
softwaredeveloping profile image
FrontEndWebDeveloping

Okay... sounds good. Thanks Rasheed.

Collapse
 
tablepad profile image
Tablepad

Awensome post!