23

I have a Action that sends a simple email:

    [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);
    }

And a email service:

    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)
        {

        }
    }

When I send a email, I am using Async method, then my method SendAsync return immediately, then RedirectToAction is called. But the response(in this case a redirect) isn´t sent by ASP.NET until client_SendCompleted is completed.

Here's what I'm trying to understand:

When watching the execution in Visual Studio debugger, the SendAsync returns immediately (and RedirectToAction is called), but nothing happens in the browser until email is sent?

If i put a breakpoint inside client_SendCompleted, the client stay at loading.... until I hit F5 at debugger.

justSteve
  • 5,444
  • 19
  • 72
  • 137
Felipe Pessoto
  • 6,855
  • 10
  • 42
  • 73

4 Answers4

27

This is by design. ASP.NET will automatically wait for any outstanding async work to finish before finishing the request if the async work was kicked off in a way that calls into the underlying SynchronizationContext. This is to ensure that if your async operation tries to interact with the HttpContext, HttpResponse, etc. it will still be around.

If you want to do true fire & forget, you need to wrap your call in ThreadPool.QueueUserWorkItem. This will force it to run on a new thread pool thread without going through the SynchronizationContext, so the request will then happily return.

Note however, that if for any reason the app domain were to go down while your send was still in progress (e.g. if you changed the web.config file, dropped a new file into bin, the app pool recycled, etc.) your async send would be abruptly interrupted. If you care about that, take a look at Phil Haacks WebBackgrounder for ASP.NET, which let's you queue and run background work (like sending an email) in such a way that will ensure it gracefully finishes in the case the app domain shuts down.

Dhwani
  • 7,484
  • 17
  • 78
  • 139
Damian Edwards
  • 7,289
  • 1
  • 25
  • 19
  • 2
    if I use Task.Factory.StartNew(() => SendEmail(), TaskCreationOptions.LongRunning) I will have the same problem? – Felipe Pessoto Feb 23 '12 at 22:46
  • [Should I never call HostingEnvironment.UnregisterObject?](http://stackoverflow.com/questions/16096378/should-i-never-call-hostingenvironment-unregisterobject) – horgh Apr 19 '13 at 02:52
6

This is an interesting one. I've reproduced the unexpected behaviour, but I can't explain it. I'll keep digging.

Anyway the solution seems to be to queue a background thread, which kind of defeats the purpose in using SendAsync. You end up with this:

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))); 

Which may as well become:

ThreadPool.QueueUserWorkItem(o => {
    using (SmtpClient client = new SmtpClient(...))
    {
        using (MailMessage mailMessage = new MailMessage(...))
        {
            client.Send(mailMessage, Tuple.Create(client, mailMessage));
        }
    }
}); 
TheCodeKing
  • 19,064
  • 3
  • 47
  • 70
  • I sent the bug to Microsoft Connect, maybe you can help by voting and clicking at "I can reproduce too" https://connect.microsoft.com/VisualStudio/feedback/details/688210/smtpclient-sendasync-blocking-my-asp-net-mvc-request – Felipe Pessoto Sep 13 '11 at 22:21
1

With .Net 4.5.2, you can do this with 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);
        });

Please read this first: http://www.hanselman.com/blog/HowToRunBackgroundTasksInASPNET.aspx

malix
  • 3,566
  • 1
  • 31
  • 41
0

I sent the bug to Microsoft Connect https://connect.microsoft.com/VisualStudio/feedback/details/688210/smtpclient-sendasync-blocking-my-asp-net-mvc-request

Felipe Pessoto
  • 6,855
  • 10
  • 42
  • 73
  • I don't know if this is apropo, but I've been having issues with SendAsync blocking my MVC 3 request, too. Maybe I'm not configuring the code correctly? Googling, I came across Jeff Widmer's Blog, where he demonstrates calling Send asynchronously. His example is a great starting point and, for me, simpler than wrestling with SendAsync. http://weblogs.asp.net/jeffwids/archive/2009/10/12/asynchronously-sending-a-system-net-mail-mailmessage-in-c.aspx?CommentPosted=true#commentmessage – Arnold Sep 23 '11 at 21:14
  • What he is doing is a generic approach to do Async. The point is, why SendAsync method block the request? Something in the framework is awkward – Felipe Pessoto Sep 24 '11 at 02:32
  • 1
    I agree, something must be amiss, at least in my framework. However, I don't see the issue. Email does get sent, but the Redirect in my Controller Action is ignored. I haven't yet seen a complete example of SendAsync(). Using a generic approach and imperatively specifying WaitOne() and Dispose() causes no problems. I would not be surprised if I have configuration issues with SendAsync(). Thanks. – Arnold Sep 30 '11 at 17:29