Controlador ASP.NET: un módulo asincrónico o controlador completado mientras que una operación asíncrona estaba pendiente

Tengo un controlador ASP.NET MVC 4 muy simple:

public class HomeController : Controller { private const string MY_URL = "http://smthing"; private readonly Task task; public HomeController() { task = DownloadAsync(); } public ActionResult Index() { return View(); } private async Task DownloadAsync() { using (WebClient myWebClient = new WebClient()) return await myWebClient.DownloadStringTaskAsync(MY_URL) .ConfigureAwait(false); } } 

Cuando comienzo el proyecto, veo mi vista y se ve bien, pero cuando actualizo la página, aparece el siguiente error:

[InvalidOperationException: un módulo o controlador asíncrono completado mientras una operación asíncrona estaba pendiente.]

¿Por que sucede? Hice un par de pruebas:

  1. Si eliminamos task = DownloadAsync(); del constructor y ponerlo en el método Index funcionará bien sin los errores.
  2. Si utilizamos otra DownloadAsync() cuerpo de DownloadAsync() return await Task.Factory.StartNew(() => { Thread.Sleep(3000); return "Give me an error"; }); funcionará correctamente

¿Por qué es imposible usar el método WebClient.DownloadStringTaskAsync dentro de un constructor del controlador?

En Async Void, ASP.Net y Count of Outstanding Operations , Stephan Cleary explica la raíz de este error:

Históricamente, ASP.NET ha admitido operaciones asincrónicas limpias desde .NET 2.0 a través del Patrón asincrónico basado en eventos (EAP), en el que los componentes asíncronos notifican al SynchronizationContext su inicio y finalización.

Lo que está sucediendo es que estás ejecutando DownloadAsync dentro del constructor de tu clase, mientras que dentro esperas la llamada http asincrónica. Esto registra la operación asincrónica con ASP.NET SynchronizationContext . Cuando su HomeController regresa, ve que tiene una operación asíncrona pendiente que aún no se ha completado, y es por eso que genera una excepción.

Si eliminamos task = DownloadAsync (); del constructor y ponerlo en el método Index funcionará bien sin los errores.

Como expliqué anteriormente, eso se debe a que ya no tiene una operación asíncrona pendiente mientras regresa del controlador.

Si utilizamos otra descarga de cuerpo de DownloadAsync () espera Task.Factory.StartNew(() => { Thread.Sleep(3000); return "Give me an error"; }); funcionará correctamente

Eso es porque Task.Factory.StartNew hace algo peligroso en ASP.NET. No registra la ejecución de tareas con ASP.NET. Esto puede llevar a casos límite en los que se ejecuta el reciclaje de un grupo, ignorando completamente la tarea de fondo, lo que provoca un aborto anormal. Es por eso que debe usar un mecanismo que registre la tarea, como HostingEnvironment.QueueBackgroundWorkItem .

Es por eso que no es posible hacer lo que haces, como lo haces. Si realmente desea que esto se ejecute en un hilo de fondo, en un estilo de “fuego y olvídate”, utiliza HostingEnvironment (si estás en .NET 4.5.2) o BackgroundTaskManager . Tenga en cuenta que al hacer esto, está utilizando un subproceso de subprocesos para realizar operaciones asíncronas IO, que es redundante y lo que async IO con async-await intenta superar.

Me encontré con un problema relacionado. Un cliente está utilizando una interfaz que devuelve la tarea y se implementa con async.

En Visual Studio 2015, el método del cliente que es asíncrono y que no usa la palabra clave await cuando se invoca el método no recibe ninguna advertencia o error, el código se comstack limpiamente. Una condición de carrera se promueve a producción.

ASP.NET considera ilegal iniciar una “operación asincrónica” vinculada a su SynchronizationContext y devolver un ActionResult antes de que se ActionResult todas las operaciones iniciadas. Todos los métodos async registran a sí mismos como “operaciones asincrónicas”, por lo que debe asegurarse de que todas las llamadas que se unen al ASP.NET SynchronizationContext completen antes de devolver un ActionResult .

En su código, regresa sin asegurarse de que DownloadAsync() haya ejecutado hasta su finalización. Sin embargo, guarda el resultado en el miembro de la task , por lo que asegurarse de que esté completo es muy fácil. Simplemente ponga la await task en await task en todos sus métodos de acción (después de la asincronización) antes de regresar:

 public async Task IndexAsync() { try { return View(); } finally { await task; } } 

EDITAR:

En algunos casos, es posible que necesite llamar a un método async que no debe completarse antes de regresar a ASP.NET . Por ejemplo, es posible que desee inicializar con pereza una tarea de servicio en segundo plano que debería continuar ejecutándose después de que se complete la solicitud actual. Este no es el caso para el código del OP porque el OP quiere que la tarea se complete antes de regresar. Sin embargo, si necesita comenzar y no esperar una tarea, hay una manera de hacerlo. Simplemente debe usar una técnica para “escapar” del actual SynchronizationContext.Current .

  • ( no recomenzado ) Una característica de Task.Run() es escapar del contexto de sincronización actual. Sin embargo, las personas recomiendan no usar esto en ASP.NET porque el grupo de temas de ASP.NET es especial. Además, incluso fuera de ASP.NET, este enfoque da como resultado un cambio de contexto adicional.

  • ( recomendado ) Una forma segura de escapar del contexto de sincronización actual sin forzar un cambio de contexto adicional o molestar el grupo de hilos de ASP.NET inmediatamente es establecer SynchronizationContext.Current en null , llamar a su método async y luego restaurar el valor original .

El método myWebClient.DownloadStringTaskAsync se ejecuta en un subproceso independiente y no bloquea. Una posible solución es hacer esto con el controlador de eventos DownloadDataCompleted para myWebClient y un campo de clase SemaphoreSlim.

 private SemaphoreSlim signalDownloadComplete = new SemaphoreSlim(0, 1); private bool isDownloading = false; 

….

 //Add to DownloadAsync() method myWebClient.DownloadDataCompleted += (s, e) => { isDownloading = false; signalDownloadComplete.Release(); } isDownloading = true; 

 //Add to block main calling method from returning until download is completed if (isDownloading) { await signalDownloadComplete.WaitAsync(); } 

El método return async Task , y ConfigureAwait(false) pueden ser una de las soluciones. Actuará como un vacío asíncrono y no continuará con el contexto de sincronización (siempre y cuando no te preocupes por el resultado final del método)

Ejemplo de notificación por correo electrónico con datos adjuntos.

 public async Task SendNotification(string SendTo,string[] cc,string subject,string body,string path) { SmtpClient client = new SmtpClient(); MailMessage message = new MailMessage(); message.To.Add(new MailAddress(SendTo)); foreach (string ccmail in cc) { message.CC.Add(new MailAddress(ccmail)); } message.Subject = subject; message.Body =body; message.Attachments.Add(new Attachment(path)); //message.Attachments.Add(a); try { message.Priority = MailPriority.High; message.IsBodyHtml = true; await Task.Yield(); client.Send(message); } catch(Exception ex) { ex.ToString(); } }