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);
});
```