How to Bulk Email with C# and .NET: Zero to Hero
Niels Swimberghe - - .NET
Follow me on Twitter, buy me a coffee
This blog post was written for Twilio and originally published at the Twilio blog.
In a previous tutorial, I shared how you can send individual emails using the SendGrid API. This use case is perfect for transactional emails where you send an email to a single recipient or a small number of recipients. But what if you need to send emails to a very large audience? Well, in this tutorial you'll learn how to send bulk emails using the SendGrid API and C# .NET.
If you're not familiar with sending transactional emails using .NET, I advise you go through the previous post about sending emails with C# and SendGrid first.
Prerequisites #
Here’s what you will need to follow along:
- Git CLI
- .NET 6 SDK
- A code editor or IDE (I recommend VS Code with the C# plugin, Visual Studio, or JetBrains Rider)
- A Twilio SendGrid account. Sign up for a SendGrid account here to send up to 100 emails per day completely free of charge.
- You'll need an Email Sender configured in SendGrid to send emails from, and you'll need an API key with permission to send emails. Both of these steps can be found in the Configuring your SendGrid account to send emails section in the previous tutorial.
- An email address for testing, preferably from an email service that supports Subaddressing.
Many email services like Gmail and Outlook allow you to put a +
sign and any string before the @
symbol like this youremail+test123@gmail.com
. Emails sent to this type of address will still be delivered to the original email address. This feature is called Subaddressing. By using subaddressing, you will be able to test sending multiple emails to "different" addresses, however, email services will still prevent large amount of emails from coming through even when using subaddressing.
You can find the source code for this tutorial on GitHub. Use it if you run into any issues, or submit an issue, if you run into problems.
Send bulk email with C# .NET #
As is common in software development, there are many solutions to a given problem, and the best solution depends on your use case; the same goes for sending email in bulk. This tutorial will walk you through all the different ways of sending bulk email, and you'll learn which to use for your specific use case.
Setup #
For this tutorial, I have provided a starter project. Open a shell and run the following commands to clone the project and navigate to the project folder:
git clone https://github.com/Swimburger/BulkEmail.git --branch start cd BulkEmail
The project is a console application which currently only sends one email.
The console app has the following C# files:
- Program.cs: This is the starting point of the application, it will configure everything and then send the emails.
- SenderOptions.cs: This file has the
SenderOptions
class which is used to store some configuration about the SendGrid Email Sender. - SubscriberRepository.cs: This file has the
SubscriberRepository
class which is responsible for returning your imaginary subscribers, represented asPerson
objects. ThePerson
class is provided by the Bogus library and the subscribers are fake data generated using the same library, for testing purposes. Let's pretend this class retrieves real data from a SQL database as it would look exactly the same from the outside.
Feel free to take a look at these C# files, but don't worry about them, because the only important C# file is EmailSender.cs. This file has the EmailSender
class which you will update so that it sends bulk email.
This project does need some configuration which you will store as user secrets. Run the following commands:
dotnet user-secrets set SendGridApiKey [SENDGRID_API_KEY] dotnet user-secrets set Sender:Email [SENDER_EMAIL] dotnet user-secrets set Sender:Name '[SENDER_NAME]' dotnet user-secrets set ToEmailTemplate '[YOUR_EMAIL_TEMPLATE]'
Replace:
[SENDGRID_API_KEY]
with the API key you created in SendGrid,[SENDER_EMAIL]
with your Sender email address that you verified in SendGrid,[SENDER_NAME]
with the name you want the recipients to see,[YOUR_EMAIL_TEMPLATE]
with the email address you use to test. If you're using subaddressing, use this format:youremail+{0}@gmail.com
.{0}
will be replaced by the program with unique numbers. If you're not using subaddressing, use your email address without any special modifications.
Now your project should be ready. Try it out by running the project using this command:
dotnet run --SubscriberCount 5
Even though you specified 5
subscribers, you should only receive one email in your inbox. That's because the EmailSender
class only sends one email at the moment.
Let's take a look at the EmailSender.cs file:
using System.Text.Encodings.Web; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using SendGrid; using SendGrid.Helpers.Mail; namespace BulkEmail; public class EmailSender { private readonly SubscriberRepository subscriberRepository; private readonly ISendGridClient sendGridClient; private readonly ILogger<EmailSender> logger; private readonly HtmlEncoder htmlEncoder; private readonly SenderOptions sender; public EmailSender( SubscriberRepository subscriberRepository, ISendGridClient sendGridClient, IOptions<SenderOptions> senderOptions, ILogger<EmailSender> logger, HtmlEncoder htmlEncoder ) { this.subscriberRepository = subscriberRepository; this.sendGridClient = sendGridClient; this.logger = logger; this.htmlEncoder = htmlEncoder; this.sender = senderOptions.Value; } public async Task SendToSubscribers() { var subscribers = subscriberRepository.GetAll(); var subscriber = subscribers.First(); var message = new SendGridMessage { From = new EmailAddress(sender.Email, sender.Name), Subject = "Ahoy matey!", HtmlContent = "Welcome aboard <b>friend</b> ⚓️"️, }; message.AddTo(new EmailAddress(subscriber.Email, subscriber.FullName)); var response = await sendGridClient.SendEmailAsync(message); if (response.IsSuccessStatusCode) logger.LogInformation("Email queued"); else logger.LogError("Email not queued"); } }
The EmailSender
receives a bunch of parameters via its constructor and stores them into fields. Some of these are unused right now but will be used later. The SendToSubscribers
method is responsible for sending an email to all subscribers coming from the SubscriberRepository
, but currently it only grabs the first subscriber and stores it in the subscriber
variable.
The SendToSubscribers
method then creates a SendGridMessage
from your configured sender and adds the subscriber
as the recipient. Next, the SendGridMessage
is passed into the SendEmailAsync
method which sends the email to the SendGrid API. The SendGrid API will queue the email and return an HTTP success status code in the response if successful. Lastly, the method logs whether the email was queued or not.
Currently, the program only emails one subscriber, however, the goal is to send an email to all subscribers. Let's fix that!
Bulk email using loops #
The first solution is to wrap the email code in a loop. In this example, you can use a foreach
-loop like this:
var subscribers = subscriberRepository.GetAll(); foreach (var subscriber in subscribers) { var message = new SendGridMessage { From = new EmailAddress(sender.Email, sender.Name), Subject = "Ahoy matey!", HtmlContent = "Welcome aboard <b>friend</b> ⚓️"️, }; message.AddTo(new EmailAddress(subscriber.Email, subscriber.FullName)); var response = await sendGridClient.SendEmailAsync(message); if (response.IsSuccessStatusCode) logger.LogInformation("Email queued"); else logger.LogError("Email not queued"); }
Replace lines 34 to 47 in the EmailSender.cs file with the above code and save the file. Use the following command to try it out with 5 subscribers, or however many you'd like to spam your own inbox with:
dotnet run --SubscriberCount 5
This is the easiest and most flexible solution, however, it is also the least performant solution.
You could further optimize this solution using parallelization or using Task.WhenAll
, however, keep in mind that you can send a maximum of 10,000 API requests per second as noted in the v3 Mail Send API FAQ.
Customize the email per recipient #
When you send emails in bulk by submitting a SendGridMessage
for every recipient, you have complete control over every email. To customize the subject and email body, you can use any means necessary.
Here's an example that uses string interpolation to customize the subject and email body:
var message = new SendGridMessage { From = new EmailAddress(sender.Email, sender.Name), Subject = $"Ahoy {subscriber.FirstName}!", HtmlContent = $"Welcome aboard <b>{htmlEncoder.Encode(subscriber.FullName)}</b> ⚓️"️ };
When you build HTML templates and embed user input, your application may become vulnerable to HTML injection attacks. To prevent HTML injection attacks, always encode user input. In the example above the subscriber's full name is HTML encoded using the HtmlEncoder.Encode
method. For more details, read How to prevent email HTML injection in C# and .NET.
This works fine for a proof of concept, but in a real application you could use a template engine to generate your HTML. Check out this article about rendering emails using Razor templates to learn more.
Bulk email using Personalizations #
The loop solution sends a new API request for every subscriber, but there is actually a way to send many emails using a single API request.
The SendGridMessage
class has a property called Personalizations
which is of type List<Personalization>
. Using these personalizations, you can customize the message for specific recipients using properties like From
, Tos
, Ccs
, Bccs
, Subject
, etc. However, you cannot override the TextContent
or HtmlContent
property from the SendGridMessage
.
Take a look at this example:
new SendGridMessage { From = new EmailAddress(sender.Email, sender.Name), Subject = "Ahoy matey!", HtmlContent = "Welcome aboard <b>friend</b> ⚓️"️, Personalizations = new List<Personalization> { new Personalization { Tos = new List<EmailAddress> { new EmailAddress("jon@localhost", "Jon") } }, new Personalization { Tos = new List<EmailAddress> { new EmailAddress("jill@localhost", "Jill"), new EmailAddress("jane@localhost", "Jane") }, Subject = "Hello world!" } } };
The SendGridMessage
in the snippet above will send 2 emails because there are 2 personalizations. One email with the subject "Ahoy matey!" to jon@localhost, and 1 email with the subject "Hello world!" to jill@localhost and jane@localhost.
When you add multiple email addresses to the Tos
property, each recipient can see and reply to all the other recipients. So Jill and Jane in the example above will be able to see and reply to each other. To avoid this you can create separate Personalization
objects for each recipient. In fact, when you previously used the SendGridMessage.AddTo
method, the method created a personalization for you.
You can have up to 1,000 recipients per SendGridMessage
. Tos
, Ccs
, and Bccs
across all personalizations all add up to the 1,000 limit. The maximum number of Personalization
objects per SendGridMessage
is also 1,000.
Since you can only have 1,000 recipients per message, you'll need to paginate over the subscribers. There is a GetByPage(int pageSize, int pageIndex)
method in the SubscriberRepository
to help with pagination. The following code will paginate over all subscribers by 1,000 subscribers at a time.
var pageSize = 1_000; var subscriberCount = subscriberRepository.Count(); var amountOfPages = (int) Math.Ceiling((double) subscriberCount / pageSize); for (var pageIndex = 0; pageIndex < amountOfPages; pageIndex++) { var subscribers = subscriberRepository.GetByPage(pageSize, pageIndex); ... }
Even if you weren't limited by 1,000 recipients per message, it would still be a good idea to paginate over the subscribers to reduce the memory usage of your application. As the number of subscribers increases, your application would run out of memory when loading all subscribers into memory at once.
Now that you have your subscribers, you can create a Personalization
object for every one of them. Here's how you can do this using a LINQ query:
var message = new SendGridMessage { From = new EmailAddress(sender.Email, sender.Name), Subject = "Ahoy matey!", HtmlContent = "Welcome aboard <b>friend</b> ⚓️"️, // max 1000 Personalizations Personalizations = subscribers.Select(s => new Personalization { Tos = new List<EmailAddress> {new EmailAddress(s.Email, s.FullName)}, }).ToList(), };
After putting all the previous code snippets together, your SendToSubscribers
method should look like this:
public async Task SendToSubscribers() { var pageSize = 1_000; var subscriberCount = subscriberRepository.Count(); var amountOfPages = (int) Math.Ceiling((double) subscriberCount / pageSize); for (var pageIndex = 0; pageIndex < amountOfPages; pageIndex++) { var subscribers = subscriberRepository.GetByPage(pageSize, pageIndex); var message = new SendGridMessage { From = new EmailAddress(sender.Email, sender.Name), Subject = "Ahoy matey!", HtmlContent = "Welcome aboard <b>friend</b> ⚓️"️, // max 1000 Personalizations Personalizations = subscribers.Select(s => new Personalization { Tos = new List<EmailAddress> {new EmailAddress(s.Email, s.FullName)}, }).ToList(), }; var response = await sendGridClient.SendEmailAsync(message); if (response.IsSuccessStatusCode) logger.LogInformation("Email queued"); else logger.LogError("Email not queued"); } }
You can lower the pageSize
number to test pagination without sending 1,000 emails to your inbox.
For example, for testing purposes, you could set the pageSize
to 5 and the SubscriberCount
to 20:
dotnet run --SubscriberCount 20
Customize the email per recipient #
As I mentioned before, you can override the email subject in a Personalization
object, but not the HtmlContent
or TextContent
. However, you can still customize the email body per recipient using Substitution Tags.
First, add substitution tags to your content like this:
HtmlContent = "Welcome aboard <b>-FullName-</b> ⚓️"️
Then, add substitutions to your Personalization
objects like this:
new Personalization { ... // Substitutions data is max 10,000 bytes per Personalization object Substitutions = new Dictionary<string, string> { {"-FullName-", htmlEncoder.Encode(s.FullName)} } }
The substitution tag is decorated with dashes (-) in this example, however, you can use any type of decoration characters, as long as the substitution tags in the HtmlContent
and TextContent
match with the keys of the Substitutions
dictionary.
You can also use these substitution tags inside of your email subject. Putting this together, you can send personalized bulk emails like this:
var message = new SendGridMessage { From = new EmailAddress(sender.Email, sender.Name), Subject = "Ahoy -FirstName_Raw-!", HtmlContent = "Welcome aboard <b>-FullName-</b> ⚓️"️, // max 1000 Personalizations Personalizations = subscribers.Select(s => new Personalization { Tos = new List<EmailAddress> {new EmailAddress(s.Email, s.FullName)}, // Substitutions data is max 10,000 bytes per Personalization object Substitutions = new Dictionary<string, string> { {"-FirstName_Raw-", s.FirstName}, {"-FullName-", htmlEncoder.Encode(s.FullName)} } }).ToList(), };
I chose to suffix the first name substitution tag with _Raw
to indicate to myself and others that this variable is not HTML encoded and should not be used in HTML content, to avoid HTML injection.
Send bulk email using Dynamic Email Templates #
Thus far, you provided the email subject and body from code, however, you can also use Dynamic Email Templates. You can create these templates using the SendGrid UI or API, and then instead of specifying the subject and content in your SendGridMessage
, you set the ID of your template to the SendGridMessage.TemplateId
property. Dynamic Email Templates use the Handlebars templating language, which allows you to do variable substitution, conditional rendering, looping, and more.
Implementing Dynamic Email Templates is out of the scope of this tutorial, however, here is what your SendGridMessage
would look like to achieve the same result as before:
var message = new SendGridMessage { From = new EmailAddress(sender.Email, sender.Name), TemplateId = "d-0a664e681ed14d76bd452637a15b20ab", Personalizations = subscribers.Select(s => new Personalization { Tos = new List<EmailAddress> {new EmailAddress(s.Email, s.FullName)}, TemplateData = new { FirstName = s.FirstName, FullName = s.FullName } }).ToList(), };
Instead of using the Substitutions
property on your Personalization
, you set the TemplateData
property with an object. The Dynamic Email Template will be able to access all the data from the TemplateData
object.
Want an in depth guide? Read this article on how to Send Emails with C#, Handlebars templating, and Dynamic Email Templates.
Since Dynamic Email Templates use the same SendGridMessage
and Personalization
classes, everything you learned about bulk email so far also applies here.
Send bulk email using Single Sends in SendGrid Marketing Campaigns #
SendGrid Marketing Campaigns are also out of scope for this tutorial, but for the sake of completeness, I will lightly touch on it. With SendGrid Marketing Campaigns you can create signup forms, manage contacts, organize contacts into lists and segments, create email designs, and more.
And of course, you can send emails to those contacts stored in SendGrid.
You can manage all of the previous features using the SendGrid UI or API. For example, you can manage contacts using the SendGrid UI or programmatically via the API, and you can also send emails to your contacts, lists, and segments using the Single Send API. Since the contacts are all stored in SendGrid, you don't have to worry about any looping and paginating.
With the Single Send API you can either provide the email body via the API or you can specify the ID of the email design stored in SendGrid. For either option, you cannot pass on data through the API to use in the email template. However, the templates have access to the data stored on the contacts. Once you created the Single Send, you can schedule the Single Send to be sent "now" or up to 72 hours from now.
Schedule your bulk email #
When you're sending a lot of emails, SendGrid recommends scheduling them to be sent in the near future instead of sending them immediately. Quoting from the SendGrid docs:
This technique allows for a more efficient way to distribute large email requests and can improve overall mail delivery time performance. This functionality:
- Improves efficiency of processing and distributing large volumes of email.
- Reduces email pre-processing time.
- Enables you to time email arrival to increase open rates.
- Is available for free to all SendGrid customers.
To schedule emails you can set the SendAt
property on the SendGridMessage
or Personalization
object. The SendAt
property is a long
representing a Unix timestamp.
To calculate this Unix timestamp, you can use the DateTimeOffset.ToUnixTimeSeconds()
method.
For example, this will generate the long
timestamp for 5 minutes from now:
DateTimeOffset.Now.AddMinutes(5).ToUnixTimeSeconds()
If you're using DateTime
instead of DateTimeOffset
, you can implicitly and explicitly cast a DateTime
to a DateTimeOffset
, like this:
DateTimeOffset fiveMinutesFromNow = DateTime.Now.AddMinutes(5); // implicit cast fiveMinutesFromNow = (DateTimeOffset) DateTime.Now.AddMinutes(5); // explicit cast
Check out this Microsoft doc on converting between DateTime and DateTimeOffset to learn more.
Here's the SendToSubscribers
method for sending the Bulk Email 5 minutes from now:
public async Task SendToSubscribers() { var sendAt = DateTimeOffset.Now.AddMinutes(5).ToUnixTimeSeconds(); var pageSize = 1_000; var subscriberCount = subscriberRepository.Count(); var amountOfPages = (int) Math.Ceiling((double) subscriberCount / pageSize); for (var pageIndex = 0; pageIndex < amountOfPages; pageIndex++) { var subscribers = subscriberRepository.GetByPage(pageSize, pageIndex); var message = new SendGridMessage { From = new EmailAddress(sender.Email, sender.Name), Subject = "Ahoy -FirstName_Raw-!", HtmlContent = "Welcome aboard <b>-FullName-</b> ⚓️"️, // max 1000 Personalizations Personalizations = subscribers.Select(s => new Personalization { Tos = new List<EmailAddress> {new EmailAddress(s.Email, s.FullName)}, // Substitutions data is max 10,000 bytes per Personalization object Substitutions = new Dictionary<string, string> { {"-FirstName_Raw-", s.FirstName}, {"-FullName-", htmlEncoder.Encode(s.FullName)} } }).ToList(), // max 72 hours from now SendAt = sendAt }; var response = await sendGridClient.SendEmailAsync(message); if (response.IsSuccessStatusCode) logger.LogInformation("Email queued"); else logger.LogError("Email not queued"); } }
You can schedule emails to be sent up to 72 hours from now. If you need to schedule beyond that time, I recommend scheduling emails using a job scheduler like Quartz.NET or Hangfire.
Unsubscribe recipients #
When recipients mark your emails as spam, it hurts your reputation. As an email sender, you want to maintain a good reputation to maximize your email deliverability. You must also comply with anti-spam laws. Make sure to use a double opt-in process for things like newsletters, and always provide an unsubscribe link in your emails. You can add your own unsubscribe logic, or you can use Global Unsubscribes or Group Unsubscribes from SendGrid.
MailHelper #
The SendGrid SDK contains a helper class called MailHelper
. MailHelper
has convenient static methods for the most common email scenarios, including bulk email.
The CreateSingleEmailToMultipleRecipients
method lets you conveniently create a SendGridMessage
to send emails to many recipients. By default it will create a Personalization
for every recipient, but there's an overload where you can set the showAllRecipients
parameter to true. When this parameter is set to true, the method will create one Personalization
with all the recipients in it.
The CreateSingleTemplateEmailToMultipleRecipients
method will create a SendGridMessage
configured with a Dynamic Email Template and a Personalization
for every recipient.
The CreateMultipleEmailsToMultipleRecipients
method will create a SendGridMessage
configured with a Personalization
for every recipient and Substitution Tokens.
Which bulk email technique should you use? #
You just learned many different ways to send email in bulk, but which should you use?
Not to be cliche, but it depends… Let's compare each technique:
When you're using loops, you have full control over every email you send and there are no limitations.However, this is the least performant solution because you have to send an HTTP request for each email.
Use loops when you need to do advanced customization of the subject and email body that is not possible using Substitution Tags, or with the Handlebars templating language. This solution is ideal if you want to generate the email body using the Razor templating language.
Using Personalizations will greatly improve the performance of your application because you can send 1,000 emails per HTTP request. If you don't need to customize the email body for each recipient, you can set the content once for all emails, without the need for Substitution Tokens or Dynamic Email Templates.
Personalizations with Substitution Tokens works great for doing simple email customizations where you need to substitute predefined variables for every recipient. You cannot do more complex templating like looping and conditional rendering.
When you're doing Personalizations with Dynamic Email Templates, you have to manage the email templates in SendGrid which could be a benefit or a drawback depending on your needs. Dynamic Email Templates use a fully featured templating language, Handlebars, which you can use to render the simplest to the most advanced templates.
If you don't want to store your contacts in your own database, you can store contacts in SendGrid and send emails to them using Single Sends. Single Sends uses the same Handlebar templating language, but can only access the data that is stored on the contact. This means you have to upload the data to your SendGrid contacts before sending the email that requires it. You can send emails to all your contacts, specific contact lists, or segments of your contacts and you can generate unsubscribe links. The unsubscribe links allow the contact to remove themselves from the contacts list. You also don't have to worry about looping or paginating over your contacts as SendGrid will do this for you. This is ideal for marketing emails and newsletters.
If you're still not sure, you could go down this decision tree:
Next steps #
Big congratulations if you made it this far! 🎉
You learned how to send emails in bulk using a bunch of methods, each with their own advantages. However, this tutorial only covers the code side of things, but you should check out this guide on bulk emails to maximize the deliverability, engagement, and minimize the unsubscription rates.
If you want to see a practical example of bulk email in action, check out this tutorial on how to build an Email Newsletter application using ASP.NET Core and SendGrid. This tutorial manages the newsletter subscribers using Entity Framework Core and uses a form to upload the HTML newsletter which is then sent using Personalizations and Substitution Tokens.