--- name: csharp-async-patterns user-invocable: false description: Use when C# asynchronous programming with async/await, Task, ValueTask, ConfigureAwait, and async streams for responsive applications. allowed-tools: - Read - Write - Edit - Grep - Glob - Bash --- # C# Async Patterns Asynchronous programming in C# enables writing responsive applications that efficiently handle I/O-bound and CPU-bound operations without blocking threads. The async/await pattern provides a straightforward way to write asynchronous code that looks and behaves like synchronous code. ## Async/Await Basics The `async` and `await` keywords transform synchronous-looking code into state machines that handle asynchronous operations. ```csharp using System; using System.Net.Http; using System.Threading.Tasks; public class AsyncBasics { // Basic async method public async Task FetchDataAsync(string url) { using var client = new HttpClient(); // await suspends execution until response received string content = await client.GetStringAsync(url); return content; } // Async method without return value public async Task ProcessDataAsync() { await Task.Delay(1000); // Simulate async work Console.WriteLine("Processing complete"); } // Multiple awaits public async Task CalculateSumAsync() { int value1 = await GetValueAsync(1); int value2 = await GetValueAsync(2); int value3 = await GetValueAsync(3); return value1 + value2 + value3; } private async Task GetValueAsync(int id) { await Task.Delay(100); return id * 10; } // Async with exception handling public async Task SafeFetchAsync(string url) { try { using var client = new HttpClient(); return await client.GetStringAsync(url); } catch (HttpRequestException ex) { Console.WriteLine($"Request failed: {ex.Message}"); return string.Empty; } } // Calling async methods public async Task DemoAsync() { // Await the result string data = await FetchDataAsync("https://api.example.com"); // Fire and forget (not recommended) _ = ProcessDataAsync(); // Wait for completion await ProcessDataAsync(); } } ``` ## Task and Task\ `Task` represents an asynchronous operation and provides methods for composition, continuation, and error handling. ```csharp using System; using System.Threading; using System.Threading.Tasks; public class TaskExamples { // Creating tasks public void CreateTasks() { // Task.Run for CPU-bound work Task task1 = Task.Run(() => { Thread.Sleep(1000); return 42; }); // Task.FromResult for already-known values Task task2 = Task.FromResult(100); // Task.CompletedTask for void operations Task task3 = Task.CompletedTask; // TaskCompletionSource for manual control var tcs = new TaskCompletionSource(); Task task4 = tcs.Task; tcs.SetResult("Done"); } // Task composition public async Task ComposeTasks() { // Sequential execution int result1 = await Task1Async(); int result2 = await Task2Async(result1); // Parallel execution Task t1 = Task1Async(); Task t2 = Task2Async(10); await Task.WhenAll(t1, t2); return $"Results: {t1.Result}, {t2.Result}"; } // Task.WhenAll - wait for all tasks public async Task WhenAllExample() { var tasks = new[] { Task.Run(() => ComputeValue(1)), Task.Run(() => ComputeValue(2)), Task.Run(() => ComputeValue(3)) }; int[] results = await Task.WhenAll(tasks); return results; } // Task.WhenAny - wait for first task public async Task WhenAnyExample() { var task1 = DelayedValue(1000, 1); var task2 = DelayedValue(2000, 2); var task3 = DelayedValue(500, 3); Task completed = await Task.WhenAny(task1, task2, task3); return await completed; } // Cancellation support public async Task CancellableOperation( CancellationToken cancellationToken) { for (int i = 0; i < 10; i++) { cancellationToken.ThrowIfCancellationRequested(); await Task.Delay(100, cancellationToken); Console.WriteLine($"Step {i + 1}"); } return "Completed"; } // Helper methods private Task Task1Async() => Task.FromResult(10); private Task Task2Async(int value) => Task.FromResult(value * 2); private int ComputeValue(int x) => x * x; private async Task DelayedValue(int delay, int value) { await Task.Delay(delay); return value; } } ``` ## ValueTask and ValueTask\ `ValueTask` provides better performance for operations that often complete synchronously, avoiding heap allocations. ```csharp using System; using System.Threading.Tasks; public class ValueTaskExamples { private readonly Dictionary _cache = new Dictionary(); // ValueTask for cached operations public ValueTask GetValueAsync(string key) { // Synchronous path - no allocation if (_cache.TryGetValue(key, out string? value)) { return new ValueTask(value); } // Asynchronous path return new ValueTask(FetchFromDatabaseAsync(key)); } private async Task FetchFromDatabaseAsync(string key) { await Task.Delay(100); // Simulate database call string value = $"Value for {key}"; _cache[key] = value; return value; } // Converting between Task and ValueTask public async ValueTask ConversionExample() { // ValueTask from Task Task task = GetTaskAsync(); ValueTask valueTask = new ValueTask(task); return await valueTask; } private Task GetTaskAsync() => Task.FromResult(42); // ValueTask best practices public async Task ValueTaskUsageAsync() { // Good: await immediately string value1 = await GetValueAsync("key1"); // Bad: storing ValueTask // ValueTask vt = GetValueAsync("key2"); // await vt; // First await // await vt; // Second await - WRONG! // Good: convert to Task if needed multiple times Task task = GetValueAsync("key2").AsTask(); await task; await task; // OK with Task } // ConfigureAwait with ValueTask public async ValueTask ConfigureAwaitExample() { // Don't capture context (for library code) string value = await GetValueAsync("key") .ConfigureAwait(false); Console.WriteLine(value); } } ``` ## ConfigureAwait `ConfigureAwait` controls whether to capture the synchronization context, critical for library code and avoiding deadlocks. ```csharp using System; using System.Threading.Tasks; public class ConfigureAwaitExamples { // Library method - use ConfigureAwait(false) public async Task LibraryMethodAsync() { // Don't capture synchronization context await Task.Delay(100).ConfigureAwait(false); // This continues on thread pool thread string result = await GetDataAsync() .ConfigureAwait(false); return result.ToUpper(); } // UI method - use default (or ConfigureAwait(true)) public async Task UpdateUIAsync() { string data = await LoadDataAsync(); // This continues on UI thread // Can safely update UI controls Console.WriteLine($"Data: {data}"); } // Avoiding deadlocks public class DeadlockExample { // This can deadlock in synchronous context public string BadSync() { // DON'T DO THIS return GetDataAsync().Result; // Deadlock! } // Fix with ConfigureAwait(false) public string GoodSync() { return GetDataAsync() .ConfigureAwait(false) .GetAwaiter() .GetResult(); } // Better: make it async public async Task BestAsync() { return await GetDataAsync(); } } // Mixing contexts public async Task MixedContextAsync() { // Runs on captured context await Task.Delay(100); Console.WriteLine("On original context"); // Runs on thread pool await Task.Delay(100).ConfigureAwait(false); Console.WriteLine("On thread pool"); // Still on thread pool (ConfigureAwait effect persists) await Task.Delay(100); Console.WriteLine("Still on thread pool"); } private async Task GetDataAsync() { await Task.Delay(100); return "data"; } private async Task LoadDataAsync() { await Task.Delay(100); return "loaded data"; } } ``` ## Async Streams (IAsyncEnumerable) Async streams enable asynchronous iteration over sequences of data, perfect for streaming APIs and large datasets. ```csharp using System; using System.Collections.Generic; using System.Net.Http; using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; public class AsyncStreamExamples { // Basic async stream public async IAsyncEnumerable GenerateNumbersAsync( int count) { for (int i = 0; i < count; i++) { await Task.Delay(100); yield return i; } } // Consuming async stream public async Task ConsumeStreamAsync() { await foreach (int number in GenerateNumbersAsync(10)) { Console.WriteLine(number); } } // Async stream with cancellation public async IAsyncEnumerable ReadLinesAsync( string filePath, [EnumeratorCancellation] CancellationToken cancellationToken = default) { using var reader = new System.IO.StreamReader(filePath); while (!reader.EndOfStream) { cancellationToken.ThrowIfCancellationRequested(); string? line = await reader.ReadLineAsync(); if (line != null) { yield return line; } } } // Filtering async stream public async IAsyncEnumerable FilterEvenNumbersAsync( IAsyncEnumerable source) { await foreach (int number in source) { if (number % 2 == 0) { yield return number; } } } // Transforming async stream public async IAsyncEnumerable FormatNumbersAsync( IAsyncEnumerable source) { await foreach (int number in source) { yield return $"Number: {number:D3}"; } } // Async stream from API public async IAsyncEnumerable FetchPagesAsync( string baseUrl, int totalPages) { using var client = new HttpClient(); for (int page = 1; page <= totalPages; page++) { string url = $"{baseUrl}?page={page}"; string content = await client.GetStringAsync(url); yield return content; } } // Composing async streams public async Task ComposeStreamsAsync() { var numbers = GenerateNumbersAsync(20); var evens = FilterEvenNumbersAsync(numbers); var formatted = FormatNumbersAsync(evens); await foreach (string value in formatted) { Console.WriteLine(value); } } } ``` ## Parallel Async Operations Combining parallelism with async operations for maximum throughput. ```csharp using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; public class ParallelAsyncExamples { // Process items in parallel public async Task> ProcessInParallelAsync( List items) { var tasks = items.Select(async item => { await Task.Delay(100); return $"Processed {item}"; }); string[] results = await Task.WhenAll(tasks); return results.ToList(); } // Throttled parallel execution public async Task> ThrottledParallelAsync( List items, int maxConcurrency) { var semaphore = new SemaphoreSlim(maxConcurrency); var tasks = items.Select(async item => { await semaphore.WaitAsync(); try { await Task.Delay(100); return $"Processed {item}"; } finally { semaphore.Release(); } }); string[] results = await Task.WhenAll(tasks); return results.ToList(); } // Parallel.ForEachAsync (.NET 6+) public async Task ParallelForEachAsyncExample(List items) { await Parallel.ForEachAsync( items, new ParallelOptions { MaxDegreeOfParallelism = 4 }, async (item, cancellationToken) => { await Task.Delay(100, cancellationToken); Console.WriteLine($"Processed {item}"); }); } // Batched processing public async Task> BatchProcessAsync( List items, int batchSize) { var results = new List(); for (int i = 0; i < items.Count; i += batchSize) { var batch = items.Skip(i).Take(batchSize); var batchResults = await ProcessInParallelAsync( batch.ToList()); results.AddRange(batchResults); } return results; } } ``` ## Error Handling in Async Code Proper error handling is crucial for robust asynchronous applications. ```csharp using System; using System.Threading.Tasks; public class AsyncErrorHandling { // Basic try-catch public async Task BasicErrorHandlingAsync() { try { return await RiskyOperationAsync(); } catch (InvalidOperationException ex) { Console.WriteLine($"Operation error: {ex.Message}"); return "default"; } catch (Exception ex) { Console.WriteLine($"Unexpected error: {ex.Message}"); throw; } } // AggregateException from WhenAll public async Task HandleMultipleErrorsAsync() { try { await Task.WhenAll( FailingTaskAsync("Task 1"), FailingTaskAsync("Task 2"), FailingTaskAsync("Task 3") ); } catch (Exception ex) { // Only first exception caught Console.WriteLine($"First error: {ex.Message}"); } } // Handling all exceptions public async Task HandleAllErrorsAsync() { var tasks = new[] { FailingTaskAsync("Task 1"), FailingTaskAsync("Task 2"), FailingTaskAsync("Task 3") }; try { await Task.WhenAll(tasks); } catch { // Iterate through all tasks to see all exceptions foreach (var task in tasks) { if (task.IsFaulted && task.Exception != null) { foreach (var ex in task.Exception.InnerExceptions) { Console.WriteLine($"Error: {ex.Message}"); } } } } } // finally blocks work as expected public async Task FinallyBlockAsync() { try { await RiskyOperationAsync(); } catch (Exception ex) { Console.WriteLine($"Error: {ex.Message}"); } finally { // Always executes Console.WriteLine("Cleanup code"); } } private async Task RiskyOperationAsync() { await Task.Delay(100); throw new InvalidOperationException("Something went wrong"); } private async Task FailingTaskAsync(string name) { await Task.Delay(100); throw new InvalidOperationException($"{name} failed"); } } ``` ## Best Practices 1. Use `async/await` all the way - avoid mixing async and sync code 2. Prefer `Task.Run` for CPU-bound work, native async APIs for I/O 3. Use `ValueTask` for hot paths that often complete synchronously 4. Always use `ConfigureAwait(false)` in library code 5. Never use `.Result` or `.Wait()` on Tasks - causes deadlocks 6. Properly handle cancellation with `CancellationToken` 7. Use `Task.WhenAll` for parallel operations, not sequential awaits 8. Implement proper exception handling for async operations 9. Use async streams for sequences that are produced asynchronously 10. Avoid `async void` except for event handlers ## Common Pitfalls 1. Blocking on async code with `.Result` or `.Wait()` causing deadlocks 2. Not using `ConfigureAwait(false)` in library code capturing context unnecessarily 3. Using `async void` methods which can't be properly awaited or caught 4. Forgetting to await tasks, causing fire-and-forget behavior 5. Not handling exceptions in parallel tasks properly 6. Over-parallelizing with too many concurrent operations 7. Using `Task.Run` for already-async I/O operations (double wrapping) 8. Not passing `CancellationToken` through async call chains 9. Storing and awaiting `ValueTask` multiple times 10. Capturing large objects in async lambda closures causing memory issues ## When to Use Async Patterns Use async patterns when you need: - Responsive UI applications that don't freeze during I/O operations - Web APIs and services handling many concurrent requests efficiently - Database operations that shouldn't block threads - File I/O operations for reading and writing large files - Network operations including HTTP requests and socket communication - Streaming large datasets without loading everything into memory - CPU-bound work offloaded to thread pool with `Task.Run` - Composable asynchronous operations with proper error handling - Cancellable long-running operations - Maximum scalability in server applications ## Resources - [Async/Await Best Practices](https://docs.microsoft.com/en-us/archive/msdn-magazine/2013/march/async-await-best-practices-in-asynchronous-programming) - [ValueTask Documentation](https://docs.microsoft.com/en-us/dotnet/api/system.threading.tasks.valuetask-1) - [Asynchronous Programming Patterns](https://docs.microsoft.com/en-us/dotnet/standard/asynchronous-programming-patterns/) - [Async Streams Tutorial](https://docs.microsoft.com/en-us/dotnet/csharp/tutorials/generate-consume-asynchronous-stream)