Подтвердить что ты не робот

SmtpClient.SendAsync блокирует мой запрос ASP.NET MVC

У меня есть действие, которое отправляет простое электронное письмо:

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

И служба электронной почты:

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

        }
    }

Когда я отправляю электронное письмо, я использую метод Async, затем мой метод SendAsync немедленно возвращается, затем вызывается RedirectToAction. Но ответ (в данном случае перенаправление) не отправляется ASP.NET до завершения client_SendCompleted.

Вот что я пытаюсь понять:

При просмотре выполнения в отладчике Visual Studio SendAsync немедленно возвращается (и вызывается RedirectToAction), но ничего не происходит в браузере до отправки сообщения электронной почты?

Если я поставил точку останова внутри client_SendCompleted, клиент останется при загрузке.... пока я не удалю F5 в отладчике.

4b9b3361

Ответ 1

Это по дизайну. ASP.NET автоматически ждет завершения любой выдающейся работы async до завершения запроса, если работа async была запущена так, что вызывается в базовый SynchronizationContext. Это должно гарантировать, что если ваша операция async пытается взаимодействовать с HttpContext, HttpResponse и т.д., Все равно будет.

Если вы хотите сделать настоящий огонь и забыть, вам нужно обернуть свой вызов в ThreadPool.QueueUserWorkItem. Это заставит его работать в новом потоке пула потоков, не пройдя через SynchronizationContext, поэтому запрос затем с радостью вернется.

Обратите внимание, что если по какой-либо причине домен приложения должен был уйти, пока ваша передача еще не была выполнена (например, если вы изменили файл web.config, сбросили новый файл в корзину, пул приложений переработал и т.д. ) ваша асинхронная передача будет внезапно прервана. Если вас это волнует, взгляните на Phil Haacks WebBackgrounder для ASP.NET, который позволяет вам запускать в очередь и запускать фоновая работа (например, отправка электронной почты) таким образом, чтобы обеспечить ее изящное завершение в случае закрытия домена приложения.

Ответ 2

Это интересно. Я воспроизвел неожиданное поведение, но я не могу это объяснить. Я буду рыть.

В любом случае решение похоже на очередь в фоновом потоке, что поражает цель использования SendAsync. Вы закончите с этим:

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

Который может также стать:

ThreadPool.QueueUserWorkItem(o => {
    using (SmtpClient client = new SmtpClient(...))
    {
        using (MailMessage mailMessage = new MailMessage(...))
        {
            client.Send(mailMessage, Tuple.Create(client, mailMessage));
        }
    }
}); 

Ответ 3

С .Net 4.5.2 вы можете сделать это с помощью 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);
        });

Прочитайте это первым: http://www.hanselman.com/blog/HowToRunBackgroundTasksInASPNET.aspx