Skip to main content

A few practical examples demonstrating async/await patterns with cancellation tokens in .NET. Including parallel operations, timeout handling, and cancellation propagation. It also demonstrates how/when to create linked token sources.

# Async/Await with Cancellation Tokens in .NET

A few practical examples demonstrating async/await patterns with cancellation tokens in .NET. Including parallel operations, timeout handling, and cancellation propagation. It also demonstrates how/when to create linked token sources.

## Long-Running Operations with Cancellation Tokens

This `DataService` class contains methods that simulate long-running operations, such as fetching data with a delay or from an external API.

```csharp
// DataService.cs
// A service class for simulated long-running operations.
public class DataService
{
    private readonly HttpClient _httpClient;

    public DataService(HttpClient httpClient)
    {
        _httpClient = httpClient;
    }

    public async Task<IEnumerable<string>> FetchDataWithTimeout(int delayMs, CancellationToken cancellationToken)
    {
        using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);

        linkedCts.CancelAfter(TimeSpan.FromSeconds(5)); // 5 second timeout

        try
        {
            await Task.Delay(delayMs, linkedCts.Token);

            return new[] { "Data1", "Data2" };
        }
        catch (OperationCanceledException) when (linkedCts.IsCancellationRequested && !cancellationToken.IsCancellationRequested)
        {
            throw new TimeoutException("Operation timed out");
        }
    }

    public async Task<string> FetchExternalApiData(string url, CancellationToken cancellationToken)
    {
        return await _httpClient.GetStringAsync(url, cancellationToken);
    }
}
```

## Async Endpoints with Cancellation Tokens

The `AsyncOperationsController` class contains endpoints that demonstrate parallel operations, sequential operations, and racing to get the first result. Each endpoint accepts a cancellation token to handle timeouts and cancellations.

```csharp
// AsyncOperationsController.cs
// A controller with various async endpoints.
[ApiController]
[Route("[controller]")]
public class AsyncOperationsController : ControllerBase
{
    private readonly DataService _dataService;

    public AsyncOperationsController(DataService dataService)
    {
        _dataService = dataService;
    }

    // Parallel execution with Task.WhenAll
    [HttpGet("parallel")]
    public async Task<IActionResult> GetParallelData(CancellationToken cancellationToken)
    {
        try
        {
            var tasks = new List<Task<IEnumerable<string>>>
            {
                _dataService.FetchDataWithTimeout(1000, cancellationToken),
                _dataService.FetchDataWithTimeout(2000, cancellationToken),
                _dataService.FetchDataWithTimeout(3000, cancellationToken)
            };

            var results = await Task.WhenAll(tasks);

            return Ok(results.SelectMany(r => r));
        }
        catch (TimeoutException ex)
        {
            return StatusCode(408, ex.Message);
        }
        catch (OperationCanceledException)
        {
            return StatusCode(499, "Client closed request");
        }
    }

    // Sequential operations with timeout
    [HttpGet("sequential")]
    public async Task<IActionResult> GetSequentialData(CancellationToken cancellationToken)
    {
        try
        {
            using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);

            cts.CancelAfter(TimeSpan.FromSeconds(10)); // Global timeout

            var result1 = await _dataService.FetchDataWithTimeout(2000, cts.Token);
            var result2 = await _dataService.FetchExternalApiData("https://api.example.com", cts.Token);

            return Ok(new { LocalData = result1, ExternalData = result2 });
        }
        catch (Exception ex) when (ex is TimeoutException or OperationCanceledException)
        {
            return StatusCode(408, "Operation timed out or was cancelled");
        }
    }

    // Race conditions using Task.WhenAny
    [HttpGet("race")]
    public async Task<IActionResult> GetFirstResult(CancellationToken cancellationToken)
    {
        try
        {
            using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);

            cts.CancelAfter(TimeSpan.FromSeconds(8));

            var task1 = _dataService.FetchDataWithTimeout(3000, cts.Token);
            var task2 = _dataService.FetchDataWithTimeout(4000, cts.Token);

            var completedTask = await Task.WhenAny(task1, task2);

            cts.Cancel(); // Cancel other ongoing task

            var result = await completedTask;

            return Ok(result);
        }
        catch (Exception ex) when (ex is TimeoutException or OperationCanceledException)
        {
            return StatusCode(408, "Operation timed out or was cancelled");
        }
    }
}

// Program.cs registration
builder.Services.AddHttpClient<DataService>();
```

## When to use CreateLinkedTokenSource

**Use `CreateLinkedTokenSource` when:**

