Skip to content

Using Cancellation Tokens

Cancellation tokens in C# play a crucial role in the design and implementation of responsive applications. They are commonly employed in multi-threaded and asynchronous programming. The purpose of cancellation tokens is to manage the execution of tasks or threads, offering an efficient way to request cancellation of a running task or thread.

Importance of Cancellation Tokens

Responsiveness: As an application grows in complexity, it may need to run lengthy operations. To maintain responsiveness and ensure a good user experience, these operations are typically run on a separate thread. However, there may be instances where it would be beneficial to cancel those operations midway. This is where cancellation tokens come into play.

Resource Management: Long running tasks can consume significant resources. If a task is no longer needed, it would be advantageous to cancel and stop it to free up resources.

Control Over Tasks or Threads: Cancellation tokens provide a standardized way to communicate that an operation should be canceled.

Conceptual Overview

In C#, the CancellationToken struct conveys the cancellation request. It contains a method IsCancellationRequested which can be checked periodically by the executing code to see if cancellation was requested. Often tasks or threads are passed a CancellationToken which they assess at suitable intervals. This allows them to cease execution in a controlled and timely fashion when requested. The CancellationTokenSource, on the other hand, controls the token and can issue the cancellation.

csharp
// Creating the CancellationTokenSource
CancellationTokenSource cancellationTokenSource = new CancellationTokenSource();

// Acquiring the token
CancellationToken token = cancellationTokenSource.Token;

// Passing the token to a task
Task.Run(() => DoWork(token), token);

// At some later point, signalling cancellation
cancellationTokenSource.Cancel();

An important aspect to note is that invoking cancellation does not forcibly terminate the operation, but instead, an operation may choose to respond to the cancellation request.

Just remember, cancellation in C# is cooperative. This means the code which is being executed must periodically check if cancellation has been requested, and if so, stop executing.

The WebApi Context

In the context of a web API request, the CancellationToken is automatically provided by ASP.NET Core as part of the controller's HttpContext. So you can include it directly as an argument in your method signature, and the runtime will provide the token for you. Consider the example below:

csharp
/// <summary>
    /// Retrieves all bin data based on the provided filter.
    /// </summary>
    /// <param name="filter">The filter object containing criteria for retrieving bin data.</param>
    /// <returns>An async task that represents the operation. The task result contains the bin data paged result.</returns>
    [HttpGet("")]
    public async Task<IActionResult> All([FromQuery] BinDataFilter filter)
    {
        var pagedResult = await _binDataProxyClient.Find(filter, Request.HttpContext.RequestAborted);
        return Ok(pagedResult);
    }

In the example above, HttpContext.RequestAborted is used to fetch a CancellationToken that is linked with the current request. If the client disconnects the request, this token will be marked as cancelled. Again, don't forget to make your code honor the CancellationToken by checking cancellationToken.IsCancellationRequested and stopping the execution where applicable.

Under the Hood

Under the hood, the cancellation token will get passed to each of the underlying services to the lowest layers; the database, external http calls etc. Here is an example showing the a method making making external http requests using the same cancellation token passed in from the controller :

csharp
 public async Task<ApiResponse<PagedResult<BinDataJson>>> Find(BinDataFilter filter, CancellationToken cancellationToken)
    {
        _logger.LogTrace("Executed method Find with parameters {@filter}", filter);
    
        try
        {
            var binCodes = filter.BinCode.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
            var request = BinDataRedis.BaseUrl.AppendPathSegment("backofficebindata")
                .AllowHttpStatus((int)HttpStatusCode.BadRequest, (int)HttpStatusCode.NotFound);
            var response = await request.GetAsync(cancellationToken: cancellationToken);

            if (!response.ResponseMessage.IsSuccessStatusCode)
            {
                _logger.LogError("Request was unsuccessful. Response status {statusCode}", response.ResponseMessage.StatusCode);
                return null;
            }

            _logger.LogInformation("Request was successful with response status {statusCode}", response.ResponseMessage.StatusCode);

            var data = await response.ResponseMessage.Content.ReadAsStringAsync(cancellationToken);
            return JsonConvert.DeserializeObject<ApiResponse<PagedResult<BinDataJson>>>(data);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "An error occurred while trying to execute the Find method.");
            return null;
        }
    }

