Claude
Skills
Sign in
Back

dotnet-csharp-async-patterns

Included with Lifetime
$97 forever

Writing async/await code. Task patterns, ConfigureAwait, cancellation, and common agent pitfalls.

AI Agents

What this skill does


# dotnet-csharp-async-patterns

Async/await best practices for .NET applications. Covers correct task usage, cancellation propagation, and the most common mistakes AI agents make when generating async code.

Cross-references: [skill:dotnet-csharp-dependency-injection] for `IHostedService`/`BackgroundService` registration, [skill:dotnet-csharp-coding-standards] for `Async` suffix naming, [skill:dotnet-csharp-modern-patterns] for language-level features.

---

## Core Rules

### Always Async All the Way

Every method in the async call chain must be `async` and `await`ed. Mixing sync and async causes deadlocks or thread pool starvation.

```csharp
// Correct: async all the way
public async Task<Order> GetOrderAsync(int id, CancellationToken ct = default)
{
    var order = await _repo.GetByIdAsync(id, ct);
    return order;
}

// WRONG: blocking on async -- causes deadlocks in ASP.NET and UI contexts
public Order GetOrder(int id)
{
    return _repo.GetByIdAsync(id).Result; // DEADLOCK RISK
}
```

### Prefer `Task` and `ValueTask`

Return `Task` or `Task<T>` by default. Use `ValueTask<T>` when the method frequently completes synchronously (cache hits, buffered I/O) to avoid `Task` allocation.

```csharp
// ValueTask: frequently synchronous completion
public ValueTask<User?> GetCachedUserAsync(int id, CancellationToken ct = default)
{
    if (_cache.TryGetValue(id, out var user))
    {
        return ValueTask.FromResult<User?>(user);
    }

    return LoadUserAsync(id, ct);
}

private async ValueTask<User?> LoadUserAsync(int id, CancellationToken ct)
{
    var user = await _repo.GetByIdAsync(id, ct);
    if (user is not null)
    {
        _cache[id] = user;
    }

    return user;
}
```

**ValueTask rules:**
- Never `await` a `ValueTask` more than once
- Never use `.Result` or `.GetAwaiter().GetResult()` on an incomplete `ValueTask`
- If you need to await multiple times or pass it around, convert with `.AsTask()`

---

## Agent Gotchas

These are the most common async mistakes AI agents make when generating C# code.

### 1. Blocking on Async (`.Result`, `.Wait()`, `.GetAwaiter().GetResult()`)

```csharp
// WRONG -- all of these can deadlock
var result = GetDataAsync().Result;
GetDataAsync().Wait();
var result = GetDataAsync().GetAwaiter().GetResult();

// CORRECT
var result = await GetDataAsync();
```

The only safe place for `.GetAwaiter().GetResult()` is in `Main()` pre-C# 7.1 or in rare infrastructure code where async is impossible (static constructors, `Dispose()`).

### 2. `async void`

`async void` methods cannot be awaited, and unhandled exceptions in them crash the process.

```csharp
// WRONG -- fire-and-forget, unobserved exceptions
async void ProcessOrder(Order order)
{
    await _repo.SaveAsync(order);
}

// CORRECT
async Task ProcessOrderAsync(Order order)
{
    await _repo.SaveAsync(order);
}
```

The **only** valid use of `async void` is event handlers (WinForms, WPF, Blazor `@onclick`), where the framework requires a `void` return type.

### 3. Missing `ConfigureAwait`

In **library code**, use `ConfigureAwait(false)` to avoid capturing the synchronization context. In **application code** (ASP.NET Core, console apps), it is not needed because there is no synchronization context.

```csharp
// Library code
public async Task<byte[]> ReadFileAsync(string path, CancellationToken ct = default)
{
    var bytes = await File.ReadAllBytesAsync(path, ct).ConfigureAwait(false);
    return bytes;
}

// Application code (ASP.NET Core) -- ConfigureAwait not needed
public async Task<IActionResult> GetOrder(int id, CancellationToken ct)
{
    var order = await _service.GetOrderAsync(id, ct);
    return Ok(order);
}
```

### 4. Fire-and-Forget Without Error Handling

```csharp
// WRONG -- exception is silently swallowed
_ = SendEmailAsync(order);

// CORRECT -- use IHostedService or a background channel
await _backgroundQueue.EnqueueAsync(ct => SendEmailAsync(order, ct));
```

