Hola! Quiero comenzar el post con la siguiente pregunta:
Si haces una solicitud a tu Web Api desde un cliente REST (Postman por ejemplo) y luego la cancelas antes de que termine, ¿Qué sucede?

En pocas palabras, podría no pasar nada o podríamos estar desperdiciando recursos sin necesidad. Todo depende de cómo estés escribiendo los controladores.

En los próximos párrafos intentaré responder esa pregunta, y mediante ejemplos, veremos como solucionarlo.

Caso de uso

Analicemos el siguiente controlador MVC de una aplicación Web API regular. Es un controlador muy simple que simula una operación costosa. En este caso es un simple Task.Delay() de 5 y 10 segundos respectivamente.

[HttpGet]
public async Task<IActionResult> Get()
{
    _logger.LogInformation("1- Ready to start");

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

    _logger.LogInformation("2- Half of the work done");

    await Task.Delay(TimeSpan.FromSeconds(10));

    _logger.LogInformation("3- We finished!!!");

    return Ok();
}

Y ahora lo probaremos desde Postman. Para esto ejecutamos la solución y luego hacemos una petición a nuestra API. La idea es esperar los primeros 5 segundos y luego cancelar la petición desde Postman. La petición completa toma algo más de 15 segundos.

Cancelación de la solicitud
Cancelación de la solicitud

Observación

En la imagen anterior vemos sobre la izquierda Postman y sobre la derecha la consola de Visual Studio mostrando los logs de la API.

Algo muy interesante es que a pesar de haber cancelado la petición luego de los 5 segundos los logs siguen apareciendo en la consola. Como podemos apreciar, Postman efectivamente cancela la solicitud, pero la API parece ignorar la cancelación.

Si efectivamente estuviéramos realizando una operación costosa, cómo puede ser acceso a base de datos o comunicación con otros servicios, habríamos desperdiciados recursos procesando parte de una solicitud que ya no tiene sentido debido a que el cliente desistió de la misma.

Aspectos técnicos

Esta situación donde una tarea es cancelada o abortada es bastante frecuente en distintos tipos de aplicaciones (ya sean web, desktop o nativas) y el framework nos provee mecanismos para hacer la tarea mucho mas simple.

A su vez, Asp.Net esta preparado para aprovechar esos mecanismos y detectar cuando el cliente canceló la solicitud o la conexión ha sido interrumpida, es nuestra responsabilidad hacer uso de ellos.

CancellationTokens

Cada vez que una acción es ejecutada el framework inyectará una instancia de un CancellationToken enlazada al ciclo de vida de la petición. De esta forma podremos detectar si la la petición ha sido cancelada.

Podemos lanzar un excepción del tipo TaskCancelledException como en el siguiente ejemplo:

[HttpGet]
public IActionResult Get(CancellationToken token)
{
    token.ThrowIfCancellationRequested();

    return Ok();
}

O bien podemos validar si se ha solicitado su cancelación:

[HttpGet]
public IActionResult Get(CancellationToken token)
{
    if(token.IsCancellationRequested)
    {
        return BadRequest("Solicitud cancelada.");
    }

    return Ok();
}

Pasando CancellationTokens en la cadena de ejecución

En general, es común que los métodos que impliquen algún tipo de tarea asincrónica (I/O en general como comunicación con base de datos, manejos de streams, network, etc.) acepten un CancellationToken como último parámetro.

[HttpGet]
public async Task<IActionResult> Get(CancellationToken token)
{
    await Task.Delay(TimeSpan.FromSeconds(5), token);

    return Ok();
}

Modificando el controlador

Cómo vimos en el ejemplo anterior, el cambio necesario para manejar correctamente la cancelación de la solicitud solo requiere agregar un parámetro adicional CancellationToken a la acción del controlador y pasar ese token al método Task.Delay().

[HttpGet]
public async Task<IActionResult> Get(CancellationToken token)
{
    _logger.LogInformation("1- Ready to start");

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

    _logger.LogInformation("2- Half of the work done");

    await Task.Delay(TimeSpan.FromSeconds(10), token);

    _logger.LogInformation("3- We finished!!!");

    return Ok();
}

Esta vez el comportamiento será diferente:

Cancelación de la solicitud - Error
Cancelación de la solicitud – Error

Dos cosas interesantes podemos destacar:

  1. Por un lado la tarea efectivamente fue cancelada y la ejecución de la petición no continuo.
  2. Se produjo una excepción

TaskCancelledException

Cuando un CancellationToken recibe la notificación de cancelación arroja una excepción del tipo TaskCancelledException. Es nuestra responsabilidad manejar en forma correcta esta situación.

Es importante mencionar que la forma en la que manejamos las cancelaciones es completamente arbitraria y depende del tipo de software y reglas de negocio con las que estemos trabajando.

A fines prácticos para nuestro ejemplo, voy a utilizar un simple try/catch.

[HttpGet]
public async Task<IActionResult> Get(CancellationToken token)
{
    try
    {
        _logger.LogInformation("1- Ready to start");

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

        _logger.LogInformation("2- Half of the work done");

        await Task.Delay(TimeSpan.FromSeconds(10), token);

        _logger.LogInformation("3- We finished!!!");

        return Ok();
    }
    catch (TaskCanceledException)
    {
        _logger.LogInformation("Task was cancelled");

        return BadRequest();
    }
}
Cancelación de la solicitud - Éxito
Cancelación de la solicitud – Éxito

Consideraciones

Un aspecto muy importante a tener en cuenta es qué tipo de consecuencias puede generar la cancelación de la solicitud en términos de consistencia, ya que podríamos potencialmente truncar una operación en la mitad.

Por ejemplo, si estamos trabajando con Entity Framework Core es conveniente agrupar las operaciones dentro de una transacción y pasar el CancellationToken. CommitAsync() y RollbackAsync() también aceptan un CancellationToken.

using (var transaction = await _dbContext.Database.BeginTransactionAsync(token))
{
    // Modifying operations here

    // Commit if successful
    await transaction.CommitAsync(token);
    // Rollback otherwise
    await transaction.RollbackAsync(token);
}

Conclusión

Por un lado podemos concluir que es importante contemplar la idea de que la petición puede ser abortada o cancelada, y podríamos estar malgastando recursos innecesarios.

Por el otro lado y para suerte nuestra, el framework nos provee todos los mecanismos necesarios para que la tarea se fácil y sencilla mediante la utilización de CancellationTokens.