2015-02-04 7 views
14

Ich versuche, E-Mail-Bestätigung für eine ASP.NET MVC5-Website, basierend auf dem Beispiel AccountController von der VS2013-Projektvorlage einrichten. Ich habe die IIdentityMessageServiceSmtpClient mit implementiert, versucht, es zu halten so einfach wie möglich:SmtpClient.SendMailAsync verursacht Deadlock beim Auslösen einer bestimmten Ausnahme

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

Der Controller-Code, der es anruft, ist gerade aus der Vorlage (in einer separaten Aktion extrahiert, da wollte ich andere ausschließen möglich) verursacht:

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

aber ich merkwürdiges Verhalten bin immer, wenn die E-Mail zu senden, nicht aber nur in einem speziellen Fall, wenn der Host irgendwie nicht erreichbar ist. Beispiel config:

In diesem Fall scheint die Anforderung Deadlock, nie etwas an den Client zurückgeben. Wenn die E-Mail aus einem anderen Grund nicht gesendet wird (z. B. wenn der Host die Verbindung aktiv verweigert), wird die Ausnahme normal verarbeitet und ich erhalte eine YSOD.

Mit Blick auf die Windows-Ereignisprotokolle scheint es, dass ein InvalidOperationException um denselben Zeitrahmen geworfen wird, mit der Nachricht "Ein asynchrones Modul oder Handler wurde abgeschlossen, während ein asynchroner Vorgang noch ausstehend war."; Ich bekomme die gleiche Nachricht in einem YSOD, wenn ich versuche, die SmtpException in der Steuerung zu fangen und eine im catch-Block zurückgeben. Also ich denke, die await -ed Operation kann in beiden Fällen nicht abgeschlossen werden.

Soweit ich das beurteilen kann, befolge ich alle async/erwarten Best Practices wie in anderen Posts auf SO beschrieben (z. B. HttpClient.GetAsync(...) never returns when using await/async), hauptsächlich "mit async/warten den ganzen Weg". Ich habe auch versucht, ConfigureAwait(false), ohne Änderung zu verwenden. Da der Code nur dann Deadlocks auslöst, wenn eine bestimmte Ausnahme ausgelöst wird, stelle ich fest, dass das allgemeine Muster in den meisten Fällen korrekt ist, aber intern etwas passiert, was es in diesem Fall falsch macht; aber da ich ziemlich neu in der Programmierung bin, habe ich das Gefühl, dass ich falsch liegen könnte.

Gibt es etwas, was ich falsch mache? Ich kann immer einen synchronen Anruf (dh. SmtpClient.Send()) in der SendAsync-Methode verwenden, aber es fühlt sich so an, als ob das so funktionieren sollte.

+1

einen Blick auf Nehmen Sie [Stephen Cleary Antwort auf den Fang eine Ausnahme auf einem void-Methode ('SendMailAsync')] (http://stackoverflow.com/a/7350534/ 209259). Async Void Methods sind manchmal problematische Kinder. –

+1

@ErikPhilips - Ich sehe keine 'async void' Methoden in der Probe (entweder implementiert oder aufgerufen) - meinst du eine bestimmte Zeile? –

+1

Als Workaround können Sie versuchen, den Host manuell aufzulösen und früher zu scheitern ... Schauen Sie auch [die Quelle] (http://referencesource.microsoft.com/#System/net/System/Net/mail/SmtpClient.cs, b9a40a3be18a4d58) für Einblicke - hoffentlich wäre es hilfreich ... –

Antwort

13

Versuchen Sie diese Implementierung, verwenden Sie einfach client.SendMailExAsync anstelle von client.SendMailAsync. Lassen Sie uns wissen, ob es einen Unterschied macht:

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

Das funktioniert; die Ausnahme wird gefangen, wie man normalerweise erwartet, sprudelt den Call-Stack und ich bekomme ein YSOD. Scheint wie eine ganze Menge Code etwas zu tun, das so einfach zu sein scheint (!), Aber ich kann sehen, wie es schnell kompliziert werden kann. Irgendwie Markierung als akzeptiert, da es es löst. Danke für deine Hilfe! – regexen

+0

Froh, es funktioniert; Es ist ein typischer 'Task'-basierter Wrapper über [EAP-Muster] (https://msdn.microsoft.com/en-us/library/ms228969%28v=vs.110%29.aspx). Ich frage mich, ob es noch funktionieren würde * ohne * 'appear Task.Yield()', vielleicht könntest du es versuchen. – Noseratio

+1

Scheint nach einem kurzen Test ohne das 'Task.Yield' zu funktionieren. – regexen