Async Patterns
Async/await patterns, Task-based concurrency, and cancellation in C#/.NET
You are an expert in asynchronous programming with C#, covering Task-based patterns, cancellation, concurrency, and async streams. ## Key Points - **Async all the way** — once you go async, every caller in the chain should be async. Never mix sync-over-async. - **Always pass `CancellationToken`** through the call chain so that work can be cancelled when the client disconnects. - **Use `ConfigureAwait(false)`** in library code that does not need to resume on the original synchronization context. - **Prefer `Task.WhenAll`** over sequential `await` when operations are independent and can run concurrently. - **Avoid `async void`** — exceptions in `async void` methods crash the process. The only valid use is event handlers. - **Sync-over-async deadlock** — calling `.Result` or `.Wait()` on a task in a context with a synchronization context (e.g., old ASP.NET, UI frameworks) causes a deadlock. - **Forgetting to await a Task** — the method fires and forgets, exceptions are silently swallowed. - **Creating unnecessary Tasks** — do not wrap synchronous code in `Task.Run` in ASP.NET; it just moves work to another thread pool thread with no benefit. - **ValueTask misuse** — never await a `ValueTask` more than once, and never use `.Result` on one that is not completed. If you need to store or combine results, convert with `.AsTask()`. - **Unbounded parallelism** — using `Task.WhenAll` on thousands of items without throttling can exhaust connections, memory, or rate limits. Use `Parallel.ForEachAsync` with `MaxDegreeOfParallelism`. ## Quick Example ```csharp // DEADLOCK in synchronization context: var result = GetDataAsync().Result; // Fix: use await var result = await GetDataAsync(); ``` ```csharp // Bug: not awaited, exception lost SendEmailAsync(order); // Fix await SendEmailAsync(order); ```
skilldb get csharp-dotnet-skills/Async PatternsFull skill: 257 linesAsync/Await Patterns — C#/.NET
You are an expert in asynchronous programming with C#, covering Task-based patterns, cancellation, concurrency, and async streams.
Core Philosophy
Overview
C#'s async/await model simplifies asynchronous I/O-bound programming by transforming methods into state machines at compile time. Correct usage is critical for application throughput and responsiveness. Misuse leads to deadlocks, thread pool starvation, and subtle race conditions.
Core Concepts
Basic Async Method Structure
public async Task<OrderDto> GetOrderAsync(int id, CancellationToken ct = default)
{
var order = await _db.Orders
.AsNoTracking()
.Where(o => o.Id == id)
.Select(o => new OrderDto(o.Id, o.Total, o.Status))
.FirstOrDefaultAsync(ct);
return order ?? throw new NotFoundException($"Order {id} not found");
}
Task vs ValueTask
// Use Task<T> as the default return type for async methods
public async Task<Product> GetProductAsync(int id) { ... }
// Use ValueTask<T> when the result is frequently available synchronously (e.g., cache hit)
public ValueTask<Product> GetProductAsync(int id)
{
if (_cache.TryGetValue(id, out var product))
return ValueTask.FromResult(product);
return new ValueTask<Product>(LoadProductFromDbAsync(id));
}
Cancellation
public async Task<List<Report>> GenerateReportsAsync(CancellationToken ct)
{
var reports = new List<Report>();
await foreach (var data in GetDataBatchesAsync(ct))
{
ct.ThrowIfCancellationRequested();
var report = await ProcessBatchAsync(data, ct);
reports.Add(report);
}
return reports;
}
// In ASP.NET, the framework passes a CancellationToken automatically:
app.MapGet("/reports", async (ReportService svc, CancellationToken ct) =>
await svc.GenerateReportsAsync(ct));
Implementation Patterns
Concurrent Execution with WhenAll
public async Task<DashboardDto> GetDashboardAsync(int userId, CancellationToken ct)
{
// Fire all three queries concurrently
var ordersTask = _orderService.GetRecentAsync(userId, ct);
var statsTask = _statsService.GetSummaryAsync(userId, ct);
var notificationsTask = _notificationService.GetUnreadAsync(userId, ct);
await Task.WhenAll(ordersTask, statsTask, notificationsTask);
return new DashboardDto(
Orders: ordersTask.Result,
Stats: statsTask.Result,
Notifications: notificationsTask.Result);
}
Controlled Parallelism
public async Task ProcessItemsAsync(IEnumerable<Item> items, CancellationToken ct)
{
var options = new ParallelOptions
{
MaxDegreeOfParallelism = 4,
CancellationToken = ct
};
await Parallel.ForEachAsync(items, options, async (item, token) =>
{
await ProcessSingleItemAsync(item, token);
});
}
Async Streams (IAsyncEnumerable)
public async IAsyncEnumerable<LogEntry> StreamLogsAsync(
string filter,
[EnumeratorCancellation] CancellationToken ct = default)
{
var offset = 0;
const int batchSize = 100;
while (!ct.IsCancellationRequested)
{
var batch = await _db.Logs
.Where(l => l.Message.Contains(filter))
.OrderBy(l => l.Timestamp)
.Skip(offset)
.Take(batchSize)
.ToListAsync(ct);
if (batch.Count == 0)
yield break;
foreach (var entry in batch)
yield return entry;
offset += batchSize;
}
}
// Consumption
await foreach (var log in svc.StreamLogsAsync("error", ct))
{
Console.WriteLine(log.Message);
}
Timeout Pattern
public async Task<Result> CallExternalServiceAsync(CancellationToken ct)
{
using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(ct);
timeoutCts.CancelAfter(TimeSpan.FromSeconds(10));
try
{
return await _httpClient.GetFromJsonAsync<Result>("/api/data", timeoutCts.Token);
}
catch (OperationCanceledException) when (!ct.IsCancellationRequested)
{
throw new TimeoutException("External service call timed out after 10 seconds");
}
}
Retry with Polly
builder.Services.AddHttpClient<IExternalApi, ExternalApi>()
.AddResilienceHandler("retry", pipeline =>
{
pipeline.AddRetry(new HttpRetryStrategyOptions
{
MaxRetryAttempts = 3,
Delay = TimeSpan.FromMilliseconds(500),
BackoffType = DelayBackoffType.Exponential,
ShouldHandle = new PredicateBuilder<HttpResponseMessage>()
.HandleResult(r => r.StatusCode == System.Net.HttpStatusCode.TooManyRequests)
.Handle<HttpRequestException>()
});
});
Channel-Based Producer-Consumer
public class BackgroundTaskQueue
{
private readonly Channel<Func<CancellationToken, Task>> _channel =
Channel.CreateBounded<Func<CancellationToken, Task>>(100);
public async ValueTask EnqueueAsync(Func<CancellationToken, Task> workItem)
{
await _channel.Writer.WriteAsync(workItem);
}
public async Task<Func<CancellationToken, Task>> DequeueAsync(CancellationToken ct)
{
return await _channel.Reader.ReadAsync(ct);
}
}
Best Practices
- Async all the way — once you go async, every caller in the chain should be async. Never mix sync-over-async.
- Always pass
CancellationTokenthrough the call chain so that work can be cancelled when the client disconnects. - Use
ConfigureAwait(false)in library code that does not need to resume on the original synchronization context. - Prefer
Task.WhenAllover sequentialawaitwhen operations are independent and can run concurrently. - Avoid
async void— exceptions inasync voidmethods crash the process. The only valid use is event handlers.
Common Pitfalls
- Sync-over-async deadlock — calling
.Resultor.Wait()on a task in a context with a synchronization context (e.g., old ASP.NET, UI frameworks) causes a deadlock.
// DEADLOCK in synchronization context:
var result = GetDataAsync().Result;
// Fix: use await
var result = await GetDataAsync();
- Forgetting to await a Task — the method fires and forgets, exceptions are silently swallowed.
// Bug: not awaited, exception lost
SendEmailAsync(order);
// Fix
await SendEmailAsync(order);
- Creating unnecessary Tasks — do not wrap synchronous code in
Task.Runin ASP.NET; it just moves work to another thread pool thread with no benefit.
// Bad: pointless Task.Run in an ASP.NET handler
var result = await Task.Run(() => ComputeSync());
// Good: just call it directly, or make the method genuinely async
var result = ComputeSync();
- ValueTask misuse — never await a
ValueTaskmore than once, and never use.Resulton one that is not completed. If you need to store or combine results, convert with.AsTask(). - Unbounded parallelism — using
Task.WhenAllon thousands of items without throttling can exhaust connections, memory, or rate limits. UseParallel.ForEachAsyncwithMaxDegreeOfParallelism.
Anti-Patterns
Over-engineering for hypothetical scale. Building for millions of users when you have hundreds adds complexity without value. Solve today's problems first.
Ignoring the existing ecosystem. Reinventing functionality that mature libraries already provide well wastes time and introduces unnecessary risk.
Premature abstraction. Creating elaborate frameworks and utilities before you have enough concrete cases to know what the abstraction should look like produces the wrong abstraction.
Neglecting error handling at boundaries. Internal code can trust its inputs, but system boundaries (user input, APIs, file I/O) require defensive validation.
Skipping documentation for obvious code. What is obvious to you today will not be obvious to your colleague next month or to you next year.
Install this skill directly: skilldb add csharp-dotnet-skills
Related Skills
Aspnet Minimal API
ASP.NET Minimal APIs for building lightweight HTTP endpoints in .NET
Blazor
Blazor component model, rendering modes, and interactive web UI development in .NET
Configuration
.NET configuration system, options pattern, and secrets management
Dependency Injection
Dependency injection patterns and service registration in .NET's built-in DI container
Entity Framework
Entity Framework Core ORM for data access, migrations, and query optimization in .NET
Mediatr
MediatR library for implementing CQRS, commands, queries, and pipeline behaviors in .NET