| 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}, ";
}
Task<[]> WhenAllExample()
{
tasks = []
{
Task.Run(() => ComputeValue()),
Task.Run(() => ComputeValue()),
Task.Run(() => ComputeValue())
};
[] results = Task.WhenAll(tasks);
results;
}
{
task1 = DelayedValue(, );
task2 = DelayedValue(, );
task3 = DelayedValue(, );
Task<> completed = Task.WhenAny(task1, task2, task3);
completed;
}
{
( i = ; i < ; i++)
{
cancellationToken.ThrowIfCancellationRequested();
Task.Delay(, cancellationToken);
Console.WriteLine();
}
;
}
=> Task.FromResult();
=>
Task.FromResult( * );
=> x * x;
{
Task.Delay(delay);
;
}
}
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;
}
{
Task<> task = GetTaskAsync();
ValueTask<> valueTask = ValueTask<>(task);
valueTask;
}
=> Task.FromResult();
{
value1 = GetValueAsync();
Task<> task = GetValueAsync().AsTask();
task;
task;
}
{
= GetValueAsync()
.ConfigureAwait();
Console.WriteLine();
}
}
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;
}
()
{
GetDataAsync()
.ConfigureAwait()
.GetAwaiter()
.GetResult();
}
{
GetDataAsync();
}
}
{
Task.Delay();
Console.WriteLine();
Task.Delay().ConfigureAwait();
Console.WriteLine();
Task.Delay();
Console.WriteLine();
}
{
Task.Delay();
;
}
{
Task.Delay();
;
}
}
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);
(!reader.EndOfStream)
{
cancellationToken.ThrowIfCancellationRequested();
? line = reader.ReadLineAsync();
(line != )
{
line;
}
}
}
{
( number source)
{
(number % == )
{
number;
}
}
}
{
( number source)
{
;
}
}
{
client = HttpClient();
( page = ; page <= totalPages; page++)
{
url = ;
content = client.GetStringAsync(url);
content;
}
}
{
numbers = GenerateNumbersAsync();
evens = FilterEvenNumbersAsync(numbers);
formatted = FormatNumbersAsync(evens);
( formatted)
{
Console.WriteLine();
}
}
}
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();
}
{
Parallel.ForEachAsync(
items,
ParallelOptions { MaxDegreeOfParallelism = },
(item, cancellationToken) =>
{
Task.Delay(, cancellationToken);
Console.WriteLine();
});
}
Task<List<>> BatchProcessAsync(
List<> items,
batchSize)
{
results = List<>();
( i = ; i < items.Count; i += batchSize)
{
batch = items.Skip(i).Take(batchSize);
batchResults = ProcessInParallelAsync(
batch.ToList());
results.AddRange(batchResults);
}
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(),
FailingTaskAsync(),
FailingTaskAsync()
};
{
Task.WhenAll(tasks);
}
{
( task tasks)
{
(task.IsFaulted && task.Exception != )
{
( ex task.Exception.InnerExceptions)
{
Console.WriteLine();
}
}
}
}
}
{
{
RiskyOperationAsync();
}
(Exception ex)
{
Console.WriteLine();
}
{
Console.WriteLine();
}
}
{
Task.Delay();
InvalidOperationException();
}
{
Task.Delay();
InvalidOperationException();
}
}
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