Langsung ke konten
~/rifqi_abrory

Halaman

Beranda
Tentang
Stack
Pengalaman
Proyek
Tulisan
Kontak
Semua proyek

Proyek

Avrist PINTAR — Agent Portal
Avrist e-Application
Avrist Academy — LMS CMS
Avrist e-Payment
PRUHub
SIYAP

Artikel

Angular signals untuk halaman admin sehari-hari
Typed reactive forms di Angular, tanpa any
Error API yang konsisten di ASP.NET Core dengan ProblemDetails
Meneruskan CancellationToken sampai ke lapisan terbawah di ASP.NET Core
Graceful shutdown untuk service HTTP di Go
Idempotency key: membuat API pembayaran aman untuk di-retry
Form kontak yang tahan spam dengan Next.js Server Actions
Form yang ramah offline di React Native: draft dan antrean submit
Validasi request dan penanganan error di Spring Boot 3
Keyset pagination di SQL Server: ketika OFFSET mulai lambat

Aksi

Ganti ke mode gelap
Read in English
Salin alamat email
Semua artikel

Meneruskan CancellationToken sampai ke lapisan terbawah di ASP.NET Core

Kenapa request HTTP yang dibatalkan seharusnya juga menghentikan query database-nya, dan kebiasaan kecil yang membuatnya terjadi.

3 menit baca

Di halaman ini

Pekerjaan yang tidak lagi ditunggu siapa pun

Seorang pengguna membuka halaman laporan, mulai tidak sabar, lalu pindah halaman. Browser menutup koneksi, tetapi server terus bekerja: controller masih menunggu query SQL yang berat, memetakan ribuan baris, dan menyerialisasi respons yang tidak akan dibaca siapa pun.

Saat sepi, hal ini tidak berbahaya. Saat beban tinggi, request yang ditinggalkan ikut berebut koneksi database dan waktu thread pool dengan request yang sungguhan. ASP.NET Core sebenarnya sudah tahu kapan client pergi, dan memberitahukannya lewat CancellationToken. Tugas kita adalah meneruskan token itu ke mana-mana.

Dari mana token berasal

Tambahkan parameter CancellationToken pada action, dan ASP.NET Core otomatis mengikatnya ke HttpContext.RequestAborted:

[HttpGet("reports/monthly")]
public async Task<IActionResult> Monthly(int year, int month, CancellationToken ct)
{
    var report = await reports.BuildMonthlyAsync(year, month, ct);
    return Ok(report);
}

Minimal API bekerja dengan cara yang sama:

app.MapGet("/reports/monthly", (int year, int month, IReportService reports, CancellationToken ct) =>
    reports.BuildMonthlyAsync(year, month, ct));

Teruskan ke semua yang di-await

Token hanya berguna kalau sampai ke kode yang benar-benar menunggu. Biasanya itu berarti EF Core, HttpClient, dan I/O lainnya:

public sealed class ReportService(AppDbContext db, HttpClient rates) : IReportService
{
    public async Task<MonthlyReport> BuildMonthlyAsync(int year, int month, CancellationToken ct)
    {
        var from = new DateTime(year, month, 1);
        var to = from.AddMonths(1);

        var premiums = await db.Payments
            .Where(p => p.PaidAt >= from && p.PaidAt < to)
            .GroupBy(p => p.ProductCode)
            .Select(g => new { Product = g.Key, Total = g.Sum(p => p.Amount) })
            .ToListAsync(ct);

        var fx = await rates.GetFromJsonAsync<FxRates>("rates/latest", ct);

        return MonthlyReport.Create(premiums, fx);
    }
}

Ketika token dibatalkan, EF Core meminta SQL Server membatalkan command yang sedang berjalan, dan HttpClient menghentikan panggilan keluarnya. Keduanya melempar OperationCanceledException.

Aturan sederhana supaya konsisten

  1. Setiap method async yang melakukan I/O menerima CancellationToken, biasanya sebagai parameter terakhir. Namai ct atau cancellationToken, dan konsisten di seluruh codebase.
  2. Jangan beri nilai default = default di kode aplikasi. Parameter default membuatnya mudah terlupa, dan compiler tidak akan mengingatkan lagi. Pengecualiannya adalah library.
  3. Jangan pernah mengoper CancellationToken.None hanya supaya bisa di-compile. Kalau sebuah panggilan memang tidak boleh dibatalkan, tulis komentar yang menjelaskan alasannya.
  4. Loop memeriksa token. Untuk loop yang berat di CPU, panggil ct.ThrowIfCancellationRequested() sekali per iterasi atau per batch.

Pekerjaan yang tidak boleh dibatalkan

Ada pekerjaan yang harus selesai walaupun pengguna sudah pergi, misalnya menulis catatan audit atau mencatat pembayaran yang sudah diterima provider. Jangan teruskan token request ke sana:

// The provider has already charged the customer; recording it must not depend on the browser.
await payments.MarkAsPaidAsync(paymentId, CancellationToken.None);

Untuk pekerjaan yang lebih panjang, serahkan ke background queue atau hosted service yang punya siklus hidup sendiri, bukan dijalankan di dalam request.

Jangan catat pembatalan sebagai error

Request yang dibatalkan itu normal, bukan bug. Kalau penanganan error global mencatat semua exception sebagai error, request yang ditinggalkan akan membanjiri log. Saring ketika client memang sudah pergi:

catch (OperationCanceledException) when (httpContext.RequestAborted.IsCancellationRequested)
{
    // Client disconnected; nothing to report.
}

Menambahkan timeout sendiri

Kadang Anda juga butuh batas waktu yang pasti, terlepas dari client. Gabungkan keduanya:

using var timeout = CancellationTokenSource.CreateLinkedTokenSource(ct);
timeout.CancelAfter(TimeSpan.FromSeconds(10));

var result = await slowService.RunAsync(timeout.Token);

Sekarang operasinya berhenti ketika client pergi atau setelah sepuluh detik, mana yang lebih dulu.

Ringkasan

  • ASP.NET Core memberikan RequestAborted secara gratis; terima sebagai parameter.
  • Teruskan ke EF Core, HttpClient, dan setiap panggilan I/O yang di-await.
  • Lewati token dengan sengaja hanya untuk pekerjaan yang wajib selesai, dan tulis alasannya.
  • Perlakukan pembatalan sebagai hal normal, bukan error di log.