And another here making a database request using the cancellation token :

csharp
    /// <summary>
    /// Gets all authentication channels for a specific ID asynchronously.
    /// </summary>
    /// <param name="id">The ID for which to retrieve authentication channels.</param>
    /// <param name="cancellationToken">The cancellation token.</param>
    /// <returns>A task representing the asynchronous operation. The task result contains the list of authentication channels.</returns>
    public async Task<ApiResponse<List<AuthenticationChannel>>> GetAllAuthenticationChannelsAsync(Guid id, CancellationToken cancellationToken)
    {
          var authenticationChannels = await _dbContext.AuthenticationConfigurations
            .Where(x => x.Id == id)
            .SelectMany(x => x.AuthenticationChannels)
            .ToListAsync(cancellationToken);

        var data = authenticationChannels.Select(x => x.AuthenticationChannel).ToList();
        return
            CommonResponses.SuccessResponse.OkResponse(data);
    }

And another here in a background task executing a long running task.

csharp
 public class MessageWorker : BackgroundService
    {
        /// <summary>
        /// Represents the service provider used for dependency injection.
        /// </summary>
        private readonly IServiceProvider _serviceProvider;
 
   
        /// <summary>
        /// Background service responsible for persisting messages.
        /// </summary>
        public MessageWorker(IServiceProvider serviceProvider)
        {
            _serviceProvider = serviceProvider;
        }

        protected override async Task ExecuteAsync(CancellationToken stoppingToken)
        {
            _logger.LogInformation("started processing messages for {environment}", _environment.EnvironmentName);

            while (!stoppingToken.IsCancellationRequested)
            {
                try
                {
                    using var scope = _serviceProvider.CreateScope();
                    var dbContext = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
                    var messageService=scope.ServiceProvider.GetRequiredService<IMessageService>();
                    var message = messageService.GetLatestMessage();
                    await dbContext.Messages.AddAsync(message, stoppingToken);
                    await dbContext.SaveChangesAsync(stoppingToken);

                    }
                }
                catch (Exception e)
                {
                    _logger.LogError(e, e.Message);
                    continue;
                }

                await Task.Delay(TimeSpan.FromSeconds(1), stoppingToken);
            }
        }

         
    }

All these methods involve I/O-bound operations, such as retrieving data from an http service, database or a memory cache. These types of operations can take a variable amount of time depending on the state of the system resources. To make these operations more efficient and prevent blocking of resources, the method uses the async and await keywords. The use of async and await leads to the introduction of the CancellationToken parameter (cancellationToken). This token allows the operation to be cancelled before it finishes. The usefulness of this becomes apparent when you consider that the operation may take a significant amount of time. Scenario for cancelling could be a user navigating away from an application or other user interactions that make the results of the operation irrelevant. The CancellationToken allows these operations to be cancelled effectively, freeing up resources that may be held by them. In the described method, cancellationToken can be used to cancel the DB operation which is fetching AuthenticationConfigurations.

Finally

Cancellation tokens in C# are a critical part of writing reliable and efficient applications, particularly ones that use asynchronous operations or tasks. They provide a cooperative mechanism for cancelling long-running operations that may no longer be necessary or requested by the user. This can result in the timely release of system resources, improved application responsiveness, and a better user experience overall. When used properly in conjunction with async and await, CancellationToken can make your application more robust and responsive, especially under heavy load or when dealing with operations that can take a long time to complete. It's important to note that cancellation isn't automatic — methods need to periodically check the cancellation token to see if cancellation has been requested and then act appropriately. Despite the need for developers to manually check the token, the cancellation framework integrates well with many of the .NET libraries and provides a standardized approach to handling cancellations.