web-dev-qa-db-fra.com

Manière correcte d'envoyer un email de manière asynchrone dans ASP.NET ... (est-ce que je le fais correctement?)

Lorsqu'un utilisateur s'enregistre sur mon site Web, je ne vois pas pourquoi je devrais le faire "attendre" que le smtp passe afin qu'il reçoive un email d'activation.

J'ai décidé de lancer ce code de manière asynchrone, et ce fut une aventure.

Imaginons que j'ai une méthode, telle que:

private void SendTheMail() { // Stuff }

Mon premier si .. filait. J'ai fait ça:

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

Cela fonctionne ... jusqu'à ce que je décide de le tester pour la capacité de gestion des erreurs. J'ai volontairement cassé l'adresse du serveur SMTP dans mon web.config et je l'ai essayé. Le résultat effrayant était que IIS BARFED avec une erreur d’exception non gérée sur w3wp.exe (c’était une erreur Windows! Comment extrême ...) ELMAH (mon enregistreur d’erreurs) n’a pas intercepté ET IIS a été redémarré, de sorte que toute session du site Web a été effacée. Résultat complètement inacceptable!

Ma pensée suivante était de faire des recherches sur les délégués asynchrones. Cela semble mieux fonctionner car les exceptions sont gérées dans le délégué asynchrone (contrairement à l'exemple de thread ci-dessus). Cependant, je crains que je ne le fasse pas correctement ou que je provoque des fuites de mémoire.

Voici ce que je fais:

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

Est-ce que je le fais bien?

19
Ralph N

J'ai eu beaucoup de bons conseils ici, par exemple en veillant à ne pas oublier d'utiliser IDisposable (je ne savais absolument pas). J'ai aussi réalisé à quel point il était important de détecter manuellement les erreurs dans un autre thread, car il n'y avait pas de contexte - j'ai travaillé sur une théorie selon laquelle je devrais laisser ELMAH gérer tout. De plus, une exploration plus poussée m'a fait comprendre que j'oubliais également d'utiliser IDisposable sur mailmessage.

En réponse à Richard, même si je vois que la solution de threading peut fonctionner (comme suggéré dans mon premier exemple) tant que je détecte les erreurs ... il y a toujours quelque chose d'effrayant sur le fait que IIS complètement explose si cette erreur n'est pas interceptée. Cela me dit qu'ASP.NET/IIS n'a jamais voulu que vous le fassiez ... c'est pourquoi je me penche plutôt pour continuer à utiliser .BeginInvoke/délégués, car cela ne gâche pas IIS quand quelque chose ne va pas et semble être plus populaire dans ASP.NET.

En réponse à ASawyer, j'ai été totalement surpris qu'il y ait un .SendAsync intégré dans le client SMTP. J'ai joué avec cette solution pendant un moment, mais cela ne semble pas faire l'affaire pour moi. Bien que je puisse ignorer le client du code qui utilise SendAsync, la page "attend" jusqu'à ce que l'événement SendCompleted soit terminé. Mon objectif était de faire avancer l'utilisateur et la page pendant que le courrier électronique était envoyé en arrière-plan. J'ai le sentiment que je pourrais toujours faire quelque chose de mal ... donc si quelqu'un vient par là, il pourrait vouloir l'essayer lui-même.

Voici ma solution complète sur la façon dont j'ai envoyé des courriels à 100% de manière asynchrone, en plus de la journalisation des erreurs ELMAH.MVC. J'ai décidé d'aller avec une version développée de l'exemple 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.")));
    }
}
25
Ralph N

A partir de .NET 4.5, SmtpClient implémente la méthode async attendue SendMailAsync . En conséquence, l'envoi d'e-mails de manière asynchrone est le suivant:

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);
    }
} 
6
Boris Lipschitz

Utilisez-vous le .Net SmtpClient pour envoyer un courrier électronique? Il peut déjà envoyer des messages asynchrones .

Edit - Si Emailer mailer = new Emailer(); n’est pas un wrapper sur SmtpClient, ce ne sera pas aussi utile que j’imagine.

3
asawyer

Si vous utilisez les classes SmtpClient et MailMessage .Net, vous devez prendre note de quelques éléments. Tout d’abord, attendez-vous à des erreurs lors de l’envoi. Deuxièmement, dans .Net 4, quelques modifications ont été apportées à ces classes et les deux implémentent maintenant IDisposable (MailMessage depuis la version 3.5, nouveau SmtpClient dans la version 4.0). Pour cette raison, votre création de SmtpClient et de MailMessage doit être encapsulée dans des blocs ou explicitement supprimée. C'est un changement radical que certaines personnes ignorent.

