Controlador ASP.NET: Um módulo asynchronous ou manipulador concluído enquanto uma operação assíncrona ainda estava pendente

Eu tenho um controlador muito simples da ASP.NET MVC 4:

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); } } 

Quando inicio o projeto, vejo minha visualização e parece bem, mas quando atualizo a página, recebo o seguinte erro:

[InvalidOperationException: um módulo asynchronous ou manipulador concluído enquanto uma operação assíncrona ainda estava pendente.]

Por que isso acontece? Eu fiz alguns testes:

  1. Se removermos task = DownloadAsync(); do construtor e colocá-lo no método Index , ele funcionará bem sem os erros.
  2. Se usarmos outro return await Task.Factory.StartNew(() => { Thread.Sleep(3000); return "Give me an error"; }); corpo de DownloadAsync() return await Task.Factory.StartNew(() => { Thread.Sleep(3000); return "Give me an error"; }); vai funcionar corretamente.

Por que é impossível usar o método WebClient.DownloadStringTaskAsync dentro de um construtor do controlador?

No Async Void, ASP.Net e Count of Outstanding Operations , Stephan Cleary explica a raiz deste erro:

Historicamente, o ASP.NET oferece suporte a operações assíncronas limpas desde o .NET 2.0 por meio do EAP (Event-based Asynchronous Pattern), no qual componentes asynchronouss notificam o SynchronizationContext de sua boot e conclusão.

O que está acontecendo é que você está triggersndo o DownloadAsync dentro do seu construtor de class, onde dentro você await na chamada http assíncrona. Isso registra a operação assíncrona com o SynchronizationContext do ASP.NET. Quando seu HomeController retorna, ele vê que tem uma operação assíncrona pendente que ainda precisa ser concluída, e é por isso que ele gera uma exceção.

Se removermos task = DownloadAsync (); do construtor e colocá-lo no método Index, ele funcionará bem sem os erros.

Como expliquei acima, é porque você não tem mais uma operação assíncrona pendente durante o retorno do controlador.

Se usarmos outro retorno do corpo de DownloadAsync (), Task.Factory.StartNew(() => { Thread.Sleep(3000); return "Give me an error"; }); vai funcionar corretamente.

Isso porque o Task.Factory.StartNew faz algo perigoso no ASP.NET. Não registra a execução das tarefas com o ASP.NET. Isso pode levar a casos de borda em que uma recyclerview de pool é executada, ignorando completamente sua tarefa em segundo plano, causando uma anormal anormal. É por isso que você precisa usar um mecanismo que registra a tarefa, como HostingEnvironment.QueueBackgroundWorkItem .

É por isso que não é possível fazer o que você está fazendo, do jeito que você está fazendo. Se você realmente deseja que isso seja executado em um thread de segundo plano, em um estilo “fire-and-forget”, use HostingEnvironment (se você estiver no .NET 4.5.2) ou BackgroundTaskManager . Observe que, ao fazer isso, você está usando um thread de threadpool para executar operações de E / S assíncronas, o que é redundante e exatamente o que o I / O asynchronous com async-await tentativas de async-await supera.

Eu encontrei um problema relacionado. Um cliente está usando uma interface que retorna Task e é implementada com async.

No Visual Studio 2015, o método do cliente que é asynchronous e que não usa a palavra-chave await ao invocar o método não recebe nenhum aviso ou erro, o código é compilado corretamente. Uma condição de corrida é promovida para produção.

O ASP.NET considera ilegal iniciar uma “operação assíncrona” vinculada ao seu SynchronizationContext e retornar um ActionResult antes de todas as operações iniciadas serem concluídas. Todos os methods async se registram como “operação assíncrona” s, portanto, você deve garantir que todas essas chamadas que se ligam ao SynchronizationContext do ASP.NET sejam concluídas antes de retornar um ActionResult .

Em seu código, você retorna sem garantir que o DownloadAsync() seja executado até a conclusão. No entanto, você salva o resultado para o membro da task , portanto, garantir que isso seja concluído é muito fácil. Basta colocar a await task em await task em todos os seus methods de ação (após assíncrona-los) antes de retornar:

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

EDITAR:

Em alguns casos, talvez seja necessário chamar um método async que não deve ser concluído antes de retornar ao ASP.NET . Por exemplo, você pode querer inicializar preguiçosamente uma tarefa de serviço em segundo plano que deve continuar em execução após a conclusão da solicitação atual. Este não é o caso para o código do OP porque o OP quer que a tarefa seja concluída antes de retornar. No entanto, se você precisar iniciar e não aguardar uma tarefa, há uma maneira de fazer isso. Você simplesmente deve usar uma técnica para “escaping” do atual SynchronizationContext.Current .

  • ( não recomeçado ) Um recurso do Task.Run() é escaping do contexto de synchronization atual. No entanto, as pessoas recomendam contra usar isso no ASP.NET porque o threadpool do ASP.NET é especial. Além disso, mesmo fora do ASP.NET, essa abordagem resulta em uma alternância de contexto extra.

  • ( recomendado ) Uma maneira segura de escaping do contexto de synchronization atual sem forçar uma alternância de contexto extra ou incomodar imediatamente o conjunto de encadeamentos do ASP.NET é definir SynchronizationContext.Current como null , chamar seu método async e restaurar o valor original .

O método myWebClient.DownloadStringTaskAsync é executado em um thread separado e não é bloqueado. Uma solução possível é fazer isso com o manipulador de events DownloadDataCompleted para myWebClient e um campo de class 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(); } 

O método return async Task e ConfigureAwait(false) podem ser uma das soluções. Ele irá agir como async vazio e não continuar com o contexto de synchronization (contanto que você realmente não diga respeito ao resultado final do método)

Exemplo de notificação por email com anexo.

 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(); } }