TGViewer
.NET Разработчик .NET Разработчик @netdeveloperdiary · 6.74K subscribers
Post #3277 1.63K
День 2735. #ЗаметкиНаПолях #Cancellation
Токен Отмены: Ошибки, Которые Незаметно Приводят к Проблемам. Начало

Пользователь закрывает вкладку во время выполнения запроса. Клиент отключается. На панели управления этот запрос должен отображаться как отменённый в течение миллисекунд. Вместо этого он продолжает выполняться ещё секунды — обращается к БД, API, записывает результат, который никто никогда не прочтёт. При тысячах прерванных запросов в день вы платите за вычислительные ресурсы, которые никому не служат. Это чаще всего токен отмены, который корректно создаётся в начале конвейера обработки запросов, а затем незаметно перестаёт передаваться дальше. ASP.NET Core предоставляет возможность отмены практически бесплатно. Токен интегрирован в конвейер, и фреймворк отменяет его в момент отключения клиента. Все ошибки происходят в коде, который забывает о существовании токена.

Каждый HTTP-запрос в ASP.NET Core содержит токен, который вы можете запросить напрямую:
app.MapGet("/reports/{id}", async (int id, HttpContext http, ReportService service) =>
{
// Этот токен отменяется при отключении клиента
// или на старте процедуры завершения работы сервера
CancellationToken ct = http.RequestAborted;
var report = await service.BuildReportAsync(id, ct);
return Results.Ok(report);
});

Редко требуется явно использовать HttpContext.RequestAborted — обработчики в минимальных API и методы-действия в MVC могут просто принимать параметр CancellationToken, и фреймворк автоматически привязывает RequestAborted к нему:
app.MapGet("/reports/{id}", async (int id, ReportService service, CancellationToken ct) =>
{
var report = await service.BuildReportAsync(id, ct);
return Results.Ok(report);
});

Вот и весь контракт: токен передаётся вам, а ваша задача — передать его каждому ожидаемому вызову ниже. Описанные ниже ошибки — это варианты нарушения этой цепочки.

Ошибка 1: «Проглатывание» токена на уровне сервиса
Наиболее распространённое нарушение - сигнатура метода сервиса игнорирует параметр:
public class ReportService
{
// …

public async Task<Report> BuildReportAsync(int id)
{
var order = await _db.Orders.FindAsync(id);
var pricing = await _prcClient.GetAsync($"/price/{id}");
return Map(order, pricing);
}
}

Здесь ничего не вызывает ошибок. На код-ревью всё выглядит нормально, если только вы специально это не проверяете. Но с этого момента запрос становится неотменяемым — клиент может исчезнуть, a запрос к БД и HTTP-вызов всё равно завершатся. Верный вариант:
public async Task<Report> BuildReportAsync(
int id, CancellationToken ct)
{
var order = await _db.Orders.FindAsync(new object[] { id }, ct);
var pricing = await _prcClient.GetAsync($"/price/{id}", ct);
return Map(order, pricing);
}

Правило: если метод ожидает чего-либо, он обязан принимать CancellationToken. Анализатор, вроде CA2016 (часть встроенных анализаторов .NET), отметит большинство таких случаев — включите его как предупреждение, а не просто как рекомендацию.

Ошибка №2: Task.Run без токена или с неправильным токеном
Task.Run имеет две перегрузки, и легко вызвать ту, которая делает вашу работу неотменяемой:
public async Task<byte[]> GenerateExportAsync(
int reportId, CancellationToken ct)
{
return await Task.Run(() => BuildReport(reportId), ct);
}

Передача ct в Task.Run останавливает запуск задачи только в том случае, если она уже отменена, но ничего не делает, если делегат уже запущен, т.к. BuildReport не может отслеживать отмену. Решение – передавать токен в исполняемый метод, а не только в Task.Run:
public async Task<byte[]> GenerateExportAsync(
int reportId, CancellationToken ct)
{
return await Task.Run(() => BuildReport(reportId, ct), ct);
}

private byte[] BuildReport(
int id, CancellationToken ct)
{
for (int page = 0; page < TotalPages(id); page++)
{
// периодически проверяем отмену
ct.ThrowIfCancellationRequested();
RenderPage(id, page);
}
…
}

Для циклов, интенсивно использующих процессор, вызывайте ct.ThrowIfCancellationRequested() между итерациями, а не только один раз в начале. Цикл, который проверяет токен один раз, а затем работает десять секунд, не может быть корректно отменён.

Продолжение следует…

Источник:
https://thecodeman.net/posts/cancellation-tokens-in-aspnet-core-mistakes
  • 👍 21
More from @netdeveloperdiary
  1. Sep 26, 2026День 2796. #ЗаметкиНаПолях #AI Рабочий процесс с Copilot для .NET. Продолжение Начало Три…
  2. Sep 25, 2026День 2795. #ЗаметкиНаПолях #AI Рабочий процесс с Copilot для .NET. Начало Проблема с позиц…
  3. Sep 24, 2026День 2794. #Оффтоп #Здоровье Сегодня будет необычный пост. Завтра в Москве стартует конфер…
  4. Sep 23, 2026День 2793. #ЗаметкиНаПолях #SQL 10 Редких Возможностей SQL, Которые Стоит Знать Каждому. Ч…
  5. Sep 22, 2026День 2792. #ЗаметкиНаПолях #SQL 10 Редких Возможностей SQL, Которые Стоит Знать Каждому. Ч…
  6. Sep 21, 2026🔍Тестовое собеседование с Senior C# разработчиком уже завтра 22 сентября(уже завтра!) в 1…
Threads Profile ViewerView any public Threads profile without an account.Open ThreadLook →Writing with AI? Make it sound human.Metric37 rewrites AI drafts so they read naturally. Free AI detector, 1,500 words free.Try Metric37 →