- Adding timeout to operations that accept cancellation tokens
- Combining multiple cancellation sources
- Need to cancel child operations independently
- Need to propagate cancellation while adding additional cancel conditions

**Don't use `CreateLinkedTokenSource` when:**

- Simply passing through a CancellationToken
- No additional cancellation conditions needed
- Performance is critical (creating linked sources has overhead)

**Example showing both cases:**

```csharp
public class DataService
{
    // DO use linked token - adding timeout
    public async Task<string> FetchWithTimeout(string url, CancellationToken cancellationToken)
    {
        using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);

        linkedCts.CancelAfter(TimeSpan.FromSeconds(5));

        return await httpClient.GetStringAsync(url, linkedCts.Token);
    }

    // DON'T use linked token - just passing through
    public async Task<string> SimpleFetch(string url, CancellationToken cancellationToken)
    {
        return await httpClient.GetStringAsync(url, cancellationToken);
    }

    // DO use linked token - need to cancel child operations
    public async Task<string> ComplexOperation(CancellationToken cancellationToken)
    {
        using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);

        try {
            var task1 = Operation1(linkedCts.Token);
            var task2 = Operation2(linkedCts.Token);

            var firstResult = await Task.WhenAny(task1, task2);
            linkedCts.Cancel(); // Cancel other operation

            return await firstResult;
        }
        catch (OperationCanceledException) {
            throw;
        }
    }
}
```

Only create linked tokens when you need additional cancellation functionality beyond the incoming token. Otherwise, pass the original token directly to the operation.

### CreateLinkedTokenSource Practical Examples

Here are some more practical examples of using `CreateLinkedTokenSource` with async/await.

#### File Operations

```csharp
public class FileProcessor
{
    private readonly string _basePath;
    private readonly ILogger<FileProcessor> _logger;

    public async Task<IEnumerable<string>> ProcessFilesAsync(
        string searchPattern,
        CancellationToken cancellationToken)
    {
        try
        {
            using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
            cts.CancelAfter(TimeSpan.FromMinutes(5));

            var files = Directory.GetFiles(_basePath, searchPattern);
            var tasks = files.Select(f => ProcessFileAsync(f, cts.Token));

            return await Task.WhenAll(tasks);
        }
        catch (OperationCanceledException)
        {
            _logger.LogWarning("File processing cancelled");
            throw;
        }
    }
}
```

#### Database Operations Example

```csharp
public class DatabaseService
{
    private readonly DbContext _context;

    public async Task<Result<List<Order>>> GetOrdersWithRetryAsync(
        DateTime startDate,
        CancellationToken cancellationToken)
    {
        const int maxRetries = 3;
        var delay = TimeSpan.FromSeconds(2);

        for (int i = 0; i < maxRetries; i++)
        {
            try
            {
                using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);

                cts.CancelAfter(TimeSpan.FromSeconds(30));

                var orders = await _context.Orders
                    .Where(o => o.OrderDate >= startDate)
                    .ToListAsync(cts.Token);

                return Result<List<Order>>.Success(orders);
            }
            catch (Exception ex) when (ex is not OperationCanceledException)
            {
                if (i == maxRetries - 1) throw;
                await Task.Delay(delay, cancellationToken);
                delay *= 2;
            }
        }

        return Result<List<Order>>.Failure("Max retries exceeded");
    }
}
```

#### HTTP Client with Timeout

```csharp
public class ExternalApiService
{
    private readonly HttpClient _httpClient;
    private readonly IMemoryCache _cache;

    public async Task<ApiResponse> FetchDataWithCacheAsync(
        string endpoint,
        CancellationToken cancellationToken)
    {
        var cacheKey = $"api_data_{endpoint}";

        if (_cache.TryGetValue(cacheKey, out ApiResponse cached))
            return cached;

        using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);

        cts.CancelAfter(TimeSpan.FromSeconds(15));

        try
        {
            var response = await _httpClient.GetFromJsonAsync<ApiResponse>(
                endpoint,
                cts.Token);

            _cache.Set(cacheKey, response, TimeSpan.FromMinutes(5));

            return response;
        }
        catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested)
        {
            throw new TimeoutException("API request timed out");
        }
    }
}
```

#### Parallel Processing Example

