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

问题描述 投票:0回答:2

我正在尝试基于 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("[email protected]", message.Destination, message.Subject, message.Body);
            await client.SendMailAsync(mailMessage);
        }
    }
}

调用它的控制器代码直接来自模板(提取到单独的操作中,因为我想排除其他可能的原因):

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

但是,当邮件无法发送时,我会遇到奇怪的行为,但仅在一种特定情况下,当主机因某种原因无法访问时。配置示例:

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

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

查看 Windows 事件日志,似乎在同一时间范围内抛出了

InvalidOperationException
,并显示消息“异步模块或处理程序已完成,而异步操作仍处于挂起状态。”;如果我尝试捕获控制器中的
SmtpException
并在 catch 块中返回
ViewResult
,我会在 YSOD 中收到相同的消息。所以我认为无论哪种情况,
await
操作都无法完成。

据我所知,我遵循 SO 上其他帖子中概述的所有异步/等待最佳实践(例如 HttpClient.GetAsync(...) 在使用等待/异步时永远不会返回),主要是“使用异步” /一路等待”。我也尝试过使用

ConfigureAwait(false)
,没有任何改变。由于代码仅在引发特定异常时才会死锁,因此我认为一般模式在大多数情况下都是正确的,但内部发生的某些事情使其在这种情况下不正确;但由于我对并发编程还很陌生,所以我感觉我可能是错的。

我做错了什么吗?我总是可以在 SendAsync 方法中使用同步调用(即

SmtpClient.Send()
),但感觉这应该按原样工作。

c# asp.net-mvc async-await asp.net-identity smtpclient
2个回答
15
投票

尝试这个实现,只需使用

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

0
投票

在 .Net 8 上,这并没有解决我的问题,而且我也没有看到抛出任何异常。

终于添加了

message.BodyEncoding = System.Text.Encoding.UTF8;

似乎解决了一些根本问题...

© www.soinside.com 2019 - 2024. All rights reserved.