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 waypublicasync 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 contextspublic Order GetOrder(int id)
{
return _repo.GetByIdAsync(id).Result; // DEADLOCK RISK
}
```text
### Prefer `Task` and `ValueTask`
Return `Task` or `Task<T>` bydefault. Use `ValueTask<T>` the method frequently completes () to avoid `Task` allocation.
```csharp
ValueTask<User?> ()
{
(_cache.TryGetValue(id, user))
{
ValueTask.FromResult<User?>(user);
}
LoadUserAsync(id, ct);
}
ValueTask<User?> LoadUserAsync( id, CancellationToken ct)
{
user = _repo.GetByIdAsync(id, ct);
(user )
{
_cache[id] = user;
}
user;
}
```text
**ValueTask rules:**
- Never `` a `ValueTask` more than once
- Never use `.Result` `.GetAwaiter().GetResult()` an incomplete `ValueTask`
- If you need to multiple times pass it around, convert `.AsTask()`
---
These are the most common mistakes AI agents make generating C
```csharp
result = GetDataAsync().Result;
GetDataAsync().Wait();
result = GetDataAsync().GetAwaiter().GetResult();
result = GetDataAsync();
```text
The only safe place `.GetAwaiter().GetResult()` `Main()` pre-C
{
_repo.SaveAsync(order);
}
{
_repo.SaveAsync(order);
}
```text
The **only** valid use of ` `
{
bytes = File.ReadAllBytesAsync(path, ct).ConfigureAwait();
bytes;
}
{
order = _service.GetOrderAsync(id, ct);
Ok(order);
}
```text
```csharp
_ = SendEmailAsync(order);
_backgroundQueue.EnqueueAsync(ct => SendEmailAsync(order, ct));
```text
If fire--forget truly necessary, at minimum log the exception:
```csharp
_ = Task.Run( () =>
{
{
SendEmailAsync(order);
}
(Exception ex)
{
_logger.LogError(ex, , order.Id);
}
});
```text
Always accept forward `CancellationToken`. Never silently drop it.
```csharp
Task<List<Order>> GetAllAsync(CancellationToken ct = )
{
_dbContext.Orders.ToListAsync();
}
Task<List<Order>> GetAllAsync(CancellationToken ct = )
{
_dbContext.Orders.ToListAsync(ct);
}
```text
---
Combine external cancellation a timeout:
```
{
cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
cts.CancelAfter(TimeSpan.FromSeconds());
DoWorkAsync(cts.Token);
}
```text
```
{
( item items)
{
ct.ThrowIfCancellationRequested();
ProcessItemAsync(item, ct);
}
}
```text
---
```
{
ordersTask = _orderService.GetRecentAsync(userId, ct);
profileTask = _profileService.GetAsync(userId, ct);
statsTask = _statsService.GetAsync(userId, ct);
Task.WhenAll(ordersTask, profileTask, statsTask);
Dashboard(ordersTask.Result, profileTask.Result, statsTask.Result);
}
```text
```csharp
Parallel.ForEachAsync(items, ParallelOptions
{
MaxDegreeOfParallelism = ,
CancellationToken = ct
}, (item, token) =>
{
ProcessItemAsync(item, token);
});
```text
---
Use `IAsyncEnumerable<T>` streaming results instead of buffering entire collections:
```
{
( order _dbContext.Orders.AsAsyncEnumerable().WithCancellation(ct))
{
order;
}
}
```text
---
For background processing, use `BackgroundService` ( `IHostedService`) instead of `Task.Run` fire--forget
patterns. See [skill:dotnet-csharp-dependency-injection] registration patterns.
```
{
{
(!stoppingToken.IsCancellationRequested)
{
scope = scopeFactory.CreateScope();
processor = scope.ServiceProvider.GetRequiredService<IOrderProcessor>();
processor.ProcessPendingAsync(stoppingToken);
Task.Delay(TimeSpan.FromSeconds(), stoppingToken);
}
}
}
```text
---
```csharp
[]
{
repo = Substitute.For<IOrderRepository>();
repo.GetByIdAsync(, Arg.Any<CancellationToken>())
.Returns( Order { Id = });
service = OrderService(repo);
result = service.GetOrderAsync();
Assert.NotNull(result);
Assert.Equal(, result.Id);
}
[]
{
cts = CancellationTokenSource();
cts.Cancel();
Assert.ThrowsAsync<OperationCanceledException>(
() => _service.ProcessAsync(cts.Token));
}
```text
---
Async patterns skill are grounded publicly available content :
- **Stephen Clearys Async Guidance** -- Practical anti-patterns diagnostic scenarios ASP.NET Core. Source:
https:
- **Stephen Toubs Async Blog](https:
- [Asynchronous programming patterns](https:
- [Task-
when
synchronously
cache
hits, buffered I/O
// ValueTask: frequently synchronous completion
public
GetCachedUserAsync
int id, CancellationToken ct = default
if
out
var
return
return
private
async
int
var
await
if
is
not
null
return
await
or
on
await
or
with
## Agent Gotchas
async
when
# code.
### 1. Blocking on Async (`.Result`, `.Wait()`, `.GetAwaiter().GetResult()`)
// WRONG -- all of these can deadlock
var
var
// CORRECT
var
await
for
is
in
# 7.1 or in rare infrastructure code where async
isimpossible (static constructors, `Dispose()`).
### 2. `asyncvoid`
`asyncvoid` methods cannot be awaited, and unhandled exceptions in them crash the process.
```csharp
// WRONG -- fire-and-forget, unobserved exceptionsasyncvoidProcessOrder(Order order)
await
// CORRECT
async Task ProcessOrderAsync(Order order)
await
async
void
iseventhandlers (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 isnot needed because there is no synchronization context.
```csharp
// Library codepublicasync Task<byte[]> ReadFileAsync(string path, CancellationToken ct = default)
var
await
false
return
// Application code (ASP.NET Core) -- ConfigureAwait not needed
'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 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'