Saltar al contenido

Artículos, tutoriales, trucos, curiosidades, reflexiones y links sobre programación web ASP.NET Core, MVC, Blazor, SignalR, Entity Framework, C#, Azure, Javascript... y lo que venga ;)

17 años online

el blog de José M. Aguilar

Inicio El autor Contactar

Artículos, tutoriales, trucos, curiosidades, reflexiones y links sobre programación web
ASP.NET Core, MVC, Blazor, SignalR, Entity Framework, C#, Azure, Javascript...

¡Microsoft MVP!
martes, 10 de octubre de 2017
ASP.NET CoreImaginad una aplicación ASP.NET Core MVC en la que insertamos un enlace o botón que dirige el navegador hacia la siguiente acción, que realiza una operación compleja y retorna un resultado:
public async Task<IActionResult> GetTheAnswerToLifeUniverseAndEverything()
{
    await Task.Delay(30000); // Simulando un proceso costoso...
    return Content("42!");
}
Cuando nuestros usuarios pulsen dicho botón, necesariamente habrán de esperar varios segundos para obtener una respuesta. Pero como suelen ser seres poseídos por una entidad demoníaca impacientes, lo normal es que se lancen en un feroz ataque contra el sistema, refrescando la página o pulsando repetidamente el botón de envío como si no hubiera un mañana. Todos hemos escuchado y sufrido en nuestras carnes una agresión de este tipo: “espera, esto parece que no va: click-click-click-click-click-click-click…

Obviamente, esto no hace sino empeorar las cosas. El servidor, que ya estaba ocupado intentando responder la primera petición, no tiene ya que atender a una, sino a todas las que se han generado tras este ataque, cuando en realidad no tiene sentido: para tranquilizar al usuario basta con entregarle el resultado de una de ellas, por lo que todos los hilos sobrantes simplemente están malgastando recursos del servidor realizando operaciones para obtener resultados que nadie va a consumir.

Estaría bien poder cancelar esas peticiones largas si tenemos la seguridad de que ningún cliente está esperándolas, ¿verdad?

Tokens de cancelación de peticiones en ASP.NET Core

En ASP.NET Core, todas las peticiones llevan asociadas un cancellation token, que es utilizado por el framework para indicar que el cliente que la originó ha sido desconectado. Esto ocurre, entre otros motivos, si el usuario pulsa el botón stop, escape, o cierra la pestaña o el navegador mientras está una petición en curso.
Ojo: si estás tras IIS, es él quien realiza la petición a Kestrel, por lo que la cancelación no llegará, al menos hasta que se solucione esta issue del módulo ASP.NET Core de Github.
Ese token de cancelación lo tenemos disponible en todas las peticiones, a través de la propiedad RequestAborted de la clase HttpContext, por lo que podemos consultarlo a nuestra voluntad para ver si la petición sigue estando activa o para cancelar tareas que ya no nos interesen. Por ejemplo, en el siguiente código vemos cómo un middleware que realiza una tarea completa (simulada mediante un delay) puede ser cancelado cuando la petición se cierre:
app.Use(async (ctx, next) =>
{
    try
    {
        // Simulamos una tarea costosa...
        await Task.Delay(5000, ctx.RequestAborted); 
        await next();
    }
    catch
    {
        logger.LogInformation("Request aborted!");
    }
});
Observad que cuando el token es cancelado porque la conexión con el cliente se ha perdido, la instrucción await Task.Delay() finaliza lanzando una excepción. Ese es el motivo de rodear la acción con un bloque try/catch.
A continuación se muestra el log por consola de una aplicación en la que hemos insertado el middleware anterior. Como se puede ver, primero hacemos una petición que dura 5 segundos, y luego pulsamos repetidas veces F5 de forma que lanzamos un ataque de peticiones, pero éstas van siendo canceladas sucesivamente hasta que la última finaliza con éxito:
Hosting environment: Development
Content root path: C:\Users\josem\source\repos\MyFirstAspNetCoreApplication\MyFirstAspNetCoreApplication
Now listening on: http://localhost:58842
Application started. Press Ctrl+C to shut down.

info: Microsoft.AspNetCore.Hosting.Internal.WebHost[1]
      Request starting HTTP/1.1 GET http://localhost:58842/
info: Microsoft.AspNetCore.Hosting.Internal.WebHost[2]
      Request finished in 5015.3097ms 404

info: Microsoft.AspNetCore.Hosting.Internal.WebHost[1]
      Request starting HTTP/1.1 GET http://localhost:58842/
info: MyFirstAspNetCoreApplication.Startup[0]
      Request aborted!
info: Microsoft.AspNetCore.Hosting.Internal.WebHost[2]
      Request finished in 1406.2337ms 0

info: Microsoft.AspNetCore.Hosting.Internal.WebHost[1]
      Request starting HTTP/1.1 GET http://localhost:58842/
info: MyFirstAspNetCoreApplication.Startup[0]
      Request aborted!
info: Microsoft.AspNetCore.Hosting.Internal.WebHost[2]
      Request finished in 620.9865ms 0

info: Microsoft.AspNetCore.Hosting.Internal.WebHost[1]
      Request starting HTTP/1.1 GET http://localhost:58842/
info: Microsoft.AspNetCore.Hosting.Internal.WebHost[2]
      Request finished in 5011.2962ms 404
Este token de cancelación podemos utilizarlo normalmente en llamadas de tinte asíncrono, como consultas a bases de datos o llamadas a un API externo:
var invoice = await context.Invoices.FirstOrDefaultAsync(
      invoice => invoice.Id == id, 
      ctx.RequestAborted
);
Y, por supuesto, en cualquier otro tipo de código como bucles, bifurcaciones, etc:
app.Use(async (ctx, next) =>
{
    for (int i = 0; i < 10 && !ctx.RequestAborted.IsCancellationRequested; i++)
    {
          logger.LogInformation($"Loop {i}/5");
          await Task.Delay(1000);
    }
});

¿Y en ASP.NET Core MVC?

ASP.NET Core MVCPues en el framework MVC es igual de sencillo, o incluso más :)

En primer lugar, el token de cancelación que en ASP.NET Core puro veíamos en HttpContext.RequestAborted sigue estando ahí. Es decir, nada impide que accedamos al contexto y lo utilicemos a nuestro antojo:
public Task<IActionResult> Get(int id)
{
    var invoice = await _invoiceServices
                             .GetByIdAsync(id, HttpContext.RequestAborted);
    if (invoice == null) 
    {
          return NotFound();
    }
    return Ok(invoice);
}
Sin embargo, aún se puede mejorar un poco. Si una acción es cancelable, basta con añadirle un parámetro de tipo CancellationToken, y el framework automáticamente le inyectará el valor de HttpContext.RequestAborted:
public Task<IActionResult> Get(int id, CancellationToken cancellationToken)
{
    var invoice = await _invoiceServices.GetByIdAsync(id, cancellationToken);
    if (invoice == null) 
    {
          return NotFound();
    }
    return Ok(invoice);
}
De esta forma aumentaremos la legibilidad del código, puesto que será más visible el hecho de que la acción sea cancelable, y al mismo tiempo facilitaremos las pruebas unitarias del método porque podremos inyectarle el token de forma manual y probar comportamientos más fácilmente.

Publicado en Variable not found.

Aún no hay comentarios, ¡sé el primero!