SmtpClient.SendAsync 阻塞了我的 ASP.NET MVC 请求

23

我有一个发送简单电子邮件的Action:

    [HttpPost, ActionName("Index")]
    public ActionResult IndexPost(ContactForm contactForm)
    {
        if (ModelState.IsValid)
        {
            new EmailService().SendAsync(contactForm.Email, contactForm.Name, contactForm.Subject, contactForm.Body, true);

            return RedirectToAction(MVC.Contact.Success());
        }
        return View(contactForm);
    }

还有一个邮件服务:

    public void SendAsync(string fromEmail, string fromName, string subject, string body, bool isBodyHtml)
    {
        MailMessage mailMessage....
        ....
        SmtpClient client = new SmtpClient(settingRepository.SmtpAddress, settingRepository.SmtpPort);

        client.EnableSsl = settingRepository.SmtpSsl;
        client.Credentials = new NetworkCredential(settingRepository.SmtpUserName, settingRepository.SmtpPassword);
        client.SendCompleted += client_SendCompleted;
        client.SendAsync(mailMessage, Tuple.Create(client, mailMessage));
    }

    private void client_SendCompleted(object sender, System.ComponentModel.AsyncCompletedEventArgs e)
    {
        Tuple<SmtpClient, MailMessage> data = (Tuple<SmtpClient, MailMessage>)e.UserState;
        data.Item1.Dispose();
        data.Item2.Dispose();

        if (e.Error != null)
        {

        }
    }
当我发送邮件时,使用Async方法,然后我的SendAsync方法立即返回,然后调用RedirectToAction。但是ASP.NET在client_SendCompleted完成之前不会发送响应(在这种情况下是重定向)。
我想要理解的是:当在Visual Studio调试器中观察执行时,SendAsync立即返回(并调用RedirectToAction),但在邮件发送完成之前,浏览器上什么都不会发生?
如果我在client_SendCompleted内设置断点,客户端会一直处于加载状态,直到我在调试器中按F5。
4个回答

27
这是有意设计的。如果异步工作是以调用底层同步上下文的方式启动的,ASP.NET将自动等待任何未完成的异步工作完成请求。这是为了确保如果您的异步操作尝试与HttpContextHttpResponse等进行交互时,它仍将存在。
如果您想要真正的fire & forget,您需要在ThreadPool.QueueUserWorkItem中包装您的调用。这将强制它在新的线程池线程上运行,而不通过SynchronizationContext,因此请求将愉快地返回。
请注意,如果由于任何原因应用程序域在发送仍在进行中时崩溃(例如,如果更改了web.config文件,将新文件放入bin,应用程序池被回收等),则您的异步发送将被突然中断。如果您关心这一点,请查看Phil Haacks为ASP.NET开发的WebBackgrounder,它允许您以确保在应用程序域关闭的情况下优雅地完成后台工作(如发送电子邮件)的方式排队和运行。

2
如果我使用 Task.Factory.StartNew(() => SendEmail(), TaskCreationOptions.LongRunning),我会遇到相同的问题吗? - Felipe Pessoto
我应该永远不要调用HostingEnvironment.UnregisterObject吗? - horgh

6
这是一个有趣的问题。我已经重现了这种意外行为,但我无法解释它。我会继续探究。
无论如何,解决方案似乎是将后台线程排队,这有点违背了使用SendAsync的目的。你最终会得到这样的结果:
MailMessage mailMessage = new MailMessage(...);
SmtpClient client = new SmtpClient(...);
client.SendCompleted += (s, e) =>
                            {
                                client.Dispose();
                                mailMessage.Dispose();
                            };

ThreadPool.QueueUserWorkItem(o => 
    client.SendAsync(mailMessage, Tuple.Create(client, mailMessage))); 

这可能会成为:

ThreadPool.QueueUserWorkItem(o => {
    using (SmtpClient client = new SmtpClient(...))
    {
        using (MailMessage mailMessage = new MailMessage(...))
        {
            client.Send(mailMessage, Tuple.Create(client, mailMessage));
        }
    }
}); 

我把这个Bug发送到了Microsoft Connect,或许你可以通过投票并点击“I can reproduce too”来帮忙 https://connect.microsoft.com/VisualStudio/feedback/details/688210/smtpclient-sendasync-blocking-my-asp-net-mvc-request - Felipe Pessoto

1
使用 .Net 4.5.2,您可以使用 ActionMailer.Net 进行此操作:
        var mailer = new MailController();
        var msg = mailer.SomeMailAction(recipient);

        var tcs = new TaskCompletionSource<MailMessage>();
        mailer.OnMailSentCallback = tcs.SetResult;
        HostingEnvironment.QueueBackgroundWorkItem(async ct =>
        {
            msg.DeliverAsync();
            await tcs.Task;
            Trace.TraceInformation("Mail sent to " + recipient);
        });

请先阅读:http://www.hanselman.com/blog/HowToRunBackgroundTasksInASPNET.aspx


0

我不知道这是否相关,但我也遇到了SendAsync阻塞我的MVC 3请求的问题。也许我没有正确配置代码?在谷歌搜索中,我找到了Jeff Widmer的博客,在那里他演示了异步调用Send的方式。对我来说,他的例子是一个很好的起点,比与SendAsync搏斗更简单。 http://weblogs.asp.net/jeffwids/archive/2009/10/12/asynchronously-sending-a-system-net-mail-mailmessage-in-c.aspx?CommentPosted=true#commentmessage - Arnold
他正在做的是一种通用的异步处理方法。问题是,为什么 SendAsync 方法会阻塞请求?框架中的某些内容可能有问题。 - Felipe Pessoto
1
我同意,至少在我的框架中一定有问题。然而,我并没有看到这个问题。电子邮件确实被发送了,但是我的控制器操作中的重定向被忽略了。我还没有看到SendAsync()的完整示例。使用通用方法并明确指定WaitOne()和Dispose()不会引起任何问题。如果我在SendAsync()方面存在配置问题,我也不会感到惊讶。谢谢。 - Arnold

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接