在 ASP.NET 中异步发送电子邮件的正确方法...(我做得对吗?)

Posted

技术标签:

【中文标题】在 ASP.NET 中异步发送电子邮件的正确方法...(我做得对吗?)【英文标题】:Proper way to asynchronously send an email in ASP.NET... (am i doing it right?) 【发布时间】:2012-02-03 13:42:51 【问题描述】:

当用户在我的网站上注册时,我不明白为什么我需要让他“等待” smtp 通过以便他收到激活电子邮件。

我决定要异步启动这段代码,这是一次冒险。

假设我有一个方法,例如:

private void SendTheMail()  // Stuff 

我的第一个.. 是线程。我这样做了:

Emailer mailer = new Emailer();
Thread emailThread = new Thread(() => mailer.SendTheMail());
emailThread.Start();

这可行...直到我决定测试它的错误处理能力。我故意破坏了我的 web.config 中的 SMTP 服务器地址并尝试了它。可怕的结果是 IIS 基本上在 w3wp.exe 上出现未处理的异常错误(这是一个 Windows 错误!多么极端......) ELMAH(我的错误记录器)没有捕捉到它并且 IIS 被重新启动,所以网站上的任何人都有他们的会话被删除。完全不能接受的结果!

我的下一个想法是对异步委托进行一些研究。这似乎工作得更好,因为异常是在异步委托中处理的(与上面的线程示例不同)。但是,我担心我做错了,或者我可能会导致内存泄漏。

这就是我正在做的事情:

Emailer mailer = new Emailer();
AsyncMethodCaller caller = new AsyncMethodCaller(mailer.SendMailInSeperateThread);
caller.BeginInvoke(message, email.EmailId, null, null);
// Never EndInvoke... 

我这样做对吗?

【问题讨论】:

所以只是为了做到这一点:在 SendTheMail 中没有任何异常处理? 我刚开始做。在里面,我捕获异常并像这样调用 elmah 记录器: Elmah.ErrorLog.GetDefault(null).Log(new Error(e));这样就行了。 【参考方案1】:

我在这里提出了很多好的建议......例如确保记住使用 IDisposable(我完全不知道)。我还意识到在另一个线程中手动捕获错误是多么重要,因为没有上下文——我一直在研究一个理论,我应该让 ELMAH 处理所有事情。此外,进一步的探索让我意识到我也忘记在邮件消息上使用 IDisposable。

作为对 Richard 的回应,尽管我看到线程解决方案可以工作(正如我在第一个示例中所建议的那样),只要我发现错误……如果那样的话,IIS 完全爆炸的事实仍然有些可怕错误没有被捕获。这告诉我 ASP.NET/IIS 从来不打算让你这样做......这就是为什么我倾向于继续使用 .BeginInvoke/delegates ,因为当出现问题并且似乎在 ASP.NET 中更受欢迎。

作为对 ASawyer 的回应,我对 SMTP 客户端内置了一个 .SendAsync 感到非常惊讶。我玩了一段时间的那个解决方案,但它似乎对我没有用。尽管我可以跳过执行 SendAsync 的代码客户端,但页面仍然“等待”直到 SendCompleted 事件完成。我的目标是让用户和页面在后台发送电子邮件时向前移动。我有一种感觉,我可能仍然做错了什么......所以如果有人通过这个他们可能想自己尝试一下。

这是我如何 100% 异步发送电子邮件以及 ELMAH.MVC 错误日志记录的完整解决方案。我决定使用示例 2 的扩展版本:

public void SendThat(MailMessage message)

    AsyncMethodCaller caller = new AsyncMethodCaller(SendMailInSeperateThread);
    AsyncCallback callbackHandler = new AsyncCallback(AsyncCallback);
    caller.BeginInvoke(message, callbackHandler, null);


private delegate void AsyncMethodCaller(MailMessage message);

private void SendMailInSeperateThread(MailMessage message)

    try
    
        SmtpClient client = new SmtpClient();
        client.Timeout = 20000; // 20 second timeout... why more?
        client.Send(message);
        client.Dispose();
        message.Dispose();

        // If you have a flag checking to see if an email was sent, set it here
        // Pass more parameters in the delegate if you need to...
    
    catch (Exception e)
    
         // This is very necessary to catch errors since we are in
         // a different context & thread
         Elmah.ErrorLog.GetDefault(null).Log(new Error(e));
    


