This is a very small blog post on why you should use CancellationToken
s in your API.
It is not only about your code
Imagine we have a long running SQL Query like the following:
SELECT COUNT_BIG(*)
FROM sys.all_objects a
CROSS JOIN sys.all_objects b
CROSS JOIN sys.all_objects c
CROSS JOIN sys.all_objects d
CROSS JOIN sys.all_objects e;
Of course, this is just "useless" demonstration code but the idea is that it runs some longer period of time and may incur significant CPU/Memory/IO utilization. If that is part of your (REST) API and the request gets cancelled, the query still continues to run.
You can check that easily via:
SELECT * FROM sys.dm_exec_requests WHERE status = 'running';
And you see that this query runs even if the REST call gets aborted (it is different if you use a console application you kill, because that would terminate the underlying connection and transaction).
Now if we provide a CancellationToken
:
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(2));
try
{
await dbContext.Database.ExecuteSqlAsync(
$"""
SELECT COUNT_BIG(*)
FROM sys.all_objects a
CROSS JOIN sys.all_objects b
CROSS JOIN sys.all_objects c
CROSS JOIN sys.all_objects d
CROSS JOIN sys.all_objects e;
""", cts.Token);
}
catch (OperationCanceledException)
{
Console.WriteLine("Query cancelled!");
}
If we then check with SELECT * FROM sys.dm_exec_requests WHERE status = 'running';
you can see that after the cancellation happened, the server also does terminate the query!
So the real power is not one-dimensional: Not only your code scales better because it can free resources that aren't needed anymore, but if the external system also supports that, well: They can also safe resources.
In this example this was an SQL Server that reacts on the cancellation. But if you also provide the CancellationToken
to an HttpClient
- of course this will abort the request and the other side can react the same way (if it is also a .NET backend with CancellationToken
the same holds true).
Not sure if all "serverless" technologies support that. For example SQLite: It has a sqlite3_interrupt
call that the C# layer could use (like Entity Framework or similar), but without looking into the code I am not sure how to hook in and check if that thing is running. So technically it would be possible, but experience may differ 😄. In any case, it is good practice to add the token because providers might add "better" support in the future.
Should I always provide a CancellationToken
?
When you see the words "always" or "never" in a question - the answer is most likely: "No". Also here: Sure the rule of thumb is to add a CancellationToken
to calls that support it, but let's have a look at the following code:
var user = new User { Name = dto.Name };
db.Users.Add(user);
await _db.SaveChangesAsync(cancellationToken);
var address = new Address { UserId = user.Id, City = dto.City };
db.Addresses.Add(address);
await _db.SaveChangesAsync(cancellationToken);
var role = new Role { UserId = user.Id, Name = "Member" };
db.Roles.Add(role);
await _db.SaveChangesAsync(cancellationToken);
Imagine now the user cancels the query while addresses are saved. The user is added, but addresses and roles are not. Now you are in an inconsistent state. Now, of course, the example is "shit" on purpose. The simple fix is to call SaveChangesAsync
only once in the end - or alternatively - use a transaction which gets commited in the end once. But: Sometimes you have 3rd party code that also stores information somewhere, where you have no control over. So it is up to you to make sure, that you don't run into invalidate states because someone aborts the request!