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;
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:
CancellationTokenSource()
CancellationTokenSource(int millisecondsDelay)
CancellationTokenSource(TimeSpan delay)
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);
}
}
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:
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");
}
}
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;
}
}
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!
Top comments (17)
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).
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!
this is very useful information
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!
Why are you mixing Thread.Sleep and async/await?
Thanks for pointing that out!
It is great!!
learned something which could be useful!
Good article on a topic probably not considered often enough, good job 👍🏻
Thank you! Glad you liked it
Awensome post!
thank you
Glad that was useful 😁
It's very useful