private void AsyncCallback(IAsyncResult ar)

    try
    
        AsyncResult result = (AsyncResult)ar;
        AsyncMethodCaller caller = (AsyncMethodCaller)result.AsyncDelegate;
        caller.EndInvoke(ar);
    
    catch (Exception e)
    
        Elmah.ErrorLog.GetDefault(null).Log(new Error(e));
        Elmah.ErrorLog.GetDefault(null).Log(new Error(new Exception("Emailer - This hacky asynccallback thing is puking, serves you right.")));
    

【讨论】:

如果您要手动调用 Dispose(而不是将代码包装在 Using 块中),那么它不应该在 finally 块中吗?无论如何,在 .NET 4.5 中只需使用等待的 SendMailAsync【参考方案2】:

从 .NET 4.5 开始,SmtpClient 实现了异步等待方法 SendMailAsync。 因此,异步发送邮件如下:

public async Task SendEmail(string toEmailAddress, string emailSubject, string emailMessage)

    var message = new MailMessage();
    message.To.Add(toEmailAddress);

    message.Subject = emailSubject;
    message.Body = emailMessage;

    using (var smtpClient = new SmtpClient())
    
        await smtpClient.SendMailAsync(message);
    
 

【讨论】:

不幸的是,此方法仍然冻结网页。 你使用.ConfigureAwait(false)吗?【参考方案3】:

如果您使用 .Net 的 SmtpClient 和 MailMessage 类,您应该注意几件事。首先,预计发送时会出现错误,因此请捕获并处理它们。其次,在 .Net 4 中对这些类进行了一些更改,并且现在都实现了 IDisposable(MailMessage 自 3.5 起,SmtpClient 在 4.0 中新增)。因此,您创建的 SmtpClient 和 MailMessage 应该使用块包装或显式处置。这是一些人不知道的重大变化。

有关使用异步发送时处置的更多信息,请参阅此 SO 问题:

What are best practices for using SmtpClient, SendAsync and Dispose under .NET 4.0

【讨论】:

谢谢。我想知道人们是如何同时进行 SendAsync 和 Dispose 的? 我认为您需要在您的 SendCompleted 回调中处理它。我在我的答案中添加了一个链接,其中显示了一个示例。【参考方案4】:

您是否使用 .Net SmtpClient 发送电子邮件? It can send asynch messages already.

编辑 - 如果 Emailer mailer = new Emailer(); 不是 SmtpClient 的包装器,我想这不会那么有用。

【讨论】:

我正在尝试这个...但页面仍在等待。我希望页面立即转到“感谢您注册!”页面,而电子邮件在后台发送。看起来 SendAsync 只工作了一半……我可以跳到下一行代码……但它会在转到下一页之前等待“SendCompleted”事件。 @RalphN 看看 Mathew 的基于 ajax 的选项。这对你有用。【参考方案5】:

线程在这里不是错误的选择,但是如果您自己不处理异常,它会冒泡并导致您的进程崩溃。您在哪个线程上执行此操作并不重要。

所以代替 mailer.SendTheMail() 试试这个:

new Thread(() =>  
  try 
  
    mailer.SendTheMail();
  
  catch(Exception ex)
  
    // Do something with the exception
  
);

如果可以,最好使用 SmtpClient 的异步功能。不过,您仍然需要处理异常。

我什至建议您看看 .Net 4 的新 Parallet Task 库。它具有额外的功能,可让您处理异常情况并与 ASP.Net 的线程池配合使用。

【讨论】:

【参考方案6】:

那么,为什么不使用专门处理发送电子邮件的单独轮询器/服务呢?因此,允许您的注册回发仅在写入数据库/消息队列所需的时间内执行,并将电子邮件的发送延迟到下一个轮询间隔。

我刚才在思考同样的问题,我在想我真的不想在服务器回发请求中启动电子邮件发送。为网页提供服务的过程应该对尽快将响应返回给用户感兴趣,您尝试做的工作越多,它就会越慢。

