.agent/skills/dotnet-csharp-async-patterns/SKILL.md
Writing async/await code. Task patterns, ConfigureAwait, cancellation, and common agent pitfalls.
npx skillsauth add rudironsoni/dotnet-harness-plugin dotnet-csharp-async-patternsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
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.
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.
## 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/)
tools
Rulesync CLI tool documentation - unified AI rule file management for various AI coding tools
tools
Authors xUnit v3 tests -- Facts, Theories, fixtures, parallelism, IAsyncLifetime.
documentation
Writes XML doc comments. Tags, inheritdoc, GenerateDocumentationFile, warning suppression.
tools
Builds WPF on .NET 8+. Host builder, MVVM Toolkit, Fluent theme, performance, modern C# patterns.