dotnet-csharp-async-patterns
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
Chinesedotnet-csharp-async-patterns
.NET C# 异步模式
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 / registration, [skill:dotnet-csharp-coding-standards] for suffix naming, [skill:dotnet-csharp-modern-patterns] for language-level features.
IHostedServiceBackgroundServiceAsync.NET应用中async/await的最佳实践,涵盖任务的正确使用、取消操作的传递,以及AI Agent生成异步代码时最常犯的错误。
交叉参考:[skill:dotnet-csharp-dependency-injection] 用于/注册,[skill:dotnet-csharp-coding-standards] 用于后缀命名规范,[skill:dotnet-csharp-modern-patterns] 用于语言级特性。
IHostedServiceBackgroundServiceAsyncCore Rules
核心规则
Always Async All the Way
始终全程异步
Every method in the async call chain must be and ed. Mixing sync and async causes deadlocks or thread pool starvation.
asyncawaitcsharp
// 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
}异步调用链中的每个方法都必须是并使用。混合同步和异步代码会导致死锁或线程池耗尽。
asyncawaitcsharp
// 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
TaskValueTask优先使用Task
和ValueTask
TaskValueTaskReturn or by default. Use when the method frequently completes synchronously (cache hits, buffered I/O) to avoid allocation.
TaskTask<T>ValueTask<T>Taskcsharp
// 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 a
awaitmore than onceValueTask - Never use or
.Resulton an incomplete.GetAwaiter().GetResult()ValueTask - If you need to await multiple times or pass it around, convert with
.AsTask()
默认返回或。当方法经常同步完成(缓存命中、缓冲I/O)时,使用以避免分配开销。
TaskTask<T>ValueTask<T>Taskcsharp
// 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规则:
- 切勿多次同一个
awaitValueTask - 切勿在未完成的上使用
ValueTask或.Result.GetAwaiter().GetResult() - 如果需要多次等待或传递该任务,使用转换
.AsTask()
Agent Gotchas
AI Agent常见误区
These are the most common async mistakes AI agents make when generating C# code.
以下是AI Agent生成C#异步代码时最常犯的错误。
1. Blocking on Async (.Result
, .Wait()
, .GetAwaiter().GetResult()
)
.Result.Wait().GetAwaiter().GetResult()1. 阻塞异步代码(.Result
、.Wait()
、.GetAwaiter().GetResult()
)
.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 is in pre-C# 7.1 or in rare infrastructure code where async is impossible (static constructors, ).
.GetAwaiter().GetResult()Main()Dispose()csharp
// WRONG -- all of these can deadlock
var result = GetDataAsync().Result;
GetDataAsync().Wait();
var result = GetDataAsync().GetAwaiter().GetResult();
// CORRECT
var result = await GetDataAsync();.GetAwaiter().GetResult()Main()Dispose()2. async void
async void2. async void
async voidasync voidcsharp
// 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 is event handlers (WinForms, WPF, Blazor ), where the framework requires a return type.
async void@onclickvoidasync voidcsharp
// 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);
}async void@onclickvoid3. Missing ConfigureAwait
ConfigureAwait3. 遗漏ConfigureAwait
ConfigureAwaitIn library code, use to avoid capturing the synchronization context. In application code (ASP.NET Core, console apps), it is not needed because there is no synchronization context.
ConfigureAwait(false)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);
}在类库代码中,使用避免捕获同步上下文。在应用程序代码(ASP.NET Core、控制台应用)中则不需要,因为不存在同步上下文。
ConfigureAwait(false)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
4. 无错误处理的即发即弃
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);
}
});csharp
// WRONG -- exception is silently swallowed
_ = SendEmailAsync(order);
// CORRECT -- use IHostedService or a background channel
await _backgroundQueue.EnqueueAsync(ct => SendEmailAsync(order, ct));如果确实需要即发即弃,至少要记录异常:
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
CancellationToken5. 遗漏CancellationToken
CancellationTokenAlways accept and forward . Never silently drop it.
CancellationTokencsharp
// 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);
}始终接收并传递,切勿静默丢弃。
CancellationTokencsharp
// 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);
}将外部取消与超时结合:
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);
}
}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
Task.WhenAll使用Task.WhenAll
处理独立操作
Task.WhenAllcsharp
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);
}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
Parallel.ForEachAsync使用Parallel.ForEachAsync
(.NET 6+)实现有限并行
Parallel.ForEachAsynccsharp
await Parallel.ForEachAsync(items, new ParallelOptions
{
MaxDegreeOfParallelism = 4,
CancellationToken = ct
}, async (item, token) =>
{
await ProcessItemAsync(item, token);
});csharp
await Parallel.ForEachAsync(items, new ParallelOptions
{
MaxDegreeOfParallelism = 4,
CancellationToken = ct
}, async (item, token) =>
{
await ProcessItemAsync(item, token);
});IAsyncEnumerable<T>
Streaming
IAsyncEnumerable<T>IAsyncEnumerable<T>
流处理
IAsyncEnumerable<T>Use for streaming results instead of buffering entire collections:
IAsyncEnumerable<T>csharp
public async IAsyncEnumerable<Order> GetOrdersStreamAsync(
[EnumeratorCancellation] CancellationToken ct = default)
{
await foreach (var order in _dbContext.Orders.AsAsyncEnumerable().WithCancellation(ct))
{
yield return order;
}
}使用实现结果流处理,而非缓冲整个集合:
IAsyncEnumerable<T>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 (or ) instead of or fire-and-forget patterns. See [skill:dotnet-csharp-dependency-injection] for registration patterns.
BackgroundServiceIHostedServiceTask.Runcsharp
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);
}
}
}对于后台处理,使用(或)而非或即发即弃模式。注册模式请参考[skill:dotnet-csharp-dependency-injection]。
BackgroundServiceIHostedServiceTask.Runcsharp
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>(
() => _service.ProcessAsync(cts.Token));
}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));
}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.
本技能中的异步模式基于以下公开内容:
- Stephen Cleary的《Concurrency in C#》及博客 -- .NET异步最佳实践的权威指南。本技能应用的核心指导原则:“全程异步”(切勿阻塞异步代码)、“无线程”(异步I/O等待时不占用线程)、正确的CancellationToken传递、通过IAsyncDisposable实现异步释放,以及用于长期运行任务的BackgroundService模式。来源:https://blog.stephencleary.com/
- David Fowler的异步指导 -- ASP.NET Core实用异步反模式及诊断场景。来源:https://github.com/davidfowl/AspNetCoreDiagnosticScenarios/blob/master/AsyncGuidance.md
- Stephen Toub的ConfigureAwait常见问题 -- 不同应用类型中ConfigureAwait行为的标准参考。来源:https://devblogs.microsoft.com/dotnet/configureawait-faq/
注意: 本技能应用公开文档中的指导原则,不代表或代言上述来源。