查看命令查询隔离主体 (http://martinfowler.com/bliki/CQRS.html)。 Martin Fowler 解释说,可以在操作的命令部分使用与查询部分不同的模型。在这种情况下,命令将是“注册用户”,查询将是激活电子邮件,使用松散的类比。相关的引用可能是:

我们通常所说的不同模型是指不同的对象模型,可能在不同的逻辑进程中运行

另外值得一读的是关于 CQRS (http://en.wikipedia.org/wiki/Command%E2%80%93query_separation) 的***文章。这里强调的一个重点是:

它显然是作为编程指南而不是良好编码的规则

意思是,在您的代码、程序执行和程序员理解会受益的地方使用它。这是一个很好的示例场景。

这种方法的额外好处是消除了所有多线程问题和所有可能带来的麻烦。

【讨论】:

这确实是最好的方法,虽然我不会写入数据库,而是使用消息队列。不过,基本架构是相同的。 @DanielMann,当然,答案会适当更新。 这是巨大的矫枉过正。 CQRS 是一个很大的话题,而不是应该被吹捧为解决发送电子邮件等简单问题的方法【参考方案7】:

我为我的项目解决了同样的问题:

首先像你一样尝试Thread: - 我失去了上下文 - 异常处理问题 - 通常说,Thread 在 IIS 线程池上是个坏主意

所以我切换并尝试使用asynchronously: - 在 asp.net Web 应用程序中,“异步”是 fake。它只是放置队列调用并切换上下文

所以我创建了 windows 服务并通过 sql 表检索值:happy end

所以为了快速解决:从ajax 端进行异步调用,告诉用户fake 是的,但在你的 mvc 控制器中继续你的发送工作

【讨论】:

“异步”是指我的第二个示例,是吗?【参考方案8】:

这样使用-

private void email(object parameters)
    
        Array arrayParameters = new object[2];
        arrayParameters = (Array)parameters;
        string Email = (string)arrayParameters.GetValue(0);
        string subjectEmail = (string)arrayParameters.GetValue(1);
        if (Email != "Email@email.com")
        
            OnlineSearch OnlineResult = new OnlineSearch();
            try
            
                StringBuilder str = new StringBuilder();
                MailMessage mailMessage = new MailMessage();

                //here we set the address
                mailMessage.From = fromAddress;
                mailMessage.To.Add(Email);//here you can add multiple emailid
                mailMessage.Subject = "";
                //here we set add bcc address
                //mailMessage.Bcc.Add(new MailAddress("bcc@site.com"));
                str.Append("<html>");
                str.Append("<body>");
                str.Append("<table width=720 border=0 align=left cellpadding=0 cellspacing=5>");

                str.Append("</table>");
                str.Append("</body>");
                str.Append("</html>");
                //To determine email body is html or not
                mailMessage.IsBodyHtml = true;
                mailMessage.Body = str.ToString();
                //file attachment for this e-mail message.
                Attachment attach = new Attachment();
                mailMessage.Attachments.Add(attach);
                mailClient.Send(mailMessage);
            

    


  protected void btnEmail_Click(object sender, ImageClickEventArgs e)
    
        try
        
            string To = txtEmailTo.Text.Trim();
            string[] parameters = new string[2];
            parameters[0] = To;
            parameters[1] = PropCase(ViewState["StockStatusSub"].ToString());
            Thread SendingThreads = new Thread(email);
            SendingThreads.Start(parameters);
            lblEmail.Visible = true;
            lblEmail.Text = "Email Send Successfully ";
        

【讨论】:

【参考方案9】:

如果你想检测泄漏,那么你需要使用像这样的分析器:

http://memprofiler.com/

我没有发现您的解决方案有任何错误,但几乎可以向您保证,这个问题将作为主观问题结束。

另一种选择是使用 jQuery 对服务器进行 ajax 调用并激发电子邮件流。这样,用户界面就不会被锁定。

祝你好运!

马特

【讨论】:

以上是关于在 ASP.NET 中异步发送电子邮件的正确方法...(我做得对吗?)的主要内容,如果未能解决你的问题,请参考以下文章

ASP.net 身份框架 - 重新发送确认电子邮件

在 ASP.NET 中发送大量电子邮件的最佳方式是啥?

ASP.Net:在 Page_Load 中调用异步方法

如何在 asp.net mvc 中验证 PayPal 企业电子邮件是不是正确

从 ASP.NET MVC 操作发送 HTTP 404 响应的正确方法是啥?

在 Asp.Net 中使用电子邮件发送忘记密码