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 junio de 2014
Microsoft ASP.NETHay muchos escenarios, y relativamente habituales, en los que es interesante lanzar una tarea desatendida, en segundo plano, desde una aplicación ASP.NET: manipulación de archivos, envío de mensajes, acceso a recursos externos costosos, procesos de colas, generación de archivos, etc.

En general, es un enfoque que puede interesarnos siempre que se trate de realizar un trabajo pesado, que no requiera intervención alguna por parte del usuario ni siquiera para darle feedback del progreso o terminación, y que pueda ejecutarse de forma independiente a las peticiones HTTP que acceden a la aplicación.

Imaginad una acción como la siguiente, donde se recibe un texto y se llama al método SendEmailToAllMyContacts() para hacérselo llegar, por ejemplo, a 1.000 contactos:
[HttpPost]
public ActionResult EmailEverybody(string text)
{
    SendEmailTo1000Contacts(text);
    return Content("Your message is being delivered");
}

private void SendEmailTo1000Contacts(string text)
{
    for (int i = 1; i <= 1000; i++)
    {
        Debug.WriteLine("Sending email #" + i);
        Task.Delay(50).Wait(); // We are doing something hard here
    }
}
Como podréis intuir, el usuario que inició la acción tendrá que esperar a que acabe todo el proceso para poder hacer otra cosa, pues su navegador estará bloqueado hasta que hayan sido enviados los mensajes. Pero fijaos que el proceso es totalmente desatendido y que ni hay feedback de progreso, ni el resultado de la operación aporta nada al usuario. Realmente, no hay motivo para poner a prueba la paciencia de nuestros usuarios de esta forma, salvo nuestra sed de venganza hacia ellos };-)

Este sería un caso ideal para usar tareas en segundo plano. La acción se limitaría a recoger el texto enviado por el cliente, registrar una tarea en background que es la que realizaría realmente el trabajo, y, sin esperar a que ésta se complete, retornaría el control al usuario indicándole que el envío está siendo procesado.

Conceptualmente sería algo así:
[HttpPost]
public ActionResult EmailEverybody(string text)
{
    StartBackgroundSendEmail(text);
    return Content("Your message is being delivered");
}
Sin embargo, en aplicaciones ASP.NET no es una tarea sencilla. Aunque crear y lanzar objetos Task o Thread es trivial y algo que podemos hacer directamente desde nuestros controladores o componentes de negocio, el problema que encontramos es que, al lanzar nosotros estas tareas, ASP.NET no es consciente de su existencia y esto provoca problemas si existen procesos de reciclado o parada del AppDomain en el que se ejecuta la aplicación, pudiendo dejar el sistema en estado inconsistente porque el proceso no será notificado y la parada podría ocurrir en cualquier momento de la ejecución.

Para simplificar estos casos, en .NET 4.5.2 se ha introducido un nuevo método estático en la clase HostingEnvironment llamado QueueBackgroundWorkItem(), que permite añadir tareas (objetos Task ) a una cola gestionada por el propio ASP.NET. A efectos prácticos, esto significa que si por cualquier motivo IIS debe reciclar la aplicación (por ejemplo, porque se ha modificado el web.config), estas tareas no serán eliminadas de forma violenta y se esperará a que completen su misión, eso sí, un máximo de 30 segundos.

Conceptualmente el enfoque es bastante simple. Estas tareas en segundo plano serán notificadas usando cancellation tokens cuando se esté solicitando su cierre; ya es responsabilidad nuestra implementar un cierre civilizado del proceso para asegurar que la aplicación queda en estado consistente. Por supuesto, si se produce un error catastrófico y el proceso de ASP.NET es eliminado de forma tajante, no seremos notificados.

QueueBackgroundWorkItem() es un método al que sólo podemos llamar desde aplicaciones ASP.NET que presenta dos sobrecargas, aunque en la práctica veremos que su uso es idéntico.
static void QueueBackgroundWorkItem(Action<CancellationToken> workItem)
static void QueueBackgroundWorkItem(Func<CancellationToken, Task> workItem)
La primera de ellas  permite indicar directamente un delegado sin retorno (Action) al que se le pasa el token de cancelación que usaremos para detectar cuándo debemos cancelar el proceso y cerrarlo de forma ordenada. Es la más indicada si la tarea en segundo plano está implementada en un método síncrono.

Volviendo al ejemplo anterior, podríamos implementarlo de la siguiente forma. Observad que hemos añadido el token de cancelación al método, y a través de él podemos detectar cuándo debemos finalizar la tarea:
[HttpPost]
public ActionResult EmailEverybody(string text)
{
    HostingEnvironment.QueueBackgroundWorkItem(
        token => SendEmailTo1000Contacts(text, token)
    );
    return Content("Your message is being delivered");
}

private void SendEmailTo1000Contacts(string text, CancellationToken token)
{
    for (int i = 1; i <= 1000 && !token.IsCancellationRequested; i++)
    {
        Debug.WriteLine("Sending email #" + i);
        Task.Delay(10, token).Wait();
    }
    if (token.IsCancellationRequested)
    {
        Debug.WriteLine("Task ending gracefully");
    }
}
La segunda sobrecarga permite indicar un delegado al que se enviará el cancellation token y retornará el objeto Task que representa a la tarea a ejecutar en segundo plano. Es la utilizada si la tarea se encuentra implementada en un método asíncrono, como en el siguiente ejemplo:
[HttpPost]
public ActionResult EmailEverybody(string text)
{
    HostingEnvironment.QueueBackgroundWorkItem(
        token => SendEmailTo1000ContactsAsync(text, token)
    );
    return Content("Your message is being delivered");
}

private async Task SendEmailTo1000ContactsAsync(
                               string text, CancellationToken token)
{
    for (int i = 1; i <= 1000 && !token.IsCancellationRequested; i++)
    {
        Debug.WriteLine("Sending email #" + i);
        await Task.Delay(10, token);
    }         
}
imagePor último, recordad que para usar esta característica tenemos que indicar en Visual Studio que el “target” de la aplicación será ASP.NET 4.5.2, bien en el momento de crearlo, o bien en las propiedades del proyecto.

Si no os aparece esta opción en el desplegable, probablemente necesitéis instalar .NET 4.5.2 developer pack. Pero antes, como siempre antes de comenzar a usar cualquier actualización de este tipo, no dejéis de leer el documento What’s new in .NET 4.5.2, y, en este caso en particular, ASP.NET 4.5.2 and EnableViewStateMac porque es un breaking change que os puede causar problemas.

Publicado en Variable not found.

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