dotnet-csharp-async-patterns

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

dotnet-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
IHostedService
/
BackgroundService
registration, [skill:dotnet-csharp-coding-standards] for
Async
suffix naming, [skill:dotnet-csharp-modern-patterns] for language-level features.

.NET应用中async/await的最佳实践,涵盖任务的正确使用、取消操作的传递,以及AI Agent生成异步代码时最常犯的错误。
交叉参考:[skill:dotnet-csharp-dependency-injection] 用于
IHostedService
/
BackgroundService
注册,[skill:dotnet-csharp-coding-standards] 用于
Async
后缀命名规范,[skill:dotnet-csharp-modern-patterns] 用于语言级特性。

Core Rules

核心规则

Always Async All the Way

始终全程异步

Every method in the async call chain must be
async
and
await
ed. Mixing sync and async causes deadlocks or thread pool starvation.
csharp
// 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
}
异步调用链中的每个方法都必须是
async
并使用
await
。混合同步和异步代码会导致死锁或线程池耗尽。
csharp
// 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

优先使用
Task
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;
}
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()

默认返回
Task
Task<T>
。当方法经常同步完成(缓存命中、缓冲I/O)时,使用
ValueTask<T>
以避免
Task
分配开销。
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;
}
ValueTask规则:
  • 切勿多次
    await
    同一个
    ValueTask
  • 切勿在未完成的
    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()
)

1. 阻塞异步代码(
.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
.GetAwaiter().GetResult()
is in
Main()
pre-C# 7.1 or in rare infrastructure code where async is impossible (static constructors,
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()
仅在C# 7.1之前的
Main()
方法中,或极少数无法使用异步的基础设施代码(静态构造函数、
Dispose()
)中是安全的。

2.
async void

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);
}
The only valid use of
async void
is event handlers (WinForms, WPF, Blazor
@onclick
), where the framework requires a
void
return type.
async void
方法无法被等待,其中未处理的异常会导致进程崩溃。
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);
}
async void
唯一合法的使用场景是事件处理程序(WinForms、WPF、Blazor
@onclick
),因为框架要求返回
void
类型。

3. Missing
ConfigureAwait

3. 遗漏
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);
}
类库代码中,使用
ConfigureAwait(false)
避免捕获同步上下文。在应用程序代码(ASP.NET Core、控制台应用)中则不需要,因为不存在同步上下文。
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

5. 遗漏
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);
}

始终接收并传递
CancellationToken
,切勿静默丢弃。
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);
}

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
处理独立操作

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);
}
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
(.NET 6+)实现有限并行

csharp
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>
流处理

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;
    }
}

使用
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
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);
        }
    }
}

对于后台处理,使用
BackgroundService
(或
IHostedService
)而非
Task.Run
或即发即弃模式。注册模式请参考[skill:dotnet-csharp-dependency-injection]。
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);
        }
    }
}

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:
Note: This skill applies publicly documented guidance. It does not represent or speak for the named sources.
本技能中的异步模式基于以下公开内容:
注意: 本技能应用公开文档中的指导原则,不代表或代言上述来源。

References

参考资料