Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Proper way to asynchronously send an email in ASP.NET... (am i doing it right?)

Tags:

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?

like image 853
Ralph N Avatar asked Jan 05 '12 18:01

Ralph N


2 Answers

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.")));
    }
}
like image 131
Ralph N Avatar answered Oct 19 '22 14:10

Ralph N


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);
    }
} 
like image 35
Boris Lipschitz Avatar answered Oct 19 '22 15:10

Boris Lipschitz