---
description: C# Async Patterns
alwaysApply: false
---

# C# Async Patterns

async/await done right. Every pitfall known.

## Golden Rules

- **Async all the way down** — never mix sync and async
- **Never block on async** — no `.Result`, `.Wait()`, `.GetAwaiter().GetResult()`
- **Always pass `CancellationToken`** — every I/O async method should accept one
- **`ConfigureAwait(false)` in library code only** — not needed in ASP.NET Core apps
- **Return `Task`, not `void`** — `async void` only for event handlers

## CancellationToken

```csharp
public async Task<OrderResult> ProcessAsync(
    CreateOrderRequest req, CancellationToken ct) {
    var user = await _userService.GetByIdAsync(req.UserId, ct);
    ct.ThrowIfCancellationRequested();
    var order = Order.Create(user, req.Items);
    await _repository.SaveAsync(order, ct);
    return new OrderResult(order.Id);
}
```

## Concurrent Operations

```csharp
var userTask = _userService.GetByIdAsync(userId, ct);
var ordersTask = _orderService.GetRecentAsync(userId, ct);
await Task.WhenAll(userTask, ordersTask);
// Use SemaphoreSlim for bounded concurrency on batch processing
```

## Channels

```csharp
private readonly Channel<DomainEvent> _channel =
    Channel.CreateBounded<DomainEvent>(new BoundedChannelOptions(1000) {
        FullMode = BoundedChannelFullMode.Wait, SingleReader = true
    });
```

## ValueTask

- Use when result is often synchronous (cached/pooled)
- Never await more than once
- Never use `.Result` on incomplete ValueTask
- Convert with `.AsTask()` before `Task.WhenAll`

## IAsyncEnumerable

```csharp
public async IAsyncEnumerable<LogEntry> StreamLogsAsync(
    string filter, [EnumeratorCancellation] CancellationToken ct = default) {
    await foreach (var line in _source.ReadLinesAsync(ct))
        if (line.Contains(filter, StringComparison.OrdinalIgnoreCase))
            yield return ParseLogEntry(line);
}
```

## Anti-Patterns

- **`async void`** — unobservable exceptions; use `async Task`
- **Fire-and-forget without error handling** — wrap in `Task.Run` with try/catch
- **`Task.Run` for I/O-bound work** — wastes thread pool thread; just `await` directly