```csharp
public class WorkflowProcessor
{
    private readonly IServiceScopeFactory _scopeFactory;

    public async Task ProcessBatchAsync(
        IEnumerable<WorkItem> items,
        CancellationToken cancellationToken)
    {
        var batches = items.Chunk(100);

        using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);

        cts.CancelAfter(TimeSpan.FromHours(1));

        try
        {
            await Parallel.ForEachAsync(
                batches,
                new ParallelOptions
                {
                    MaxDegreeOfParallelism = Environment.ProcessorCount,
                    CancellationToken = cts.Token
                },
                async (batch, token) =>
                {
                    using var scope = _scopeFactory.CreateScope();
                    var processor = scope.ServiceProvider.GetRequiredService<IBatchProcessor>();
                    await processor.ProcessBatchAsync(batch, token);
                });
        }
        catch (OperationCanceledException) when (cts.IsCancellationRequested)
        {
            throw new TimeoutException("Batch processing exceeded time limit");
        }
    }
}
```

## Example Cancellation Token Usage in Background Services

This example shows a production-ready implementation of periodic cancellation checking in a background service scenario.

```csharp
// BackgroundTaskService.cs
public class BackgroundTaskService : BackgroundService
{
    private readonly ILogger<BackgroundTaskService> _logger;
    private readonly IConfiguration _configuration;
    private readonly IMessageQueue _messageQueue;
    private readonly IMetricsTracker _metrics;

    // Track processing state
    private int _processedCount;
    private readonly TimeSpan _processingInterval;

    public BackgroundTaskService(
        ILogger<BackgroundTaskService> logger,
        IConfiguration configuration,
        IMessageQueue messageQueue,
        IMetricsTracker metrics)
    {
        _logger = logger;
        _configuration = configuration;
        _messageQueue = messageQueue;
        _metrics = metrics;

        // Get interval from config with fallback
        _processingInterval = TimeSpan.FromSeconds(
            configuration.GetValue("ProcessingIntervalSeconds", 30));
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation("Background service starting...");

        try
        {
            // Continue processing until cancellation is requested
            while (!stoppingToken.IsCancellationRequested)
            {
                try
                {
                    // Process batch of messages
                    await ProcessMessageBatchAsync(stoppingToken);

                    // Update metrics
                    _metrics.RecordProcessedCount(_processedCount);

                    // Wait for next interval, can be cancelled
                    await Task.Delay(_processingInterval, stoppingToken);
                }
                catch (OperationCanceledException)
                {
                    // Expected when shutdown is requested
                    throw;
                }
                catch (Exception ex)
                {
                    _logger.LogError(ex, "Error processing message batch");

                    // Wait shorter interval after error
                    await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken);
                }
            }
        }
        finally
        {
            _logger.LogInformation(
                "Service stopped after processing {Count} messages",
                _processedCount);
        }
    }

    private async Task ProcessMessageBatchAsync(CancellationToken cancellationToken)
    {
        // Create linked token with timeout
        using var timeoutCts =
            CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);

        timeoutCts.CancelAfter(TimeSpan.FromMinutes(5));

        try
        {
            // Get messages to process
            var messages = await _messageQueue
                .GetPendingMessagesAsync(timeoutCts.Token);

            foreach (var message in messages)
            {
                // Check cancellation before each message
                cancellationToken.ThrowIfCancellationRequested();

                await ProcessMessageAsync(message, timeoutCts.Token);
                _processedCount++;

                // Optional delay between messages
                await Task.Delay(100, cancellationToken);
            }

            _logger.LogInformation(
                "Processed {Count} messages in batch",
                messages.Count);
        }
        catch (OperationCanceledException) when (timeoutCts.IsCancellationRequested)
        {
            _logger.LogWarning("Batch processing timed out");
            throw;
        }
    }

    private async Task ProcessMessageAsync(
        Message message,
        CancellationToken cancellationToken)
    {
        // Process individual message with timeout
        using var cts = new CancellationTokenSource(
            TimeSpan.FromSeconds(30));

        try
        {
            using var linkedCts =
                CancellationTokenSource.CreateLinkedTokenSource(
                    cancellationToken,
                    cts.Token);

            // Simulate processing
            await Task.Delay(Random.Shared.Next(100, 1000), linkedCts.Token);

            await _messageQueue.MarkAsProcessedAsync(
                message.Id,
                linkedCts.Token);
        }
        catch (OperationCanceledException) when (cts.IsCancellationRequested)
        {
            _logger.LogWarning(
                "Message {MessageId} processing timed out",
                message.Id);

            await _messageQueue.MarkAsFailedAsync(
                message.Id,
                "Processing timed out",
                cancellationToken);
        }
    }
}

// Program.cs setup
builder.Services.AddHostedService<BackgroundTaskService>();
builder.Services.Configure<HostOptions>(opts =>
{
    opts.ShutdownTimeout = TimeSpan.FromSeconds(30);
});
```