If fire-and-forget is truly necessary, at minimum log the exception:

```csharp
_ = Task.Run(async () =>
{
    try
    {
        await SendEmailAsync(order);
    }
    catch (Exception ex)
    {
        _logger.LogError(ex, "Failed to send email for order {OrderId}", order.Id);
    }
});
```

### 5. Forgetting `CancellationToken`

Always accept and forward `CancellationToken`. Never silently drop it.

```csharp
// WRONG -- token not forwarded
public async Task<List<Order>> GetAllAsync(CancellationToken ct = default)
{
    return await _dbContext.Orders.ToListAsync(); // missing ct!
}

// CORRECT
public async Task<List<Order>> GetAllAsync(CancellationToken ct = default)
{
    return await _dbContext.Orders.ToListAsync(ct);
}
```

---

## Cancellation Patterns

### Creating Linked Tokens

Combine external cancellation with a timeout:

```csharp
public async Task<Result> ProcessWithTimeoutAsync(CancellationToken ct = default)
{
    using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
    cts.CancelAfter(TimeSpan.FromSeconds(30));

    return await DoWorkAsync(cts.Token);
}
```

### Responding to Cancellation

```csharp
public async Task ProcessBatchAsync(IEnumerable<Item> items, CancellationToken ct = default)
{
    foreach (var item in items)
    {
        ct.ThrowIfCancellationRequested();
        await ProcessItemAsync(item, ct);
    }
}
```

---

## Parallel Async

### `Task.WhenAll` for Independent Operations

```csharp
public async Task<Dashboard> LoadDashboardAsync(int userId, CancellationToken ct = default)
{
    var ordersTask = _orderService.GetRecentAsync(userId, ct);
    var profileTask = _profileService.GetAsync(userId, ct);
    var statsTask = _statsService.GetAsync(userId, ct);

    await Task.WhenAll(ordersTask, profileTask, statsTask);

    return new Dashboard(ordersTask.Result, profileTask.Result, statsTask.Result);
}
```

### `Parallel.ForEachAsync` (.NET 6+) for Bounded Parallelism

```csharp
await Parallel.ForEachAsync(items, new ParallelOptions
{
    MaxDegreeOfParallelism = 4,
    CancellationToken = ct
}, async (item, token) =>
{
    await ProcessItemAsync(item, token);
});
```

---

## `IAsyncEnumerable<T>` Streaming

Use `IAsyncEnumerable<T>` for streaming results instead of buffering entire collections:

```csharp
public async IAsyncEnumerable<Order> GetOrdersStreamAsync(
    [EnumeratorCancellation] CancellationToken ct = default)
{
    await foreach (var order in _dbContext.Orders.AsAsyncEnumerable().WithCancellation(ct))
    {
        yield return order;
    }
}
```

---

## Background Work

For background processing, use `BackgroundService` (or `IHostedService`) instead of `Task.Run` or fire-and-forget patterns. See [skill:dotnet-csharp-dependency-injection] for registration patterns.

```csharp
public sealed class OrderProcessorWorker(
    IServiceScopeFactory scopeFactory,
    ILogger<OrderProcessorWorker> logger) : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            using var scope = scopeFactory.CreateScope();
            var processor = scope.ServiceProvider.GetRequiredService<IOrderProcessor>();

            await processor.ProcessPendingAsync(stoppingToken);
            await Task.Delay(TimeSpan.FromSeconds(30), stoppingToken);
        }
    }
}
```

---

## Testing Async Code

```csharp
[Fact]
public async Task GetOrderAsync_WhenFound_ReturnsOrder()
{
    // Arrange
    var repo = Substitute.For<IOrderRepository>();
    repo.GetByIdAsync(42, Arg.Any<CancellationToken>())
        .Returns(new Order { Id = 42 });
    var service = new OrderService(repo);

    // Act
    var result = await service.GetOrderAsync(42);

    // Assert
    Assert.NotNull(result);
    Assert.Equal(42, result.Id);
}

[Fact]
public async Task ProcessAsync_WhenCancelled_ThrowsOperationCanceled()
{
    using var cts = new CancellationTokenSource();
    cts.Cancel();

    await Assert.ThrowsAsync<OperationCanceledException>(
    

Related in AI Agents