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がリクエストに対応するトークンを自動で渡します。
ブラウザが接続を切った場合などに、IsCancellationRequestedがtrueになります。
呼び出すメソッドへ渡す
Controllerで受け取るだけでは、下位の処理はキャンセルされません。
サービス、リポジトリ、データベース処理まで同じトークンを渡します。
ProductService.cspublic sealed class ProductService(ProductRepository repository)
{
public Task<IReadOnlyList<Product>> FindAllAsync(
CancellationToken cancellationToken)
{
return repository.FindAllAsync(cancellationToken);
}
}ProductRepository.cspublic 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.cspublic 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は、引数で受け取るだけでなく、実際の非同期処理まで渡すことで効果があります。