Dispara y olvida el método asincrónico en asp.net mvc

Las respuestas generales, como aquí y aquí para ” disparar y olvidar” preguntas, no es utilizar async / Task.Run , sino usar Task.Run o TaskFactory.StartNew pasando el método síncrono.
Sin embargo, a veces el método que deseo disparar y olvidar es asincrónico y no hay un método de sincronización equivalente.

Nota / advertencia de actualización: como señaló Stephen Cleary a continuación, es peligroso continuar trabajando en una solicitud después de haber enviado la respuesta. La razón es porque AppDomain puede cerrarse mientras el trabajo todavía está en progreso. Vea el enlace en su respuesta para más información. De todos modos, solo quería señalarlo por adelantado, para que no envíe a nadie por el camino equivocado.

Creo que mi caso es válido porque el trabajo real lo realiza un sistema diferente (computadora diferente en un servidor diferente), así que solo necesito saber que el mensaje ha quedado para ese sistema. Si hay una excepción, no hay nada que el servidor o usuario pueda hacer al respecto y no afecta al usuario, todo lo que tengo que hacer es consultar el registro de excepciones y limpiarlo manualmente (o implementar algún mecanismo automatizado). Si el AppDomain está cerrado tendré un archivo residual en un sistema remoto, pero lo recogeré como parte de mi ciclo de mantenimiento habitual y dado que su existencia ya no es conocida por mi servidor web (base de datos) y su nombre es único. con marca de tiempo, no causará ningún problema mientras aún persista.

Sería ideal si tuviera acceso a un mecanismo de persistencia como Stephen Cleary señaló, pero desafortunadamente no lo hago en este momento.

Consideré simplemente fingir que la solicitud DeleteFoo se completó correctamente en el lado del cliente (javascript) mientras mantenía la solicitud abierta, pero necesito información en la respuesta para continuar, por lo que se mantendrían las cosas.

Entonces, la pregunta original …

por ejemplo:

 //External library public async Task DeleteFooAsync(); 

En mi código asp.net mvc, quiero llamar a DeleteFooAsync de una manera que no se puede olvidar: no quiero mantener la respuesta esperando que se complete DeleteFooAsync. Si DeleteFooAsync falla (o arroja una excepción) por alguna razón, no hay nada que el usuario o el progtwig puedan hacer al respecto, así que solo quiero registrar un error.

Ahora, sé que cualquier excepción dará lugar a excepciones no observadas, por lo que el caso más simple que puedo pensar es:

 //In my code Task deleteTask = DeleteFooAsync() //In my App_Start TaskScheduler.UnobservedTaskException += ( sender, e ) => { m_log.Debug( "Unobserved exception! This exception would have been unobserved: {0}", e.Exception ); e.SetObserved(); }; 

¿Hay algún riesgo al hacer esto?

La otra opción que puedo pensar es hacer mi propia envoltura, como por ejemplo:

 private void async DeleteFooWrapperAsync() { try { await DeleteFooAsync(); } catch(Exception exception ) { m_log.Error("DeleteFooAsync failed: " + exception.ToString()); } } 

y luego llamar a eso con TaskFactory.StartNew (probablemente envolviendo una acción asíncrona). Sin embargo, esto parece mucho código de envoltura cada vez que quiero llamar a un método asíncrono en una manera de olvidar.

Mi pregunta es, ¿cuál es la forma correcta de llamar a un método asíncrono en una manera de disparar y olvidar?

ACTUALIZAR:

Bueno, encontré lo siguiente en mi controlador (no es que la acción del controlador deba ser asíncrona porque hay otras llamadas asíncronas que están esperando):

 [AcceptVerbs( HttpVerbs.Post )] public async Task DeleteItemAsync() { Task deleteTask = DeleteFooAsync(); ... } 

provocó una excepción de la forma:

Excepción no controlada: System.NullReferenceException: referencia de objeto no establecida en una instancia de un objeto. en System.Web.ThreadContext.AssociateWithCurrentThread (BooleansetImpersonationContext)

Esto se discute aquí y parece que tiene que ver con el SynchronizationContext y ‘la tarea devuelta se transfirió a un estado de terminal antes de que se completara todo el trabajo asincrónico’.

Entonces, el único método que funcionó fue:

 Task foo = Task.Run( () => DeleteFooAsync() ); 

Mi comprensión de por qué esto funciona es porque StartNew recibe un nuevo hilo para que DeleteFooAsync funcione.

Tristemente, la sugerencia de Scott a continuación no funciona para manejar excepciones en este caso, porque foo ya no es una tarea DeleteFooAsync, sino más bien la tarea de Task.Run, por lo que no maneja las excepciones de DeleteFooAsync. Mi UnobservedTaskException finalmente recibe una llamada, por lo que al menos eso todavía funciona.

Entonces, supongo que la pregunta sigue en pie, ¿cómo se puede disparar y olvidar un método asíncrono en asp.net mvc?

En primer lugar, permítanme señalar que “disparar y olvidar” casi siempre es un error en las aplicaciones ASP.NET. “Dispara y olvida” es solo un enfoque aceptable si no te importa si DeleteFooAsync realmente se completa.

Si está dispuesto a aceptar esa limitación, tengo un código en mi blog que registrará las tareas con el tiempo de ejecución ASP.NET, y acepta tanto el trabajo síncrono como el asíncrono.

Puede escribir un método de contenedor único para registrar excepciones como tales:

 private async Task LogExceptionsAsync(Func code) { try { await code(); } catch(Exception exception) { m_log.Error("Call failed: " + exception.ToString()); } } 

Y luego uso el BackgroundTaskManager de mi blog como tal:

 BackgroundTaskManager.Run(() => LogExceptionsAsync(() => DeleteFooAsync())); 

Alternativamente, puede mantener TaskScheduler.UnobservedTaskException y simplemente TaskScheduler.UnobservedTaskException así:

 BackgroundTaskManager.Run(() => DeleteFooAsync()); 

A partir de .NET 4.5.2, puede hacer lo siguiente

 HostingEnvironment.QueueBackgroundWorkItem(async cancellationToken => await LongMethodAsync()); 

Pero solo funciona dentro del dominio ASP.NET

El método HostingEnvironment.QueueBackgroundWorkItem le permite progtwigr elementos de trabajo de fondo pequeños. ASP.NET realiza un seguimiento de estos elementos y evita que IIS finalice abruptamente el proceso de trabajo hasta que se completen todos los elementos de trabajo en segundo plano. Este método no se puede llamar fuera de un dominio de aplicación administrada de ASP.NET.

Más aquí: https://msdn.microsoft.com/en-us/library/ms171868(v=vs.110).aspx#v452

La mejor manera de manejarlo es usar el método ContinueWith y pasar la opción OnlyOnFaulted .

 private void button1_Click(object sender, EventArgs e) { var deleteFooTask = DeleteFooAsync(); deleteFooTask.ContinueWith(ErrorHandeler, TaskContinuationOptions.OnlyOnFaulted); } private void ErrorHandeler(Task obj) { MessageBox.Show(String.Format("Exception happened in the background of DeleteFooAsync.\n{0}", obj.Exception)); } public async Task DeleteFooAsync() { await Task.Delay(5000); throw new Exception("Oops"); } 

Donde puse mi buzón de mensajes pondría su registrador.