mcpField
D
dotnet-csharp-async-patterns

dotnet-csharp-async-patterns

github.com/wshaddix开发86127 次安装35 周前更新
代码仓库

关于 dotnet-csharp-async-patterns

面向为 .NET 应用编写异步代码的开发者和编码 Agent,整理 async/await、取消传播及常见错误的实践。内容还涵盖并行处理、流式枚举、后台任务与异步测试。

主要功能

  1. 01async/await 使用规则
  2. 02CancellationToken 传播
  3. 03并行异步与有界并发
  4. 04IAsyncEnumerable 流式处理
  5. 05后台服务与异步测试

适用场景

  1. 01排查阻塞异步导致的死锁
  2. 02为异步操作传递取消信号
  3. 03并行加载多个独立结果
  4. 04使用后台服务处理队列任务

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/wshaddix/dotnet-skills --skill dotnet-csharp-async-patterns

安装后在 Claude Code 中输入/dotnet-csharp-async-patterns确认已加载。

详细介绍

dotnet-csharp-async-patterns

.NET 应用的 async/await 最佳实践。涵盖正确使用任务、传播取消,以及 AI Agent 生成异步代码时最常见的错误。

交叉参考:[skill:dotnet-csharp-dependency-injection],了解 IHostedService/BackgroundService 注册;[skill:dotnet-csharp-coding-standards],了解 Async 后缀命名;[skill:dotnet-csharp-modern-patterns],了解语言级功能。


核心规则

始终贯彻异步

异步调用链中的每个方法都必须是 async,并且要使用 await。混用同步和异步会导致死锁或线程池耗尽。

// 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
}
优先使用 Task 和 ValueTask

默认返回 Task 或 Task<T>。如果方法经常同步完成(例如命中缓存、缓冲 I/O),则使用 ValueTask<T> 来避免分配 Task。

// 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 规则:

  • 切勿对同一个 ValueTask 执行多次 await
  • 切勿对未完成的 ValueTask 使用 .Result 或 .GetAwaiter().GetResult()
  • 如果需要多次等待或传递它,请使用 .AsTask() 进行转换

Agent 常见陷阱

以下是 AI Agent 生成 C# 代码时最常见的异步错误。

1. 阻塞异步操作(.Result、.Wait()、.GetAwaiter().GetResult())
// 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

async void 方法无法被等待,其中未处理的异常会导致进程崩溃。

// 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. 缺少 ConfigureAwait

在库代码中,使用 ConfigureAwait(false) 来避免捕获同步上下文。在应用代码(ASP.NET Core、控制台应用)中则不需要,因为不存在同步上下文。

// 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. 未处理异常的即发即弃操作
// WRONG -- exception is silently swallowed
_ = SendEmailAsync(order);

// CORRECT -- use IHostedService or a background channel
await _backgroundQueue.EnqueueAsync(ct => SendEmailAsync(order, ct));

如果确实必须使用即发即弃操作,至少要记录异常:

_ = Task.Run(async () =>
{
    try
    {
        await SendEmailAsync(order);
    }
    catch (Exception ex)
    {
        _logger.LogError(ex, "Failed to send email for order {OrderId}", order.Id);
    }
});
5. 忘记 CancellationToken

始终接收并传递 CancellationToken。切勿静默丢弃它。

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

取消模式

创建关联令牌

将外部取消与超时合并:

public async Task<Result> ProcessWithTimeoutAsync(CancellationToken ct = default)
{
    using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
    cts.CancelAfter(TimeSpan.FromSeconds(30));

    return await DoWorkAsync(cts.Token);
}
响应取消
public async Task ProcessBatchAsync(IEnumerable<Item> items, CancellationToken ct = default)
{
    foreach (var item in items)
    {
        ct.ThrowIfCancellationRequested();
        await ProcessItemAsync(item, ct);
    }
}

并行异步

使用 Task.WhenAll 执行独立操作
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+)实现有界并行
await Parallel.ForEachAsync(items, new ParallelOptions
{
    MaxDegreeOfParallelism = 4,
    CancellationToken = ct
}, async (item, token) =>
{
    await ProcessItemAsync(item, token);
});

IAsyncEnumerable<T> 流式处理

使用 IAsyncEnumerable<T> 流式处理结果,而不是缓冲整个集合:

public async IAsyncEnumerable<Order> GetOrdersStreamAsync(
    [EnumeratorCancellation] CancellationToken ct = default)
{
    await foreach (var order in _dbContext.Orders.AsAsyncEnumerable().WithCancellation(ct))
    {
        yield return order;
    }
}

后台工作

对于后台处理,请使用 BackgroundService(或 IHostedService),而不要使用 Task.Run 或即发即弃模式。有关注册模式,请参阅 [skill:dotnet-csharp-dependency-injection]。

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

测试异步代码

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

知识来源

此技能中的异步模式基于以下公开内容:

**注意:**此技能采用了公开记录的指导原则,不代表也不代言上述来源。

参考资料

评论

支持 Markdown