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

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

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

让我们想象一下我有一个方法,例如:

private void SendTheMail() { // Stuff } 

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

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

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

我的下一个想法是,对异步代表进行一些研究。 这看起来效果更好,因为在异步委托中处理exception(与上面的线程示例不同)。 但是,我担心的是我做错了还是我可能导致内存泄漏。

这就是我正在做的事情:

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

我这样做了吗?

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

为了回应理查德,虽然我看到线程解决方案可以工作(正如我的第一个例子中所示),只要我抓住错误……如果错误不是,那么IIS完全爆炸的事实仍然是可怕的。抓住了。 这告诉我,ASP.NET / IIS从来没有意味着你这样做…这就是为什么我倾向于继续使用.BeginInvoke / delegates而不是因为当出现问题时它不会弄乱IIS并且似乎在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."))); } } 

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

您使用.Net SmtpClient发送电子邮件吗? 它可以发送异步消息 。

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

线程在这里不是错误的选择,但是如果你自己没有处理exception,它会冒泡并崩溃你的进程。 你做哪个线程并不重要。

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

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

更好的是,如果可以的话,使用SmtpClient的异步function。 你仍然需要处理exception。

我甚至建议您查看.Net 4的新Parallet任务库。 它具有额外的function,可以让您处理exception情况,并与ASP.Net的线程池一起使用。

如果您使用.Net的SmtpClient和MailMessage类,您应该注意几件事。 首先,期望发送错误,因此陷阱和处理它们。 其次,在.Net 4中,这些类有一些变化,现在都实现了IDisposable(自3.5以来的MailMessage,4.0中的新的SmtpClient)。 因此,您应该使用块或显式处理来创建SmtpClient和MailMessage。 这是一些人们不知道的突破性变化。

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

在.NET 4.0下使用SmtpClient,SendAsync和Dispose的最佳实践是什么

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

我刚才正在思考同样的问题,我想我甚至不想在服务器回发请求中发起电子邮件发送。 服务网页背后的过程应该有兴趣尽快回复用户,你尝试做的工作越多越慢。

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

通过单独的模型,我们通常表示不同的对象模型,可能在不同的逻辑过程中运行

另外值得一读的是关于CQRS的维基百科文章( http://en.wikipedia.org/wiki/Command%E2%80%93query_separation )。 这个亮点的一个重点是:

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

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

这种方法的另一个好处是可以消除所有可能带来的所有multithreading问题和令人头疼的问题。

我为我的项目工作过同样的问题:

首先尝试Thread就像你一样:
– 我松散的背景
– exception处理问题
– 通常说, Thread在IIS ThreadPool上是个坏主意

所以我切换并asynchronously尝试:
– ‘异步’在asp.net Web应用程序中是fake 。 它只是放入队列调用并打开上下文

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

所以对于快速解决方案:从ajax方面做异步调用告诉用户fake是,但继续你的mvc控制器发送工作

使用这种方式 –

 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(""); str.Append(""); str.Append(""); str.Append("
"); str.Append(""); str.Append(""); //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 "; }

如果要检测泄漏,则需要使用如下所示的分析器:

http://memprofiler.com/

我没有看到你的解决方案有任何问题 ,但几乎可以保证你这个问题将被视为主观的。

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

祝好运!

马特