抛出特定异常时,SmtpClient.SendMailAsync会导致死锁

reg*_*xen 16 c# asp.net-mvc smtpclient async-await asp.net-identity

我正在尝试基于VS2013项目模板中的示例AccountController为ASP.NET MVC5网站设置电子邮件确认.我已经实现了IIdentityMessageService使用SmtpClient,试图尽可能简单:

public class EmailService : IIdentityMessageService
{
    public async Task SendAsync(IdentityMessage message)
    {
        using(var client = new SmtpClient())
        {
            var mailMessage = new MailMessage("some.guy@company.com", message.Destination, message.Subject, message.Body);
            await client.SendMailAsync(mailMessage);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

调用它的控制器代码直接来自模板(由于我想排除其他可能的原因,因此将其解压缩为单独的操作):

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();
}
Run Code Online (Sandbox Code Playgroud)

但是,当邮件无法发送时,我会遇到奇怪的行为,但只有在一个特定的实例中,当主机无法访问时.示例配置:

<system.net>
    <mailSettings>
        <smtp deliveryMethod="Network">
            <network host="unreachablehost" defaultCredentials="true" port="25" />
        </smtp>
    </mailSettings>
</system.net>
Run Code Online (Sandbox Code Playgroud)

在这种情况下,请求似乎死锁,永远不会向客户端返回任何内容.如果邮件由于任何其他原因而无法发送(例如主机主动拒绝连接),则异常处理正常并且我得到YSOD.

查看Windows事件日志,似乎InvalidOperationException在同一时间范围内抛出了一个消息,"异步模块或处理程序在异步操作仍未完成时完成."; 如果我试图抓住SmtpException控制器并ViewResult在catch块中返回一个,我在YSOD中得到相同的消息.所以我认为await在任何一种情况下-ed操作都无法完成.

据我所知,我遵循SO上其他帖子中概述的所有async/await最佳实践(例如,当使用await/async时,HttpClient.GetAsync(...)永远不会返回),主要是"使用async/await all上升的方式".我也尝试过使用ConfigureAwait(false),没有任何改变.由于代码仅在抛出特定异常时死锁,我认为一般模式在大多数情况下都是正确的,但在内部发生的某些事情使得它在这种情况下不正确; 但由于我对并发编程很陌生,我觉得我可能错了.

有什么我做错了吗?我总是可以SmtpClient.Send()在SendAsync方法中使用同步调用(即.),但感觉这应该按原样工作.

nos*_*tio 14

尝试这个实现,只需使用client.SendMailExAsync而不是client.SendMailAsync.如果它有任何区别,请告诉我们:

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();
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

  • 那一个有效; 正如人们通常所期望的那样捕获异常,冒泡调用堆栈并获得YSOD.似乎有很多代码可以做一些看似简单的事情(!),但我可以看到它如何快速复杂化.无论如何标记为已接受,因为它确实解决了它.感谢你的帮助! (3认同)