Voir cette SO question pour plus d'informations sur la suppression lors de l'utilisation d'async envoie:

Quelles sont les meilleures pratiques pour utiliser SmtpClient, SendAsync et Dispose sous .NET 4.0

3
hatchet

Les threads ne sont pas la mauvaise option ici, mais si vous ne gérez pas une exception vous-même, cela va bouillonner et bloquer votre processus. Peu importe le fil sur lequel vous le faites.

Donc au lieu de mailer.SendTheMail (), essayez ceci:

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

Mieux encore, utilisez les capacités asynchrones du SmtpClient si vous le pouvez. Vous devrez quand même gérer les exceptions.

Je vous suggérerais même de jeter un coup d'œil à la nouvelle bibliothèque de tâches de Parallet de .Net 4. Cela a des fonctionnalités supplémentaires qui vous permettent de gérer les cas exceptionnels et fonctionne bien avec le pool de threads d'ASP.Net.

3
Richard

Alors, pourquoi ne pas avoir un poller/service séparé qui traite exclusivement de l’envoi de courriels? Ainsi, autoriser l’exécution ultérieure de votre enregistrement uniquement dans le délai nécessaire pour écrire dans la base de données/la file de messages et retarder l’envoi du courrier électronique jusqu’au prochain intervalle de sondage.

Je réfléchis au même problème tout à l'heure et je pense que je ne veux vraiment pas même lancer l'envoi d'e-mails dans la demande de post-traitement du serveur. Le processus de création des pages Web devrait être intéressé pour que l'utilisateur reçoive une réponse, le plus de travail que vous tentez d'accomplir, le plus lentement possible.

Consultez le principe de séparation des requêtes de commandes ( http://martinfowler.com/bliki/CQRS.html ). Martin Fowler explique qu'il est possible d'utiliser dans la partie commande d'une opération différents modèles que ceux utilisés dans la partie requête. Dans ce scénario, la commande serait "enregistrer l'utilisateur", la requête serait le courrier électronique d'activation, en utilisant l'analogie simplifiée. La citation pertinente serait probablement:

Par modèles séparés, nous entendons le plus souvent différents modèles d'objet, s'exécutant probablement dans différents processus logiques

L'article de Wikipedia sur CQRS ( http://en.wikipedia.org/wiki/Command%E2%80%93query_separation ) mérite également d'être lu. Un point important que cela met en évidence est:

il est clairement conçu comme une directive de programmation plutôt que comme une règle pour un bon codage

En d'autres termes, utilisez-le là où votre code, l'exécution du programme et la compréhension du programmeur pourraient en bénéficier. Ceci étant un bon exemple de scénario.

Cette approche présente l’avantage supplémentaire de supprimer toutes les préoccupations liées au multithreading et les maux de tête que tout ce que cela peut entraîner.

2
A. Murray

Utilisez cette façon-

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 ";
        }
1
Rahul

J'ai travaillé le même problème pour mon projet:

D'abord essayé Thread comme vous le faites:
- Je perds le contexte
- Problème de gestion des exceptions
- Communément dit, Thread sont une mauvaise idée sur IIS ThreadPool 

Donc je change et j'essaie avec asynchronously:
- 'asynchrone' est fake dans une application Web asp.net. Il suffit de mettre les appels en file d'attente et swicth le contexte 

Donc, je fais un service Windows et récupère les valeurs via une table SQL: happy end

Donc, pour une solution rapide: du côté ajax faire un appel async dire à l'utilisateur fake oui, mais continuez votre travail d'envoi dans votre contrôleur mvc

1
asdf_enel_hak

Si vous souhaitez détecter des fuites, vous devez utiliser un profileur comme celui-ci:

http://memprofiler.com/

Je ne vois rien mauvais avec votre solution, mais je peux presque vous garantir que cette question sera fermée de manière subjective.

Une autre option consiste à utiliser jQuery pour effectuer un appel ajax vers le serveur et déclencher le flux de messagerie. De cette façon, l'interface utilisateur n'est pas verrouillée.

Bonne chance!

Mat

0
Matt Cashatt