| 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.
using System;
using System.Net.Http;
using System.Threading.Tasks;
public class AsyncBasics
{
public async Task<string> FetchDataAsync(string url)
{
using var client = new HttpClient();
string content = await client.GetStringAsync(url);
return content;
}
public async Task ProcessDataAsync()
{
await Task.Delay(1000);
Console.WriteLine("Processing complete");
}
public async Task<int> CalculateSumAsync()
{
int value1 = await GetValueAsync(1);
int value2 = await GetValueAsync(2);
int value3 = await GetValueAsync(3);
return value1 + value2 + value3;
}
private async Task<int> GetValueAsync(int id)
{
await Task.Delay(100);
return id * 10;
}
public async Task<string> 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;
}
}
public async Task DemoAsync()
{
string data = await FetchDataAsync("https://api.example.com");
_ = ProcessDataAsync();
await ProcessDataAsync();
}
}
Task and Task<T>
Task represents an asynchronous operation and provides methods for
composition, continuation, and error handling.
using System;
using System.Threading;
using System.Threading.Tasks;
public class TaskExamples
{
public void CreateTasks()
{
Task<int> task1 = Task.Run(() =>
{
Thread.Sleep(1000);
return 42;
});
Task<int> task2 = Task.FromResult(100);
Task task3 = Task.CompletedTask;
var tcs = new TaskCompletionSource<string>();
Task<string> task4 = tcs.Task;
tcs.SetResult("Done");
}
public async Task<string> ComposeTasks()
{
int result1 = await Task1Async();
int result2 = await Task2Async(result1);
Task<int> t1 = Task1Async();
Task<int> t2 = Task2Async(10);
await Task.WhenAll(t1, t2);
return $"Results: {t1.Result}, {t2.Result}";
}
public async Task<int[]> WhenAllExample()
{
var tasks = new[]
{
Task.Run(() => ComputeValue(1)),
Task.Run(() => ComputeValue(2)),
Task.Run(() => ComputeValue(3))
};
int[] results = await Task.WhenAll(tasks);
return results;
}
public async Task<int> WhenAnyExample()
{
var task1 = DelayedValue(1000, 1);
var task2 = DelayedValue(2000, 2);
var task3 = DelayedValue(500, 3);
Task<int> completed = await Task.WhenAny(task1, task2, task3);
return await completed;
}
public async Task<string> CancellableOperation(
CancellationToken cancellationToken)
{
for (int i = 0; i < 10; i++)
{
cancellationToken.ThrowIfCancellationRequested();
await Task.Delay(100, cancellationToken);
Console.WriteLine($"Step {i + 1}");
}
return "Completed";
}
private Task<int> Task1Async() => Task.FromResult(10);
private Task<int> Task2Async(int value) =>
Task.FromResult(value * 2);
private int ComputeValue(int x) => x * x;
private async Task<int> DelayedValue(int delay, int value)
{
await Task.Delay(delay);
return value;
}
}
ValueTask and ValueTask<T>
ValueTask provides better performance for operations that often complete
synchronously, avoiding heap allocations.
using System;
using System.Threading.Tasks;
public class ValueTaskExamples
{
private readonly Dictionary<string, string> _cache =
new Dictionary<string, string>();
public ValueTask<string> GetValueAsync(string key)
{
if (_cache.TryGetValue(key, out string? value))
{
return new ValueTask<string>(value);
}
return new ValueTask<string>(FetchFromDatabaseAsync(key));
}
private async Task<string> FetchFromDatabaseAsync(string key)
{
await Task.Delay(100);
string value = $"Value for {key}";
_cache[key] = value;
return value;
}
public async ValueTask<int> ConversionExample()
{
Task<int> task = GetTaskAsync();
ValueTask<int> valueTask = new ValueTask<int>(task);
return await valueTask;
}
private Task<int> GetTaskAsync() => Task.FromResult(42);
public async Task ValueTaskUsageAsync()
{
string value1 = await GetValueAsync("key1");
Task<string> task = GetValueAsync("key2").AsTask();
await task;
await task;
}
public async ValueTask ConfigureAwaitExample()
{
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.
using System;
using System.Threading.Tasks;
public class ConfigureAwaitExamples
{
public async Task<string> LibraryMethodAsync()
{
await Task.Delay(100).ConfigureAwait(false);
string result = await GetDataAsync()
.ConfigureAwait(false);
return result.ToUpper();
}
public async Task UpdateUIAsync()
{
string data = await LoadDataAsync();
Console.WriteLine($"Data: {data}");
}
public class DeadlockExample
{
public string BadSync()
{
return GetDataAsync().Result;
}
public string GoodSync()
{
return GetDataAsync()
.ConfigureAwait(false)
.GetAwaiter()
.GetResult();
}
public async Task<string> BestAsync()
{
return await GetDataAsync();
}
}
public async Task MixedContextAsync()
{
await Task.Delay(100);
Console.WriteLine("On original context");
await Task.Delay(100).ConfigureAwait(false);
Console.WriteLine("On thread pool");
await Task.Delay(100);
Console.WriteLine("Still on thread pool");
}
private async Task<string> GetDataAsync()
{
await Task.Delay(100);
return "data";
}
private async Task<string> 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.
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
public class AsyncStreamExamples
{
public async IAsyncEnumerable<int> GenerateNumbersAsync(
int count)
{
for (int i = 0; i < count; i++)
{
await Task.Delay(100);
yield return i;
}
}
public async Task ConsumeStreamAsync()
{
await foreach (int number in GenerateNumbersAsync(10))
{
Console.WriteLine(number);
}
}
public async IAsyncEnumerable<string> 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;
}
}
}
public async IAsyncEnumerable<int> FilterEvenNumbersAsync(
IAsyncEnumerable<int> source)
{
await foreach (int number in source)
{
if (number % 2 == 0)
{
yield return number;
}
}
}
public async IAsyncEnumerable<string> FormatNumbersAsync(
IAsyncEnumerable<int> source)
{
await foreach (int number in source)
{
yield return $"Number: {number:D3}";
}
}
public async IAsyncEnumerable<string> 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;
}
}
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.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
public class ParallelAsyncExamples
{
public async Task<List<string>> ProcessInParallelAsync(
List<int> items)
{
var tasks = items.Select(async item =>
{
await Task.Delay(100);
return $"Processed {item}";
});
string[] results = await Task.WhenAll(tasks);
return results.ToList();
}
public async Task<List<string>> ThrottledParallelAsync(
List<int> 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();
}
public async Task ParallelForEachAsyncExample(List<int> items)
{
await Parallel.ForEachAsync(
items,
new ParallelOptions { MaxDegreeOfParallelism = 4 },
async (item, cancellationToken) =>
{
await Task.Delay(100, cancellationToken);
Console.WriteLine($"Processed {item}");
});
}
public async Task<List<string>> BatchProcessAsync(
List<int> items,
int batchSize)
{
var results = new List<string>();
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.
using System;
using System.Threading.Tasks;
public class AsyncErrorHandling
{
public async Task<string> 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;
}
}
public async Task HandleMultipleErrorsAsync()
{
try
{
await Task.WhenAll(
FailingTaskAsync("Task 1"),
FailingTaskAsync("Task 2"),
FailingTaskAsync("Task 3")
);
}
catch (Exception ex)
{
Console.WriteLine($"First error: {ex.Message}");
}
}
public async Task HandleAllErrorsAsync()
{
var tasks = new[]
{
FailingTaskAsync("Task 1"),
FailingTaskAsync("Task 2"),
FailingTaskAsync("Task 3")
};
try
{
await Task.WhenAll(tasks);
}
catch
{
foreach (var task in tasks)
{
if (task.IsFaulted && task.Exception != null)
{
foreach (var ex in task.Exception.InnerExceptions)
{
Console.WriteLine($"Error: {ex.Message}");
}
}
}
}
}
public async Task FinallyBlockAsync()
{
try
{
await RiskyOperationAsync();
}
catch (Exception ex)
{
Console.WriteLine($"Error: {ex.Message}");
}
finally
{
Console.WriteLine("Cleanup code");
}
}
private async Task<string> 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
- Use
async/await all the way - avoid mixing async and sync code
- Prefer
Task.Run for CPU-bound work, native async APIs for I/O
- Use
ValueTask<T> for hot paths that often complete synchronously
- Always use
ConfigureAwait(false) in library code
- Never use
.Result or .Wait() on Tasks - causes deadlocks
- Properly handle cancellation with
CancellationToken
- Use
Task.WhenAll for parallel operations, not sequential awaits
- Implement proper exception handling for async operations
- Use async streams for sequences that are produced asynchronously
- Avoid
async void except for event handlers
Common Pitfalls
- Blocking on async code with
.Result or .Wait() causing deadlocks
- Not using
ConfigureAwait(false) in library code capturing context
unnecessarily
- Using
async void methods which can't be properly awaited or caught
- Forgetting to await tasks, causing fire-and-forget behavior
- Not handling exceptions in parallel tasks properly
- Over-parallelizing with too many concurrent operations
- Using
Task.Run for already-async I/O operations (double wrapping)
- Not passing
CancellationToken through async call chains
- Storing and awaiting
ValueTask multiple times
- 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