Sending E-mails with SendGrid and Azure Functions

Published by Mika Berglund on

SendGrid loves Azure Functions

A while ago I got the opportunity to dig a little bit deeper into SendGrid. In a project that I’m currently working on, we use Azure AD to send out guest user invitations. If you ever received a guest user invitation from Azure AD, you know that there is room for improvement. A lot of room! So we set out to see if we could improve on these e-mails sent by Azure AD. And allow our users to easily design and customize the e-mails too.

As a result of this, we decided to go with SendGrid and dynamic templates. There is a possible additional cost to using SendGrid, especially if you have a large volume of e-mails you send. However, the benefits are just overwhelmingly outmanning the drawbacks.

Disclaimer! This is not a paid endorsement by SendGrid. I just think they provide a lot of additional value, especially through Azure subscriptions.

Using Dynamic Templates in SendGrid

Previously, I have been using SendGrid mostly as an SMTP service together with the now obsolete SmtpClient. Applications just create the full body of the e-mail message and send them away. But, SendGrid also provides dynamic templates that can take your e-mails to that famous next level.

The idea is pretty simple. You create one or more templates with placeholders for your data. Then when you send an e-mail, you just specify the data for the placeholders. SendGrid provides you with a rich set of tools to author your templates. There’s also a nice template gallery that can help you get started.

Placeholders are just text formatted as {{FieldName}}. You can use placeholders everywhere in a template. This includes subject, body text, link URLs etc. The code explained below shows you how you then specify the value for FieldName and other fields you specify in your template.

Looking at the Code

Ok, enough of the theory. Let’s have a look at some code. As always, I created a repository on GitHub that contains a fully working solution. It should be easy enough for you get it working for you too.

Structure of the Repository

There are three projects in the solution. For just a sample, you could argue that three separate projects might be a bit over-engineered. But, in a real-world scenario, this is how you would split up your code. Even if I write just samples, I want to make them “production proof” too.

  • Abstractions – A class library that defines various types used by the other two projects. This library does not reference any other libraries or packages.
  • SendGridFunctions – The Azure Functions application that exposes one HTTP triggered function that you invoke to send an e-mail with.
  • Services – A separate library with a service implementation for communicating with the SendGrid service.

SendGridFunctions Project

The Abstractions project is perhaps not that interesting, so we’ll skip that. The SendGridFunctions project is a standard Azure Functions project with one HTTP triggered function.

The wiring of this application happens in the Startup.cs class. There we load in the configuration, serialize it to types specified in the Abstractions project, and store them as services. This is also were we add the EmailService service implementation to the application.

Sending an e-mail starts with the SendEmailWithTemplateHttp function in the SendGridFunctions class. You trigger the function with HTTP POST requests. When triggered, it reads information about the e-mail to send from a set of query string parameters. It also reads the request body. The body contains a JSON document with key-value pairs representing the placeholders in the e-mail template.

Let’s say you have the following HTML body in your e-mail template.

Hello <strong>{{FirstName}}</strong>! Thanks for visiting <a href="{{SiteUrl}}">my blog</a>. I hope you find the information you need.

Then, the JSON document that you would POST to the HTTP triggered function described above would look like this:

{
  "FirstName": "John",
  "SiteUrl": "https://mikaberglund.com"
}

Pretty simple, right? The function then uses this information to create a SendEmailWithTemplateRequest object. This object contains all necessary information about the e-mail that we need to send. This also includes the JSON document deserialized into a dictionary object. The function then hands over the delivery of the e-mail to an orchestration function built with the Durable Functions extension.

Improving Reliability with Durable Functions

In Durable Functions, a function can have only one input parameter. That’s why I wrap up all of the data into one object. In this case, we start a new orchestration function. When the orchestration function has been started, our HTTP triggered function is complete and can return to the caller. Please have a look at the last row in the HTTP triggered function. That uses a feature of the Durable Functions extension to create a special kind of response object. First of all, the response status is 202, meaning that the server has accepted (but not completed) the request. Second, the response is a JSON document containing a set of URIs. You can use them to interact with the orchestration function that you started when invoking the HTTP endpoint.

The reason why I chose to use Durable Functions here is that it will increase the reliability. This is because Durable Functions has a built-in retry mechanism that will retry any activity function that threw an exception. You can fully control how the activity function is retried by specifying retry options when calling the function. To make this easier, I created two extension methods in the Extensions class. The CallActivityWithDefaultRetryAsync methods will use a common default RetryOptions object to call an activity function.

For details on Durable Functions, see this overview on Azure docs.

Services Project

The Azure Functions application uses the Services project to handle the actual sending of the e-mail. In the real world, you would probably have multiple projects for your service implementations. Especially if you connect to several different systems or services from your application. I would not say there is a single right way to group your services into projects. I tend to group my services based on what dependencies they have. This means that all services depending on the same set of dependencies, like Nuget packages, would go into one project. However, at the end of the day, it is all up to you to decide. As long as it makes sense to you and your team.

EmailService Service

All of the magic is happening in the EmailService class. As you can see from the constructor, the class has a dependency on the SendGridConfigurationSection class. This dependency is injected as a service in the Startup class of the Azure Functions application.

The e-mail sending happens in the SendEmailWithTemplateAsync method. The method takes one parameter, an instance of the SendEmailWithTemplateRequest class. This class instance was created by the HTTP triggered function we examined above.

The implementation of this method is pretty straight forward. It uses the SendGrid SDK to create a message object. Then, the method adds personalization data to the message. Personalization includes the recipient as well as the dictionary containing the data for the placeholders specified in the template.

Then we just send the e-mail with the SendGridClient object from the SendGrid SDK.

Conclusion

Previously, you would normally send e-mails from a .NET application using the System.Net.Mail.SmtpClient class, using any SMTP server to handle the delivery. However, this class has now been deprecated, so you should not build new applications using that class.

The MailKit library is mentioned in the deprecation notice for SmtpClient. I haven’t looked into that library that much, but based on the documentation, you probably still would need an SMTP service to handle the delivery. Still, a very good option if you just need simple e-mail delivery.

However, as a complete service, SendGrid can offer you much more than just a class library. One of these features is the dynamic templates feature. Another area where SendGrid can provide you added value is e-mail analytics. For instance, SendGrid will keep track of each e-mail delivered. It will tell you how many have been opened, how many have bounced and how many have been blocked. It will also give you click-stats for all the links in your e-mails, and much more. This applies to all e-mails sent through SendGrid. Not just those sent using dynamic templates.

Hopefully this has give you a better overview of what you can do with SendGrid. And with the help of the sample code repository, it should be quite easy to implement in your application, even if your application is not an Azure Functions application.


0 Comments

Leave a Reply

Avatar placeholder

Your email address will not be published. Required fields are marked *