CancellationToken en .NET: Guia Completa con Ejemplos en APIs
Aprende a usar CancellationToken en APIs .NET para mejor rendimiento, shutdown graceful y codigo responsivo con ejemplos reales.
Los CancellationTokens son una característica fundamental de programación async en .NET que frecuentemente se malinterpreta o se ignora. Si alguna vez has escrito código async en C#, probablemente has visto el parámetro CancellationToken y te has preguntado: “¿Realmente necesito esto?”
La respuesta corta: Sí. Los CancellationTokens mejoran el rendimiento, previenen fugas de recursos y hacen tus APIs más robustas. En este artículo, cubriremos qué son, cómo funcionan y cómo usarlos apropiadamente en APIs .NET.
¿Qué es un CancellationToken?
Un CancellationToken es un struct que permite la cancelación cooperativa de operaciones async. “Cooperativa” significa que el código puede verificar si se ha solicitado cancelación y detenerse voluntariamente.
El Problema Sin CancellationToken
Sin cancelación, una vez que inicias una operación async, no hay forma de detenerla tempranamente:
public async Task<List<Order>> GetOrdersAsync()
{
await Task.Delay(5000); // Simula operación lenta
return await _dbContext.Orders.ToListAsync();
}
Si el usuario navega fuera o cierra el navegador después de 1 segundo, la operación continúa ejecutándose durante los 4 segundos completos, desperdiciando recursos de base de datos y CPU.
La Solución: CancellationToken
public async Task<List<Order>> GetOrdersAsync(CancellationToken cancellationToken)
{
await Task.Delay(5000, cancellationToken); // Respeta cancelación
return await _dbContext.Orders.ToListAsync(cancellationToken);
}
Ahora, si se solicita cancelación, la operación se detiene tempranamente, liberando recursos inmediatamente.
Cómo Funcionan los CancellationTokens
Los CancellationTokens trabajan con CancellationTokenSource:
using var cts = new CancellationTokenSource();
var token = cts.Token;
// Inicia trabajo async
var task = DoWorkAsync(token);
// Solicita cancelación después de 2 segundos
cts.CancelAfter(TimeSpan.FromSeconds(2));
try
{
await task;
}
catch (OperationCanceledException)
{
Console.WriteLine("Operation was cancelled");
}
async Task DoWorkAsync(CancellationToken cancellationToken)
{
for (int i = 0; i < 10; i++)
{
cancellationToken.ThrowIfCancellationRequested(); // Lanza si se canceló
await Task.Delay(1000, cancellationToken);
Console.WriteLine($"Step {i + 1} completed");
}
}
Cómo Funciona
CancellationTokenSourcecrea y gestiona el token.- El token se pasa a métodos async.
cts.Cancel()octs.CancelAfter()señala cancelación.- El código verifica
cancellationToken.IsCancellationRequestedo llamaThrowIfCancellationRequested(). - Se lanza
OperationCanceledException, deteniendo la operación.
CancellationToken en APIs ASP.NET Core
ASP.NET Core automáticamente proporciona un CancellationToken que se dispara cuando el cliente desconecta (ej. cierra el navegador, timeout de red).
Ejemplo: API Endpoint
[ApiController]
[Route("api/[controller]")]
public class OrdersController : ControllerBase
{
private readonly AppDbContext _dbContext;
public OrdersController(AppDbContext dbContext)
{
_dbContext = dbContext;
}
[HttpGet]
public async Task<ActionResult<List<Order>>> GetOrders(CancellationToken cancellationToken)
{
var orders = await _dbContext.Orders
.Where(o => o.Status == "Pending")
.ToListAsync(cancellationToken);
return Ok(orders);
}
}
ASP.NET Core automáticamente inyecta el CancellationToken del request. Si el cliente desconecta, el token se cancela, y Entity Framework deja de ejecutar la query.
Beneficios
- Rendimiento: Deja de procesar requests abandonados.
- Recursos de base de datos: Cancela queries que ya no se necesitan.
- Escalabilidad: Libera threads para manejar otros requests.
Usando CancellationToken con Entity Framework Core
EF Core soporta CancellationToken en todos los métodos async:
public async Task<Order?> GetOrderByIdAsync(int id, CancellationToken cancellationToken)
{
return await _dbContext.Orders
.Include(o => o.Items)
.FirstOrDefaultAsync(o => o.Id == id, cancellationToken);
}
public async Task<int> CreateOrderAsync(Order order, CancellationToken cancellationToken)
{
_dbContext.Orders.Add(order);
return await _dbContext.SaveChangesAsync(cancellationToken);
}
Si se cancela el token:
- Queries: Se detienen en la base de datos.
- SaveChanges: Rollback de transacción, sin cambios aplicados.
CancellationToken con HttpClient
Al hacer requests HTTP, siempre pasa un CancellationToken para evitar requests colgados:
public async Task<WeatherData> GetWeatherAsync(string city, CancellationToken cancellationToken)
{
using var client = new HttpClient();
var response = await client.GetAsync(
$"https://api.weather.com/v1/current?city={city}",
cancellationToken
);
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<WeatherData>(cancellationToken);
}
Si el cliente desconecta o el request toma demasiado tiempo, el HttpClient aborta el request.
Combinando con Timeout
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
var weather = await GetWeatherAsync("London", cts.Token);
Esto cancela después de 5 segundos sin importar qué.
Linked CancellationTokens
Puedes combinar múltiples fuentes de cancelación:
public async Task<Data> FetchDataAsync(CancellationToken requestToken)
{
using var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(requestToken, timeoutCts.Token);
return await _httpClient.GetFromJsonAsync<Data>(
"https://api.example.com/data",
linkedCts.Token
);
}
Esto cancela si:
- El cliente desconecta (
requestToken), O - Pasan 10 segundos (
timeoutCts)
Mejores Prácticas
1. Siempre Acepta CancellationToken en Métodos Async
// ✅ BIEN
public async Task<List<User>> GetUsersAsync(CancellationToken cancellationToken)
{
return await _dbContext.Users.ToListAsync(cancellationToken);
}
// ❌ MAL
public async Task<List<User>> GetUsersAsync()
{
return await _dbContext.Users.ToListAsync();
}
2. Pasa el Token Hacia Abajo
public async Task<OrderSummary> GetOrderSummaryAsync(int orderId, CancellationToken cancellationToken)
{
var order = await GetOrderAsync(orderId, cancellationToken);
var items = await GetOrderItemsAsync(orderId, cancellationToken);
return new OrderSummary { Order = order, Items = items };
}
private async Task<Order> GetOrderAsync(int id, CancellationToken cancellationToken)
{
return await _dbContext.Orders.FindAsync(new object[] { id }, cancellationToken);
}
3. No Ignores OperationCanceledException
try
{
await DoWorkAsync(cancellationToken);
}
catch (OperationCanceledException)
{
// Esto es esperado cuando se cancela, no lo registres como error
_logger.LogInformation("Operation cancelled");
}
catch (Exception ex)
{
_logger.LogError(ex, "Unexpected error");
throw;
}
4. Usa CancellationToken.None Cuando No Hay Cancelación
Si llamas a un método que requiere un token pero no tienes uno:
var users = await GetUsersAsync(CancellationToken.None);
CancellationToken.None nunca se cancela.
5. No Crees Nuevos CancellationTokenSource Sin Necesidad
// ❌ MAL: Innecesario
public async Task DoSomethingAsync()
{
using var cts = new CancellationTokenSource();
await DoWorkAsync(cts.Token);
}
// ✅ BIEN: Acepta del llamante
public async Task DoSomethingAsync(CancellationToken cancellationToken)
{
await DoWorkAsync(cancellationToken);
}
Ejemplo Avanzado: Background Job con Cancelación Graceful
public class OrderProcessingService : BackgroundService
{
private readonly IServiceProvider _serviceProvider;
private readonly ILogger<OrderProcessingService> _logger;
public OrderProcessingService(IServiceProvider serviceProvider, ILogger<OrderProcessingService> logger)
{
_serviceProvider = serviceProvider;
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.LogInformation("Order processing service started");
while (!stoppingToken.IsCancellationRequested)
{
try
{
using var scope = _serviceProvider.CreateScope();
var dbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
var pendingOrders = await dbContext.Orders
.Where(o => o.Status == "Pending")
.ToListAsync(stoppingToken);
foreach (var order in pendingOrders)
{
if (stoppingToken.IsCancellationRequested)
break;
await ProcessOrderAsync(order, stoppingToken);
}
await Task.Delay(TimeSpan.FromSeconds(30), stoppingToken);
}
catch (OperationCanceledException)
{
_logger.LogInformation("Order processing cancelled");
break;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error processing orders");
await Task.Delay(TimeSpan.FromSeconds(60), stoppingToken);
}
}
_logger.LogInformation("Order processing service stopped");
}
private async Task ProcessOrderAsync(Order order, CancellationToken cancellationToken)
{
// Procesar lógica de orden
await Task.Delay(1000, cancellationToken);
order.Status = "Processed";
}
}
Cuando la aplicación se apaga, ASP.NET Core señala stoppingToken, permitiendo al servicio limpiar gracefully.
Conclusión
Los CancellationTokens son esenciales para escribir código .NET async robusto. Permiten:
- Mejor rendimiento: Detén trabajo innecesario tempranamente.
- Gestión de recursos: Libera connections de base de datos, sockets HTTP, etc.
- Graceful shutdown: Maneja el cierre de aplicación limpiamente.
- APIs responsivas: Responde a desconexiones de cliente inmediatamente.
Puntos clave:
- Siempre acepta
CancellationTokenen métodos async. - Pásalo a todos los métodos async que llames (EF Core, HttpClient, Task.Delay, etc.).
- Usa
CancellationTokenSource.CreateLinkedTokenSourcepara combinar múltiples condiciones de cancelación. - Maneja
OperationCanceledExceptionapropiadamente. - ASP.NET Core proporciona tokens automáticamente en endpoints de API.
Empieza a usar CancellationToken hoy — tu aplicación será más escalable, responsiva y robusta.