Reputation: 4460
When a user registers on my website, I don't see why I need to make him "wait" for the smtp to go through so that he gets an activation email.
I decided I want to launch this code asynchronously, and it's been an adventure.
Lets imagine I have a method, such as:
private void SendTheMail() { // Stuff }
My first though.. was threading. I did this:
Emailer mailer = new Emailer();
Thread emailThread = new Thread(() => mailer.SendTheMail());
emailThread.Start();
This works... until I decided to test it for error-handling capability. I purposely broke the SMTP server address in my web.config and tried it. The scary result was that IIS basically BARFED with an unhandled exception error on w3wp.exe (it was a windows error! how extreme...) ELMAH (my error logger) did NOT catch it AND IIS was restarted so anyone on the website had their session erased. Completely unacceptable result!
My next thought, was to do some research on Asynchronous delegates. This seems to work better because exceptions are being handled within the asynch delegate (unlike the thread example above). However, i'm concerned if i'm doing it wrong or maybe I'm causing memory leaks.
Here's what i'm doing:
Emailer mailer = new Emailer();
AsyncMethodCaller caller = new AsyncMethodCaller(mailer.SendMailInSeperateThread);
caller.BeginInvoke(message, email.EmailId, null, null);
// Never EndInvoke...
Am I doing this right?
Upvotes: 20
Views: 35817
Reputation: 2801
So, why not have a separate poller/service which deals exclusively with sending emails? Thus, allowing your registration post-back to execute in only the time it takes to write to the database/message queue and delaying the sending of the email til the next polling interval.
I'm pondering the same issue just now and I'm thinking that I really don't want to even initiate the email sending within the server post back request. The process behind serving the web pages should be interested in getting a response back to the user ASAP, the more work you try to do the slower it will be.
Have a look at the Command Query Segregation Principal (http://martinfowler.com/bliki/CQRS.html). Martin Fowler explains that different models can be used in the command part of an operation than are used in the query part. In this scenario the command would be "register user", the query would be the activation email, using the loose analogy. The pertinent quote would probably be:
By separate models we most commonly mean different object models, probably running in different logical processes
Also worth a read is the Wikipedia article on CQRS (http://en.wikipedia.org/wiki/Command%E2%80%93query_separation). An important point which this highlights is:
it is clearly intended as a programming guideline rather than a rule for good coding
Meaning, use it where your code, program execution and programmer understanding would benefit. This being a good example scenario.
This approach has the added benefit of negating all the mufti-threading concerns and the headaches all that can bring.
Upvotes: 2
Reputation: 9516
As of .NET 4.5 SmtpClient implements async awaitable method
SendMailAsync
.
As a result, to send email asynchronously is as the following:
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);
}
}
Upvotes: 6
Reputation: 11
Use this way-
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 protected]")
{
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("[email protected]"));
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 ";
}
Upvotes: 1
Reputation: 4460
There was a lot of good advice that I upvoted here... such as making sure to remember to use IDisposable (i totally didn't know). I also realized how important it is to manually catch errors when in another thread since there is no context -- I have been working on a theory that I should just let ELMAH handle everything. Also, further exploration made me realize I was forgetting to use IDisposable on mailmessage, too.
In response to Richard, although I see that the threading solution can work (as suggested in my first example) as long as i'm catching the errors... there's still something scary about the fact that IIS completely explodes if that error isn't caught. That tells me that ASP.NET/IIS never meant for you to do that... which is why i'm leaning towards continuing to use .BeginInvoke/delegates instead since that doesn't mess up IIS when something goes wrong and seems to be more popular in ASP.NET.
In response to ASawyer, I was totally surprised that there was a .SendAsync built into the SMTP client. I played with that solution for a while, but it doesn't seem to do the trick for me. Although I can skip through the client of code that does SendAsync, the page still "waits" until the SendCompleted event is done. My goal was to have the user and the page move forward while the email is getting sent in the background. I have a feeling that I might still be doing something wrong... so if someone comes by this they might want to try it themselves.
Here's my full solution for how I sent emails 100% asynchronously in addition with ELMAH.MVC error logging. I decided to go with an expanded version of example 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.")));
}
}
Upvotes: 25
Reputation: 16259
If you are using .Net's SmtpClient and MailMessage classes, you should take note of a couple things. First, expect errors on the send, so trap and handle them. Second, in .Net 4 there were some changes to these classes, and both now implement IDisposable (MailMessage since 3.5, SmtpClient new in 4.0). Because of this, your creation of the SmtpClient and the MailMessage should be wrapped in using blocks or explicitly disposed. This is a breaking change some people are unaware of.
See this SO question for more info on disposing when using async sends:
What are best practices for using SmtpClient, SendAsync and Dispose under .NET 4.0
Upvotes: 4
Reputation: 7630
I worked same issue for my project:
First tried Thread
as you do:
- I loose context
- Exception handling problem
- Commonly said, Thread
are bad idea on IIS ThreadPool
So I switch and try with asynchronously
:
- 'asynchronously' is fake
in a asp.net web application. It just put queue calls and swicth the context
So I make windows service and retrive the values through sql table: happy end
So for quick solution: from ajax
side make async call tell the user fake
yes, but continue your sending job in your mvc controller
Upvotes: 1
Reputation: 1142
Threading isn't the wrong option here, but if you don't handle an exception yourself, it will bubble up and crash your process. It doesn't matter which thread you do that on.
So instead of mailer.SendTheMail() try this:
new Thread(() => {
try
{
mailer.SendTheMail();
}
catch(Exception ex)
{
// Do something with the exception
}
});
Better yet, use the asynchronous capabilities of the SmtpClient if you can. You'll still need to handle exceptions though.
I would even suggest you have a look at .Net 4's new Parallet Task library. That has extra functionality which lets you handle exceptional cases and works well with ASP.Net's thread pool.
Upvotes: 3
Reputation: 17808
Are you using the .Net SmtpClient to send email? It can send asynch messages already.
Edit - If Emailer mailer = new Emailer();
is not a wrapper over SmtpClient, this won't be so useful I imagine.
Upvotes: 3
Reputation: 24198
If you want to detect leaks, then you need to use a profiler like this one:
I don't see anything wrong with your solution, but can almost guarantee you that this question will be closed as subjective.
One other option is to use jQuery to make an ajax call to the server and spark the e-mail flow. That way, the UI is not locked up.
Good luck!
Matt
Upvotes: 0