skills/core/async-patterns/SKILL.md
Use when writing async code, propagating CancellationTokens, or fixing async/await pitfalls.
npx skillsauth add faysilalshareef/dotnet-ai-kit 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.
CancellationToken through the call chainasync/await throughout — do not mix blocking and async codeValueTask<T> for hot paths that often complete synchronouslyConfigureAwait(false) in library code, not in ASP.NET Core app codeasync void except for event handlers// Always accept and forward CancellationToken
public sealed class OrderService(
IOrderRepository repository,
ILogger<OrderService> logger)
{
public async Task<Order?> GetOrderAsync(Guid id, CancellationToken ct)
{
logger.LogInformation("Fetching order {OrderId}", id);
return await repository.FindAsync(id, ct);
}
public async Task<List<Order>> GetOrdersAsync(
OrderFilter filter, CancellationToken ct)
{
var orders = await repository.ListAsync(filter, ct);
return orders;
}
}
// Controller / endpoint — ct comes from framework
app.MapGet("/orders/{id}", async (
Guid id, ISender sender, CancellationToken ct) =>
{
var result = await sender.Send(new GetOrderQuery(id), ct);
return result is not null ? Results.Ok(result) : Results.NotFound();
});
public async Task<DashboardData> GetDashboardAsync(CancellationToken ct)
{
// Run independent queries in parallel
var ordersTask = _orderRepository.GetRecentAsync(10, ct);
var statsTask = _statsService.GetSummaryAsync(ct);
var alertsTask = _alertService.GetActiveAsync(ct);
await Task.WhenAll(ordersTask, statsTask, alertsTask);
return new DashboardData(
Orders: ordersTask.Result,
Stats: statsTask.Result,
Alerts: alertsTask.Result);
}
// Use ValueTask when result is often cached / synchronous
public ValueTask<Product?> GetProductAsync(Guid id, CancellationToken ct)
{
if (_cache.TryGetValue(id, out Product? cached))
return ValueTask.FromResult(cached);
return LoadFromDatabaseAsync(id, ct);
}
private async ValueTask<Product?> LoadFromDatabaseAsync(
Guid id, CancellationToken ct)
{
var product = await _repository.FindAsync(id, ct);
if (product is not null)
_cache.Set(id, product, TimeSpan.FromMinutes(5));
return product;
}
// IAsyncEnumerable for large datasets
public async IAsyncEnumerable<OrderResponse> StreamOrdersAsync(
[EnumeratorCancellation] CancellationToken ct)
{
await foreach (var order in _repository.StreamAllAsync(ct))
{
yield return order.ToResponse();
}
}
// Consuming
await foreach (var order in service.StreamOrdersAsync(ct))
{
await ProcessOrderAsync(order, ct);
}
public sealed class ExpensiveResourceService : IAsyncDisposable
{
private readonly SemaphoreSlim _initLock = new(1, 1);
private ExpensiveResource? _resource;
public async Task<ExpensiveResource> GetResourceAsync(CancellationToken ct)
{
if (_resource is not null)
return _resource;
await _initLock.WaitAsync(ct);
try
{
// Double-check after acquiring lock
_resource ??= await ExpensiveResource.CreateAsync(ct);
return _resource;
}
finally
{
_initLock.Release();
}
}
public async ValueTask DisposeAsync()
{
_initLock.Dispose();
if (_resource is not null)
await _resource.DisposeAsync();
}
}
public async Task<Result<OrderResponse>> ProcessOrderAsync(
Guid orderId, CancellationToken ct)
{
// Create a linked token with timeout
using var timeoutCts = CancellationTokenSource
.CreateLinkedTokenSource(ct);
timeoutCts.CancelAfter(TimeSpan.FromSeconds(30));
try
{
var order = await _repository.FindAsync(orderId, timeoutCts.Token);
if (order is null)
return Result<OrderResponse>.Failure(
Error.NotFound("Order.NotFound", "Order not found"));
await _processor.ProcessAsync(order, timeoutCts.Token);
return Result<OrderResponse>.Success(order.ToResponse());
}
catch (OperationCanceledException) when (!ct.IsCancellationRequested)
{
// Timeout occurred, not caller cancellation
return Result<OrderResponse>.Failure(
Error.Unexpected("Order.Timeout", "Order processing timed out"));
}
}
public sealed class OrderProcessorService(
IServiceScopeFactory scopeFactory,
ILogger<OrderProcessorService> logger) : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
try
{
// Create a scope for each unit of work
using var scope = scopeFactory.CreateScope();
var processor = scope.ServiceProvider
.GetRequiredService<IOrderProcessor>();
await processor.ProcessPendingOrdersAsync(stoppingToken);
}
catch (Exception ex) when (ex is not OperationCanceledException)
{
logger.LogError(ex, "Error processing orders");
}
await Task.Delay(TimeSpan.FromSeconds(30), stoppingToken);
}
}
}
public sealed class OrderChannel
{
private readonly Channel<OrderEvent> _channel =
Channel.CreateBounded<OrderEvent>(new BoundedChannelOptions(1000)
{
FullMode = BoundedChannelFullMode.Wait,
SingleReader = false,
SingleWriter = false
});
public ChannelWriter<OrderEvent> Writer => _channel.Writer;
public ChannelReader<OrderEvent> Reader => _channel.Reader;
}
// BAD: async void — exceptions are unobservable
async void ProcessOrder(Order order) { }
// BAD: blocking on async code — can deadlock
var result = GetOrderAsync(id).Result;
var result2 = GetOrderAsync(id).GetAwaiter().GetResult();
// BAD: not propagating CancellationToken
public async Task<Order?> GetAsync(Guid id)
{
return await _repository.FindAsync(id); // missing ct!
}
// BAD: unnecessary async state machine
public async Task<Order?> GetAsync(Guid id, CancellationToken ct)
{
return await _repository.FindAsync(id, ct); // just return the Task
}
// GOOD: elide async when simply forwarding
public Task<Order?> GetAsync(Guid id, CancellationToken ct)
=> _repository.FindAsync(id, ct);
// BAD: Task.Run in ASP.NET Core (steals thread pool thread)
await Task.Run(() => ComputeExpensiveSync());
async Task and async ValueTask method signaturesCancellationToken is accepted and forwarded consistently.Result or .GetAwaiter().GetResult() blocking callsasync void methods (should only be event handlers)ConfigureAwait(false) usage patterns (library vs app code)Task.Run usage in ASP.NET Core request handlersct parameter to all async methods.Result and .Wait() calls and convert to awaitCancellationTokenIServiceScopeFactory_ = DoAsync() with proper background processing| Scenario | Recommendation |
|----------|---------------|
| Return type | Task<T> for most cases, ValueTask<T> for hot paths |
| Multiple independent calls | Task.WhenAll for parallel execution |
| Large result sets | IAsyncEnumerable<T> for streaming |
| Background work in web app | BackgroundService with IServiceScopeFactory |
| Thread-safe lazy init | SemaphoreSlim with double-check pattern |
| Producer/consumer queue | System.Threading.Channels |
| Timeout | CancellationTokenSource.CreateLinkedTokenSource + CancelAfter |
This skill depends on the always-on async-concurrency convention rule:
The rule is loaded into context whenever this skill is active (FR-011 universal whitelist).
data-ai
Use when about to claim work is complete, fixed, passing, or ready — before committing, creating PRs, or moving to the next task. Requires running verification commands and confirming output before making any success claims.
development
Use when encountering any bug, test failure, build error, or unexpected behavior — before proposing fixes or making changes.
development
Use when checkpointing, wrapping up, or handing off an AI-assisted development session.
development
Use when following the Specification-Driven Development lifecycle from plan through ship.