-1

TLDR; Which of the following (or otherwise) is most suitable for sending 300-500 emails quickly to an external mail server?

  1. SmtpClient.SendAsync
  2. SmtpClient.SendMailAsync
  3. new Thread
  4. Parallel.ForEach

I'm amending code for sending emails to now use an external sender (e.g. MailGun, SendGrid etc.) instead of HMailServer which is currently installed on the same server as the application. This is obviously introducing latency.

I've read the documentation for all of the above, but am struggling to fully understand the implications (particularly the gotcha's) of each. There seems to be very differing opinions about whether the above are suitable or not, particularly:

  1. If I have to await each email, then is that not simulating the current sync approach?
  2. I'm trying to use a single instance of the SmptClient object, for speed and efficiency
  3. Based on extensive reading today, many examples I've seen are old, and may not use the latest .Net capabilities

I'd welcome input please from people who have actually achieved this successfully. Naturally I can go write code for each, but I'm looking for experienced people to guide me on the right path to begin with here.

My simplified (existing sync) code is as follows:

var sb = new StringBuilder();

using (var mc = new SmtpClient() {
    Host = "127.0.0.1", // Current HMailServer installation - will be changed to external API
    DeliveryMethod = SmtpDeliveryMethod.Network,
    Port = 25,
    UseDefaultCredentials = false,
    Credentials = new NetworkCredential("Username", "Password")
})
{
    foreach(var result in GetData())
    {
        using(var mm = new MailMessage())
        {
            mm.To.Add(new MailAddress(result.Email, result.FirstName + " " + result.Surname));
            mm.Subject = "Your monthly report";
            mm.From = new MailAddress("noreply@example.com");
            mm.ReplyToList.Add(new MailAddress(result.Email));

            // Email body constructed here for each individual recipient
            mm.Body = sb.ToString();
            sb.Clear();

            mc.Send(mm);
        }
    }
}
EvilDr
  • 8,943
  • 14
  • 73
  • 133
  • 2
    250 emails are NOT a lot - hardly a difference. The answer will be very different from a large number of emails (like 100k).. Also, the server matters - there may be limits on connections and number of emails per connection depending on server. – TomTom Feb 25 '20 at 14:30
  • 1
    Sending an email is a I/O-bound operation, not a CPU-bound operation, so you can immediately discard the `new Thread` and `Parallel.ForEach` options. These are intended for CPU-bound operations. For I/O-bound operations you need async-await and `Task.WhenAll`. Realistically the remote server will not be happy if you send 300 concurrent requests, so you 'll have to throttle. Look [here](https://stackoverflow.com/questions/10806951/how-to-limit-the-amount-of-concurrent-async-i-o-operations) for async throttling techniques. – Theodor Zoulias Feb 25 '20 at 16:16
  • @TheodorZoulias *"...you can immediately discard the new Thread and Parallel.ForEach options"* - highlights the issues with answers on SO which are clearly not fit-for-purpose then :( https://stackoverflow.com/a/3408421/792888 – EvilDr Feb 26 '20 at 09:22

1 Answers1

1

For I/O-bound tasks like sending email, you do not want to use Parallel. This goes double if you're running on ASP.NET. Also, you don't want to use new Thread unless you're doing COM interop.

If you want to make it asynchronous, the easiest way is to keep everything the same and just call SendAsync instead of Send:

var sb = new StringBuilder();

using (var mc = new SmtpClient() {
    Host = "127.0.0.1", // Current HMailServer installation - will be changed to external API
    DeliveryMethod = SmtpDeliveryMethod.Network,
    Port = 25,
    UseDefaultCredentials = false,
    Credentials = new NetworkCredential("Username", "Password")
})
{
    foreach(var result in GetData())
    {
        using(var mm = new MailMessage())
        {
            mm.To.Add(new MailAddress(result.Email, result.FirstName + " " + result.Surname));
            mm.Subject = "Your monthly report";
            mm.From = new MailAddress("noreply@example.com");
            mm.ReplyToList.Add(new MailAddress("admin@example.com"));

            // Email body constructed here for each individual recipient
            mm.Body = sb.ToString();
            sb.Clear();

            await mc.SendAsync(mm);
        }
    }
}

Now, if you want to do it concurrently, then you'll want to use Task.WhenAll:

using (var mc = new SmtpClient() {
    Host = "127.0.0.1", // Current HMailServer installation - will be changed to external API
    DeliveryMethod = SmtpDeliveryMethod.Network,
    Port = 25,
    UseDefaultCredentials = false,
    Credentials = new NetworkCredential("Username", "Password")
})
{
    var tasks = GetData().Select(async result =>
    {
        using(var mm = new MailMessage())
        {
            mm.To.Add(new MailAddress(result.Email, result.FirstName + " " + result.Surname));
            mm.Subject = "Your monthly report";
            mm.From = new MailAddress("noreply@example.com");
            mm.ReplyToList.Add(new MailAddress("admin@example.com"));

            var sb = new StringBuilder();
            // Email body constructed here for each individual recipient
            mm.Body = sb.ToString();

            await mc.SendAsync(mm);
        }
    });
    await Task.WhenAll(tasks);
}

(note that the StringBuilder is no longer shared)

I haven't used the SendGrid SMTP API at scale, but I have hit their REST API with a considerable number of requests.

Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
  • Thank you. If it is done concurrently, would that send *all* the emails at the same time (e.g. 350 gets sent to SendGrid at once)? Also with the first approach, how does that avoid the latency if we need to await each email in turn please? – EvilDr Feb 26 '20 at 09:03
  • Second approach generates an error, *"InvalidOperationException: An asynchronous call is already in progress. It must be completed or canceled before you can call this method."* on `SendMailAsync(System.Net.Mail.MailMessage)`. Might be because I swapped out `SendAsync` with `SendMailAsync` because it had the same signature as `Send`. – EvilDr Feb 26 '20 at 10:31
  • 1
    @EvilDr: It's very odd that `X` and `XAsync` would not have compatible signatures. I assumed they would. If you're getting that exception, then you'd need a separate `SmtpClient` for each request. – Stephen Cleary Feb 26 '20 at 14:52
  • Stephen does your book (in your profile) cover *when* to use specific approaches, like how you began this answer? – EvilDr Feb 26 '20 at 22:26
  • 1
    @EvilDr: For `async` and `Parallel`, yes. There's some discussion but the general rule is pretty simple: `async` is for I/O; `Parallel` is for computation. Those two rules are sufficient for 99% of cases. I also mention `Thread` in the book ("mention" as in "don't use this old type unless you're doing COM interop"). – Stephen Cleary Feb 28 '20 at 02:30