zukucode
主にWEB関連の情報を技術メモとして発信しています。

ASP.NET Core CancellationTokenを非同期処理へ渡す

CancellationTokenは、非同期処理へ中止の要求を伝えるための値です。

ASP.NET Coreでは、ブラウザが通信を中断した場合などに、実行中の処理へキャンセルを伝えられます。

今回はControllerでCancellationTokenを受け取り、データベース処理やHTTP通信へ渡す基本的な方法を紹介します。

キャンセルの目的は、処理を途中で強制終了させることではありません。呼び出し元が結果を待っていないと分かった時点で、データベース接続や外部通信などの不要な処理を協調的に止めることです。そのため、Controllerで受け取るだけでなく、実際に待機している処理まで同じトークンを渡します。

Controllerで受け取る

Actionの引数へCancellationTokenを追加します。

ProductsController.cs
[ApiController]
[Route("api/products")]
public sealed class ProductsController(ProductService service) : ControllerBase
{
    [HttpGet]
    public async Task<IActionResult> FindAll(CancellationToken cancellationToken)
    {
        var products = await service.FindAllAsync(cancellationToken);
        return Ok(products);
    }
}

ASP.NET Coreがリクエストに対応するトークンを自動で渡します。

ブラウザが接続を切った場合などに、IsCancellationRequestedtrueになります。

呼び出すメソッドへ渡す

Controllerで受け取るだけでは、下位の処理はキャンセルされません。

サービス、リポジトリ、データベース処理まで同じトークンを渡します。

ProductService.cs
public sealed class ProductService(ProductRepository repository)
{
    public Task<IReadOnlyList<Product>> FindAllAsync(
        CancellationToken cancellationToken)
    {
        return repository.FindAllAsync(cancellationToken);
    }
}
ProductRepository.cs
public sealed class ProductRepository(AppDbContext dbContext)
{
    public async Task<IReadOnlyList<Product>> FindAllAsync(
        CancellationToken cancellationToken)
    {
        return await dbContext.Products
            .AsNoTracking()
            .OrderBy(product => product.Name)
            .ToListAsync(cancellationToken);
    }
}

Entity Framework CoreのToListAsyncには、CancellationTokenを受け取る引数があります。

HttpClientへ渡す

外部APIを呼び出す場合も同様です。

WeatherClient.cs
public sealed class WeatherClient(HttpClient httpClient)
{
    public async Task<string> FindAsync(
        string city,
        CancellationToken cancellationToken)
    {
        var url = $"weather?city={Uri.EscapeDataString(city)}";
        using var response = await httpClient.GetAsync(url, cancellationToken);
        response.EnsureSuccessStatusCode();

        return await response.Content.ReadAsStringAsync(cancellationToken);
    }
}

キャンセル要求が届くと、対応している非同期メソッドは通常OperationCanceledExceptionを発生させます。

ループ処理でキャンセルを確認する

時間のかかる独自のループでは、定期的にキャンセルを確認します。

集計処理の例
public static long Sum(
    IEnumerable<int> values,
    CancellationToken cancellationToken)
{
    long total = 0;

    foreach (var value in values)
    {
        cancellationToken.ThrowIfCancellationRequested();
        total += value;
    }

    return total;
}

ThrowIfCancellationRequestedは、キャンセルされていればOperationCanceledExceptionを発生させます。

非常に短い処理で毎回確認すると余分な処理が増えるため、重い処理の区切りで確認します。

Task.Delayにも渡す

待機を含む処理では、Task.Delayにもトークンを渡します。

await Task.Delay(TimeSpan.FromSeconds(5), cancellationToken);

トークンを渡さない場合、キャンセルされても5秒間の待機が続きます。

キャンセルを通常のエラーとして記録しない

キャンセルは、通信障害やプログラムの不具合とは異なります。

キャンセルを区別する例
try
{
    await service.ExecuteAsync(cancellationToken);
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
    // 必要ならデバッグログを残し、そのまま上位へ伝える
    throw;
}

共通の例外処理で、キャンセルを未処理例外としてエラーログへ大量に記録しないようにします。

ただし、キャンセルを握りつぶして成功レスポンスを返すことも避けます。

CancellationToken.Noneとの使い分け

CancellationToken.Noneを渡すと、その処理にはキャンセルが伝わりません。

通常のリクエスト処理では、Controllerで受け取ったトークンをそのまま渡します。

複数の更新を行う途中でキャンセルされると、一部だけ保存される可能性があります。

まとめて成功または失敗させる必要がある更新は、データベーストランザクションの中で実行します。

CancellationTokenは、引数で受け取るだけでなく、実際の非同期処理まで渡すことで効果があります。


関連記事