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.
Scope
- Async/await best practices and Task patterns
- ConfigureAwait usage and SynchronizationContext
- Cancellation token propagation
- Common async agent pitfalls and fixes
Out of scope
- Thread synchronization primitives (lock, SemaphoreSlim) -- see [skill:dotnet-csharp-concurrency-patterns]
- Channel producer/consumer patterns -- see [skill:dotnet-channels]
- BackgroundService registration and lifecycle -- see [skill:dotnet-background-services]
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 awaited. Mixing sync and async causes deadlocks or thread
pool starvation.
// 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
}
```text
### 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;
}
```text
**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();
```text
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);
}
```text
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);
}
```text
### 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));
```text
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);
}
});
```text
### 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);
}
```text
---
## 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);
}
```text
### 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);
}
}
```text
---
## 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);
}
```text
### `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);
});
```text
---
## `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;
}
}
```text
---
## 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);
}
}
}
```text
---
## 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>(
() => _service.ProcessAsync(cts.Token));
}
```text
---
## Knowledge Sources
Async patterns in this skill are grounded in publicly available content from:
- **Stephen Cleary's "Concurrency in C#" and Blog** -- Definitive async best practices for .NET. Key guidance applied in
this skill: "async all the way" (never block on async), "there is no thread" (async I/O does not consume a thread
while waiting), correct CancellationToken propagation, async disposal via IAsyncDisposable, and BackgroundService
patterns for long-running work. Source: https://blog.stephencleary.com/
- **David Fowler's Async Guidance** -- Practical async anti-patterns and diagnostic scenarios for ASP.NET Core. Source:
https://github.com/davidfowl/AspNetCoreDiagnosticScenarios/blob/master/AsyncGuidance.md
- **Stephen Toub's ConfigureAwait FAQ** -- Canonical reference for ConfigureAwait behavior across application types.
Source: https://devblogs.microsoft.com/dotnet/configureawait-faq/
> **Note:** This skill applies publicly documented guidance. It does not represent or speak for the named sources.
## Code Navigation (Serena MCP)
**Primary approach:** Use Serena symbol operations for efficient code navigation:
1. **Find definitions**: `serena_find_symbol` instead of text search
2. **Understand structure**: `serena_get_symbols_overview` for file organization
3. **Track references**: `serena_find_referencing_symbols` for impact analysis
4. **Precise edits**: `serena_replace_symbol_body` for clean modifications
**When to use Serena vs traditional tools:**
- ✅ **Use Serena**: Navigation, refactoring, dependency analysis, precise edits
- ✅ **Use Read/Grep**: Reading full files, pattern matching, simple text operations
- ✅ **Fallback**: If Serena unavailable, traditional tools work fine
**Example workflow:**
```text
# Instead of:
Read: src/Services/OrderService.cs
Grep: "public void ProcessOrder"
# Use:
serena_find_symbol: "OrderService/ProcessOrder"
serena_get_symbols_overview: "src/Services/OrderService.cs"
```
## References
- [Async/await best practices (David Fowler)](https://github.com/davidfowl/AspNetCoreDiagnosticScenarios/blob/master/AsyncGuidance.md)
- [Stephen Cleary's Async Blog](https://blog.stephencleary.com/)
- [Asynchronous programming patterns](https://learn.microsoft.com/en-us/dotnet/csharp/asynchronous-programming/)
- [Task-based asynchronous pattern (TAP)](https://learn.microsoft.com/en-us/dotnet/standard/asynchronous-programming-patterns/task-based-asynchronous-pattern-tap)
- [ConfigureAwait FAQ](https://devblogs.microsoft.com/dotnet/configureawait-faq/)
- [Framework Design Guidelines](https://learn.microsoft.com/en-us/dotnet/standard/design-guidelines/)
1---2name: dotnet-csharp-async-patterns3description: Writing async/await code. Task patterns, ConfigureAwait, cancellation, and common agent pitfalls.4license: MIT5targets: ['*']6category: fundamentals7subcategory: language-patterns8tags:9 - csharp10 - dotnet11 - skill12 - language-patterns13 - async14version: '1.0.0'15author: 'dotnet-agent-harness'16invocable: true17related_skills:18 - dotnet-csharp-coding-standards19 - dotnet-csharp-concurrency-patterns20 - dotnet-csharp-dependency-injection21claudecode:22 allowed-tools: ['Read', 'Grep', 'Glob', 'Bash', 'Write', 'Edit']23codexcli:24 short-description: '.NET skill guidance for csharp tasks'25opencode:26 allowed-tools: ['Read', 'Grep', 'Glob', 'Bash', 'Write', 'Edit']27copilot: {}28geminicli: {}29antigravity: {}30---3132# dotnet-csharp-async-patterns3334Async/await best practices for .NET applications. Covers correct task usage, cancellation propagation, and the most35common mistakes AI agents make when generating async code.3637## Scope3839- Async/await best practices and Task patterns40- ConfigureAwait usage and SynchronizationContext41- Cancellation token propagation42- Common async agent pitfalls and fixes4344## Out of scope4546- Thread synchronization primitives (lock, SemaphoreSlim) -- see [skill:dotnet-csharp-concurrency-patterns]47- Channel<T> producer/consumer patterns -- see [skill:dotnet-channels]48- BackgroundService registration and lifecycle -- see [skill:dotnet-background-services]4950Cross-references: [skill:dotnet-csharp-dependency-injection] for `IHostedService`/`BackgroundService` registration,51[skill:dotnet-csharp-coding-standards] for `Async` suffix naming, [skill:dotnet-csharp-modern-patterns] for52language-level features.5354---5556## Core Rules5758### Always Async All the Way5960Every method in the async call chain must be `async` and `await`ed. Mixing sync and async causes deadlocks or thread61pool starvation.6263````csharp6465// Correct: async all the way66public async Task<Order> GetOrderAsync(int id, CancellationToken ct = default)67{68 var order = await _repo.GetByIdAsync(id, ct);69 return order;70}7172// WRONG: blocking on async -- causes deadlocks in ASP.NET and UI contexts73public Order GetOrder(int id)74{75 return _repo.GetByIdAsync(id).Result; // DEADLOCK RISK76}7778```text7980### Prefer `Task` and `ValueTask`8182Return `Task` or `Task<T>` by default. Use `ValueTask<T>` when the method frequently completes synchronously (cache83hits, buffered I/O) to avoid `Task` allocation.8485```csharp8687// ValueTask: frequently synchronous completion88public ValueTask<User?> GetCachedUserAsync(int id, CancellationToken ct = default)89{90 if (_cache.TryGetValue(id, out var user))91 {92 return ValueTask.FromResult<User?>(user);93 }9495 return LoadUserAsync(id, ct);96}9798private async ValueTask<User?> LoadUserAsync(int id, CancellationToken ct)99{100 var user = await _repo.GetByIdAsync(id, ct);101 if (user is not null)102 {103 _cache[id] = user;104 }105106 return user;107}108109```text110111**ValueTask rules:**112113- Never `await` a `ValueTask` more than once114- Never use `.Result` or `.GetAwaiter().GetResult()` on an incomplete `ValueTask`115- If you need to await multiple times or pass it around, convert with `.AsTask()`116117---118119## Agent Gotchas120121These are the most common async mistakes AI agents make when generating C# code.122123### 1. Blocking on Async (`.Result`, `.Wait()`, `.GetAwaiter().GetResult()`)124125```csharp126127// WRONG -- all of these can deadlock128var result = GetDataAsync().Result;129GetDataAsync().Wait();130var result = GetDataAsync().GetAwaiter().GetResult();131132// CORRECT133var result = await GetDataAsync();134135```text136137The only safe place for `.GetAwaiter().GetResult()` is in `Main()` pre-C# 7.1 or in rare infrastructure code where async138is impossible (static constructors, `Dispose()`).139140### 2. `async void`141142`async void` methods cannot be awaited, and unhandled exceptions in them crash the process.143144```csharp145146// WRONG -- fire-and-forget, unobserved exceptions147async void ProcessOrder(Order order)148{149 await _repo.SaveAsync(order);150}151152// CORRECT153async Task ProcessOrderAsync(Order order)154{155 await _repo.SaveAsync(order);156}157158```text159160The **only** valid use of `async void` is event handlers (WinForms, WPF, Blazor `@onclick`), where the framework161requires a `void` return type.162163### 3. Missing `ConfigureAwait`164165In **library code**, use `ConfigureAwait(false)` to avoid capturing the synchronization context. In **application code**166(ASP.NET Core, console apps), it is not needed because there is no synchronization context.167168```csharp169170// Library code171public async Task<byte[]> ReadFileAsync(string path, CancellationToken ct = default)172{173 var bytes = await File.ReadAllBytesAsync(path, ct).ConfigureAwait(false);174 return bytes;175}176177// Application code (ASP.NET Core) -- ConfigureAwait not needed178public async Task<IActionResult> GetOrder(int id, CancellationToken ct)179{180 var order = await _service.GetOrderAsync(id, ct);181 return Ok(order);182}183184```text185186### 4. Fire-and-Forget Without Error Handling187188```csharp189190// WRONG -- exception is silently swallowed191_ = SendEmailAsync(order);192193// CORRECT -- use IHostedService or a background channel194await _backgroundQueue.EnqueueAsync(ct => SendEmailAsync(order, ct));195196```text197198If fire-and-forget is truly necessary, at minimum log the exception:199200```csharp201202_ = Task.Run(async () =>203{204 try205 {206 await SendEmailAsync(order);207 }208 catch (Exception ex)209 {210 _logger.LogError(ex, "Failed to send email for order {OrderId}", order.Id);211 }212});213214```text215216### 5. Forgetting `CancellationToken`217218Always accept and forward `CancellationToken`. Never silently drop it.219220```csharp221222// WRONG -- token not forwarded223public async Task<List<Order>> GetAllAsync(CancellationToken ct = default)224{225 return await _dbContext.Orders.ToListAsync(); // missing ct!226}227228// CORRECT229public async Task<List<Order>> GetAllAsync(CancellationToken ct = default)230{231 return await _dbContext.Orders.ToListAsync(ct);232}233234```text235236---237238## Cancellation Patterns239240### Creating Linked Tokens241242Combine external cancellation with a timeout:243244```csharp245246public async Task<Result> ProcessWithTimeoutAsync(CancellationToken ct = default)247{248 using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct);249 cts.CancelAfter(TimeSpan.FromSeconds(30));250251 return await DoWorkAsync(cts.Token);252}253254```text255256### Responding to Cancellation257258```csharp259260public async Task ProcessBatchAsync(IEnumerable<Item> items, CancellationToken ct = default)261{262 foreach (var item in items)263 {264 ct.ThrowIfCancellationRequested();265 await ProcessItemAsync(item, ct);266 }267}268269```text270271---272273## Parallel Async274275### `Task.WhenAll` for Independent Operations276277```csharp278279public async Task<Dashboard> LoadDashboardAsync(int userId, CancellationToken ct = default)280{281 var ordersTask = _orderService.GetRecentAsync(userId, ct);282 var profileTask = _profileService.GetAsync(userId, ct);283 var statsTask = _statsService.GetAsync(userId, ct);284285 await Task.WhenAll(ordersTask, profileTask, statsTask);286287 return new Dashboard(ordersTask.Result, profileTask.Result, statsTask.Result);288}289290```text291292### `Parallel.ForEachAsync` (.NET 6+) for Bounded Parallelism293294```csharp295296await Parallel.ForEachAsync(items, new ParallelOptions297{298 MaxDegreeOfParallelism = 4,299 CancellationToken = ct300}, async (item, token) =>301{302 await ProcessItemAsync(item, token);303});304305```text306307---308309## `IAsyncEnumerable<T>` Streaming310311Use `IAsyncEnumerable<T>` for streaming results instead of buffering entire collections:312313```csharp314315public async IAsyncEnumerable<Order> GetOrdersStreamAsync(316 [EnumeratorCancellation] CancellationToken ct = default)317{318 await foreach (var order in _dbContext.Orders.AsAsyncEnumerable().WithCancellation(ct))319 {320 yield return order;321 }322}323324```text325326---327328## Background Work329330For background processing, use `BackgroundService` (or `IHostedService`) instead of `Task.Run` or fire-and-forget331patterns. See [skill:dotnet-csharp-dependency-injection] for registration patterns.332333```csharp334335public sealed class OrderProcessorWorker(336 IServiceScopeFactory scopeFactory,337 ILogger<OrderProcessorWorker> logger) : BackgroundService338{339 protected override async Task ExecuteAsync(CancellationToken stoppingToken)340 {341 while (!stoppingToken.IsCancellationRequested)342 {343 using var scope = scopeFactory.CreateScope();344 var processor = scope.ServiceProvider.GetRequiredService<IOrderProcessor>();345346 await processor.ProcessPendingAsync(stoppingToken);347 await Task.Delay(TimeSpan.FromSeconds(30), stoppingToken);348 }349 }350}351352```text353354---355356## Testing Async Code357358```csharp359360[Fact]361public async Task GetOrderAsync_WhenFound_ReturnsOrder()362{363 // Arrange364 var repo = Substitute.For<IOrderRepository>();365 repo.GetByIdAsync(42, Arg.Any<CancellationToken>())366 .Returns(new Order { Id = 42 });367 var service = new OrderService(repo);368369 // Act370 var result = await service.GetOrderAsync(42);371372 // Assert373 Assert.NotNull(result);374 Assert.Equal(42, result.Id);375}376377[Fact]378public async Task ProcessAsync_WhenCancelled_ThrowsOperationCanceled()379{380 using var cts = new CancellationTokenSource();381 cts.Cancel();382383 await Assert.ThrowsAsync<OperationCanceledException>(384 () => _service.ProcessAsync(cts.Token));385}386387```text388389---390391## Knowledge Sources392393Async patterns in this skill are grounded in publicly available content from:394395- **Stephen Cleary's "Concurrency in C#" and Blog** -- Definitive async best practices for .NET. Key guidance applied in396 this skill: "async all the way" (never block on async), "there is no thread" (async I/O does not consume a thread397 while waiting), correct CancellationToken propagation, async disposal via IAsyncDisposable, and BackgroundService398 patterns for long-running work. Source: https://blog.stephencleary.com/399- **David Fowler's Async Guidance** -- Practical async anti-patterns and diagnostic scenarios for ASP.NET Core. Source:400 https://github.com/davidfowl/AspNetCoreDiagnosticScenarios/blob/master/AsyncGuidance.md401- **Stephen Toub's ConfigureAwait FAQ** -- Canonical reference for ConfigureAwait behavior across application types.402 Source: https://devblogs.microsoft.com/dotnet/configureawait-faq/403404> **Note:** This skill applies publicly documented guidance. It does not represent or speak for the named sources.405406407408## Code Navigation (Serena MCP)409410**Primary approach:** Use Serena symbol operations for efficient code navigation:4114121. **Find definitions**: `serena_find_symbol` instead of text search4132. **Understand structure**: `serena_get_symbols_overview` for file organization4143. **Track references**: `serena_find_referencing_symbols` for impact analysis4154. **Precise edits**: `serena_replace_symbol_body` for clean modifications416417**When to use Serena vs traditional tools:**418- ✅ **Use Serena**: Navigation, refactoring, dependency analysis, precise edits419- ✅ **Use Read/Grep**: Reading full files, pattern matching, simple text operations420- ✅ **Fallback**: If Serena unavailable, traditional tools work fine421422**Example workflow:**423```text424# Instead of:425Read: src/Services/OrderService.cs426Grep: "public void ProcessOrder"427428# Use:429serena_find_symbol: "OrderService/ProcessOrder"430serena_get_symbols_overview: "src/Services/OrderService.cs"431```432## References433434- [Async/await best practices (David Fowler)](https://github.com/davidfowl/AspNetCoreDiagnosticScenarios/blob/master/AsyncGuidance.md)435- [Stephen Cleary's Async Blog](https://blog.stephencleary.com/)436- [Asynchronous programming patterns](https://learn.microsoft.com/en-us/dotnet/csharp/asynchronous-programming/)437- [Task-based asynchronous pattern (TAP)](https://learn.microsoft.com/en-us/dotnet/standard/asynchronous-programming-patterns/task-based-asynchronous-pattern-tap)438- [ConfigureAwait FAQ](https://devblogs.microsoft.com/dotnet/configureawait-faq/)439- [Framework Design Guidelines](https://learn.microsoft.com/en-us/dotnet/standard/design-guidelines/)440````