2015-02-04 11 views
14

Sto tentando di impostare la conferma dell'e-mail per un sito Web ASP.NET MVC5, in base all'esempio AccountController del modello di progetto VS2013. Ho implementato il IIdentityMessageService utilizzando SmtpClient, cercando di tenerlo il più semplice possibile:SmtpClient.SendMailAsync causa deadlock quando si genera un'eccezione specifica

public class EmailService : IIdentityMessageService 
{ 
    public async Task SendAsync(IdentityMessage message) 
    { 
     using(var client = new SmtpClient()) 
     { 
      var mailMessage = new MailMessage("[email protected]", message.Destination, message.Subject, message.Body); 
      await client.SendMailAsync(mailMessage); 
     } 
    } 
} 

Il codice di controllo che chiama esso è direttamente dal modello (estratto in un'azione separata dato che volevo escludere altre possibili provoca):

public async Task<ActionResult> TestAsyncEmail() 
{ 
    Guid userId = User.Identity.GetUserId(); 

    string code = await UserManager.GenerateEmailConfirmationTokenAsync(userId); 
    var callbackUrl = Url.Action("ConfirmEmail", "Account", new { userId = userId, code = code }, protocol: Request.Url.Scheme); 
    await UserManager.SendEmailAsync(userId, "Confirm your account", "Please confirm your account by clicking <a href=\"" + callbackUrl + "\">here</a>"); 

    return View(); 
} 

Tuttavia mi sto comportamento strano quando la posta non riesce a inviare, ma solo in un caso specifico, quando l'host è in qualche modo irraggiungibile. Esempio di configurazione:

<system.net> 
    <mailSettings> 
     <smtp deliveryMethod="Network"> 
      <network host="unreachablehost" defaultCredentials="true" port="25" /> 
     </smtp> 
    </mailSettings> 
</system.net> 

In tal caso, la richiesta sembra bloccata, non restituire mai nulla al client. Se la posta non riesce a inviare per qualsiasi altra ragione (ad esempio l'host rifiuta attivamente la connessione), l'eccezione viene gestita normalmente e io ottengo un YSOD.

Guardando i registri degli eventi di Windows, sembra che uno InvalidOperationException venga lanciato intorno allo stesso periodo di tempo, con il messaggio "Un modulo o un handler asincrono completato mentre un'operazione asincrona era ancora in sospeso."; Ottengo lo stesso messaggio in un YSOD se provo a catturare lo SmtpException nel controller e restituisco un ViewResult nel blocco catch. Quindi immagino che l'operazione await -ed non sia completata in entrambi i casi.

Per quanto posso dire, sto seguendo tutte le best practice asincrone/attese come delineato in altri post su SO (ad esempio HttpClient.GetAsync(...) never returns when using await/async), principalmente "usando async/attendo fino in fondo". Ho anche provato a utilizzare ConfigureAwait(false), senza modifiche. Dato che il codice si blocca solo se viene generata un'eccezione specifica, immagino che lo schema generale sia corretto per la maggior parte dei casi, ma qualcosa sta accadendo internamente che lo rende scorretto in quel caso; ma dato che sono abbastanza nuovo alla programmazione simultanea, ho la sensazione che potrei sbagliarmi.

C'è qualcosa che sto facendo male? Posso sempre usare una chiamata sincrona (ovvero SmtpClient.Send()) nel metodo SendAsync, ma sembra che questo dovrebbe funzionare così com'è.

+1

Date un'occhiata a [risposta di Stephen Cleary sulla cattura un'eccezione su un metodo void ('SendMailAsync')] (http://stackoverflow.com/a/7350534/ 209.259). I metodi Void asincroni sono a volte bambini problematici. –

+1

@ErikPhilips - Non vedo alcun metodo 'async void' nell'esempio (implementato o chiamato) - intendevi qualche riga particolare? –

+1

Come soluzione temporanea, puoi provare a risolvere manualmente l'host e fallire prima ... Guarda anche [l'origine] (http://referencesource.microsoft.com/#System/net/System/Net/mail/SmtpClient.cs, b9a40a3be18a4d58) per approfondimenti - si spera che sarebbe utile ... –

risposta

13

Prova questa implementazione, usa solo client.SendMailExAsync anziché client.SendMailAsync. Fateci sapere se fa alcuna differenza:

public static class SendMailEx 
{ 
    public static Task SendMailExAsync(
     this System.Net.Mail.SmtpClient @this, 
     System.Net.Mail.MailMessage message, 
     CancellationToken token = default(CancellationToken)) 
    { 
     // use Task.Run to negate SynchronizationContext 
     return Task.Run(() => SendMailExImplAsync(@this, message, token)); 
    } 

    private static async Task SendMailExImplAsync(
     System.Net.Mail.SmtpClient client, 
     System.Net.Mail.MailMessage message, 
     CancellationToken token) 
    { 
     token.ThrowIfCancellationRequested(); 

     var tcs = new TaskCompletionSource<bool>(); 
     System.Net.Mail.SendCompletedEventHandler handler = null; 
     Action unsubscribe =() => client.SendCompleted -= handler; 

     handler = async (s, e) => 
     { 
      unsubscribe(); 

      // a hack to complete the handler asynchronously 
      await Task.Yield(); 

      if (e.UserState != tcs) 
       tcs.TrySetException(new InvalidOperationException("Unexpected UserState")); 
      else if (e.Cancelled) 
       tcs.TrySetCanceled(); 
      else if (e.Error != null) 
       tcs.TrySetException(e.Error); 
      else 
       tcs.TrySetResult(true); 
     }; 

     client.SendCompleted += handler; 
     try 
     { 
      client.SendAsync(message, tcs); 
      using (token.Register(() => client.SendAsyncCancel(), useSynchronizationContext: false)) 
      { 
       await tcs.Task; 
      } 
     } 
     finally 
     { 
      unsubscribe(); 
     } 
    } 
} 
+3

Quella funziona; l'eccezione viene catturata come ci si aspetterebbe normalmente, fa scoppiare lo stack delle chiamate e ottengo un YSOD. Sembra un sacco di codice per fare qualcosa che sembra così semplice (!), Ma posso vedere come può complicarsi velocemente. Comunque segnare come accettato dal momento che lo risolve. Grazie per tutto il vostro aiuto! – regexen

+0

Felice che funzioni; è un tipico wrapper basato su 'Task' su [modello EAP] (https://msdn.microsoft.com/en-us/library/ms228969%28v=vs.110%29.aspx). Mi chiedo se funzionerebbe ancora * senza * 'attendere Task.Yield()', forse potresti provare. – Noseratio

+1

Sembra funzionare senza 'Task.Yield', secondo un test rapido. – regexen

Problemi correlati