How to send recurring emails in C# .NET using SendGrid and Quartz.NET

Niels Swimburger.NET 🍔 - Jan 24 '22 - - Dev Community

This blog post was written for Twilio and originally published at the Twilio blog.

Many applications have the need to send emails on a periodic basis. A common example of this is a weekly digest, where the email recaps everything that happened that week. There are many ways you could send recurring emails using .NET. You could use the Task Scheduler on Windows, use crontab on Linux, or even develop your own job scheduling implementation. In this tutorial, you'll learn how to schedule recurring emails using Twilio SendGrid and Quartz.NET.

Prerequisites

You will need the following things to follow along:

  • OS that supports .NET (Windows/Mac/Linux)
  • .NET 6 SDK (download)
  • A code editor or IDE (Recommended: VS Code with C# plugin, Visual Studio, JetBrains Rider)
  • A Twilio SendGrid account (signup)

Everything you’ll need to do for this tutorial applies even if you’re using an older version of .NET (Core), however you’ll need to make minor adjustments.

Create your .NET application

You'll be using the .NET worker template because it is the best starting point for this type of application.

Create a new directory and change the current directory to it:

mkdir ScheduleMailUsingQuartz
cd ScheduleMailUsingQuartz
Enter fullscreen mode Exit fullscreen mode

Use the .NET CLI to create a new project using the worker template:

dotnet new worker
Enter fullscreen mode Exit fullscreen mode

Run the new .NET project using the .NET CLI:

dotnet run
Enter fullscreen mode Exit fullscreen mode

Out of the box, the worker template will continuously run and print a new line to the console every second. The output of the command should look like this:

Building...
info: DotNetWorker.Worker[0]
      Worker running at: 01/04/2022 16:31:08 -05:00
info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
      Hosting environment: Development
info: Microsoft.Hosting.Lifetime[0]
      Content root path: /Users/nswimberghe/DotNetWorker
info: DotNetWorker.Worker[0]
      Worker running at: 01/04/2022 16:31:09 -05:00
info: DotNetWorker.Worker[0]
      Worker running at: 01/04/2022 16:31:10 -05:00
info: DotNetWorker.Worker[0]
      Worker running at: 01/04/2022 16:31:11 -05:00
info: DotNetWorker.Worker[0]
      Worker running at: 01/04/2022 16:31:12 -05:00
^Cinfo: Microsoft.Hosting.Lifetime[0]
      Application is shutting down...
Enter fullscreen mode Exit fullscreen mode

Integrate Quartz.NET into worker project

What is Quartz.NET

Quartz.NET is an open source job scheduler for .NET. You can use this library to schedule one-off jobs and jobs that should recur on a timed schedule. For this tutorial, you'll run a single instance of Quartz and store the jobs in memory, but you can also run multiple instances of Quartz and store the jobs in a database. Storing the jobs in a database and having multiple instances will help your application become more resilient and process more jobs at a time.

There are a lot more features and capabilities that you can learn more about at the Quartz.NET website.

Add Quartz.NET to your project

Use the .NET CLI to add these two Quartz NuGet packages to your project:

dotnet add package Quartz
dotnet add package Quartz.Extensions.Hosting
Enter fullscreen mode Exit fullscreen mode

The Quartz package contains the core capabilities to schedule jobs. The Quartz.Extensions.Hosting package contains extensions to integrate Quartz with .NET's generic host APIs, which the worker template is already using.

Open up the ScheduleMailUsingQuartz directory you created earlier in your preferred text editor and you will see a Program.cs file within it. Open up the file and replace the existing code with the following code:

using Quartz;

IHost host = Host.CreateDefaultBuilder(args)
    .ConfigureServices((context, services) =>
    {
        services.AddQuartz(q =>
        {
            q.UseMicrosoftDependencyInjectionJobFactory();
            q.ScheduleJob<SendMailJob>(trigger => trigger
                .WithIdentity("SendRecurringMailTrigger")
                .WithSimpleSchedule(s =>
                    s.WithIntervalInSeconds(15)
                    .RepeatForever()
                )
                .WithDescription("This trigger will run every 15 seconds to send emails.")
            );
        });

        services.AddQuartzHostedService(options =>
        {
            // when shutting down we want jobs to complete gracefully
            options.WaitForJobsToComplete = true;
        });
    })
    .Build();

await host.RunAsync();

class SendMailJob : IJob
{
    private readonly ILogger logger;

    public SendMailJob(ILogger<SendMailJob> logger)
    {
        this.logger = logger;
    }

    public async Task Execute(IJobExecutionContext context)
    {
        logger.LogInformation("Greetings from SendMailJob!");
    }
}
Enter fullscreen mode Exit fullscreen mode

If you are using older versions of .NET or older templates of .NET, you will need to put the above code and any subsequent code inside the Main method of the Program class. Put any additional classes below the Program class.

With these changes, Quartz is now configured to run the SendMailJob every 15 seconds.

The SendMailJob.Execute method is where you want to add the logic of your job. For now, it logs "Greetings from SendMailJob".

Run the project again and observe the result:

dotnet run
Enter fullscreen mode Exit fullscreen mode

The output will contain information about Quartz getting up and running. Once Quartz is running, you should see "Greetings from SendMailJob" logged every 15 seconds. The output looks like this:

info: Quartz.Core.SchedulerSignalerImpl[0]
      Initialized Scheduler Signaller of type: Quartz.Core.SchedulerSignalerImpl
info: Quartz.Core.QuartzScheduler[0]
      Quartz Scheduler created
info: Quartz.Core.QuartzScheduler[0]
      JobFactory set to: Quartz.Simpl.MicrosoftDependencyInjectionJobFactory
info: Quartz.Simpl.RAMJobStore[0]
      RAMJobStore initialized.
info: Quartz.Core.QuartzScheduler[0]
      Scheduler meta-data: Quartz Scheduler (v3.3.3.0) 'QuartzScheduler' with instanceId 'NON\_CLUSTERED'
        Scheduler class: 'Quartz.Core.QuartzScheduler' - running locally.
        NOT STARTED.
        Currently in standby mode.
        Number of jobs executed: 0
        Using thread pool 'Quartz.Simpl.DefaultThreadPool' - with 10 threads.
        Using job-store 'Quartz.Simpl.RAMJobStore' - which does not support persistence. and is not clustered.

info: Quartz.Impl.StdSchedulerFactory[0]
      Quartz scheduler 'QuartzScheduler' initialized
info: Quartz.Impl.StdSchedulerFactory[0]
      Quartz scheduler version: 3.3.3.0
info: Quartz.Core.QuartzScheduler[0]
      Scheduler QuartzScheduler_$_NON_CLUSTERED started.
info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
      Hosting environment: Development
info: Microsoft.Hosting.Lifetime[0]
      Content root path: /Users/nswimberghe/ScheduleMailUsingQuartz
info: SendMailJob[0]
      Greetings from SendMailJob!
info: SendMailJob[0]
      Greetings from SendMailJob!
info: SendMailJob[0]
      Greetings from SendMailJob!
Enter fullscreen mode Exit fullscreen mode

Great job! You've added Quartz to your .NET application and have a job that recurs every 15 seconds.

It doesn't make sense to send an email every 15 seconds, but it makes testing a lot easier! You can use the simple schedule capabilities to run the job every X amount of seconds, minutes, or hours. You can also use CRON expressions to configure longer and more complicated schedules to run your job on.

Integrate SendGrid to send emails

Configuring your SendGrid account to send emails

There are two things you need to configure before you can send emails. First, you’ll need to set up Sender Authentication. This will verify that you own the email address or domain that you will send emails from. Second, you’ll need to create a SendGrid API Key with permission to send emails.

Sender Authentication

It is recommended to configure Domain Authentication which requires you to add a record to your DNS host. To keep things simple, you will use Single Sender Verification instead. This will verify that you own the single email address that you want to send emails from. Single Sender Verification is great for testing purposes, but it is not recommended for production.

Twilio recommends Domain Authentication for production environments. An authenticated domain proves to Inbox Service Providers you own the domain and removes the via sendgrid.net text that inbox providers would otherwise append to your from address.

Go to the SendGrid website and log in. Click on the Settings tab in the side menu. Once the settings tab opens up, click on Sender Authentication.

Side-navigation with a Settings group and multiple links in the group. The link "Sender Authentication" is highlighted.

Click on Get Started under the Single Sender Verification section.

A button saying "Get Started" to verify an individual email address.

This will open a form on the right-side panel. Fill out the form with your information and email address.

A form to create a sender. The form asks for contact information including the email address that will be used to send emails from.

Click Create after filling out the form. Another panel will appear on the right, asking you to confirm your email address. An email has been sent to the email address you entered.Go to your inbox, open the email from SendGrid, and click Verify Single Sender.

An email from SendGrid with a button saying "Verify Single Sender".

Your email address has been verified and you can now use it to send emails!

Create a SendGrid API key to send emails

Back on the SendGrid website, click on API Keys under the Settings tab. Click on Create API Key in the top right corner. This will open another form in the right-side panel. Give your API Key a useful name. You can assign different permissions to the API Key. For optimal security, you should only give the minimum amount of permissions that you need. Click on Restricted Access.

A form to create an API Key. The form asks for a name for the key and what permissions to grant to the key.

Scroll down to the Mail Send accordion item and click on it to reveal the permissions underneath. Drag the slider to the right for the Mail Send permission.

A list of permissions that you can control using a slider bar next to each permission. The "Mail Send" accordion item is expanded to reveal the "Mail Send" permission underneath.

Scroll to the bottom of the form and click on Create & View. The API key will now be displayed on your screen. You will not be able to retrieve the API key again once you leave this screen.

Copy the secret somewhere safe.

API key is displayed to copy and store elsewhere. Key will not be shown again.

With the sender verified and the API Key created, you’re ready to write some code!

Add Twilio SendGrid to your Quartz.NET job

Add the following two SendGrid NuGet packages to your project using the .NET CLI:

dotnet add package SendGrid
dotnet add package SendGrid.Extensions.DependencyInjection
Enter fullscreen mode Exit fullscreen mode

The SendGrid NuGet package will add the core email capabilities. The SendGrid.Extensions.DependencyInjection NuGet package adds extensions to add the ISendGridClient to the dependency injection container.

Update the Program.cs file with the highlighted lines:

using Quartz;
using SendGrid;
using SendGrid.Extensions.DependencyInjection;
using SendGrid.Helpers.Mail;

IHost host = Host.CreateDefaultBuilder(args)
    .ConfigureServices((context, services) =>
    {
        services.AddSendGrid(options =>
            options.ApiKey = context.Configuration.GetValue<string>("SendGridApiKey"));

        services.AddQuartz(q =>
        {
            q.UseMicrosoftDependencyInjectionJobFactory();
            q.ScheduleJob<SendMailJob>(trigger => trigger
                .WithIdentity("SendRecurringMailTrigger")
                .WithSimpleSchedule(s =>
                    s.WithIntervalInSeconds(15)
                    .RepeatForever()
                )
                .WithDescription("This trigger will run every 15 seconds to send emails.")
            );
        });

        services.AddQuartzHostedService(options =>
        {
            // when shutting down we want jobs to complete gracefully
            options.WaitForJobsToComplete = true;
        });
    })
    .Build();

await host.RunAsync();

class SendMailJob : IJob
{
    private readonly ISendGridClient sendGridClient;
    private readonly ILogger logger;

    public SendMailJob(ISendGridClient sendGridClient, ILogger<SendMailJob> logger)
    {
        this.sendGridClient = sendGridClient;
        this.logger = logger;
    }

    public async Task Execute(IJobExecutionContext context)
    {
        var msg = new SendGridMessage()
        {
            From = new EmailAddress("[REPLACE WITH YOUR EMAIL]", "[REPLACE WITH YOUR NAME]"),
            Subject = "Sending with Twilio SendGrid is Fun",
            PlainTextContent = "and easy to do anywhere, especially with C# .NET"
        };
        msg.AddTo(new EmailAddress("[REPLACE WITH DESIRED TO EMAIL]", "[REPLACE WITH DESIRED TO NAME]"));
        var response = await sendGridClient.SendEmailAsync(msg);

        // A success status code means SendGrid received the email request and will process it.
        // Errors can still occur when SendGrid tries to send the email. 
        // If email is not received, use this URL to debug: https://app.sendgrid.com/email\_activity 
        logger.LogInformation(response.IsSuccessStatusCode ? "Email queued successfully!" : "Something went wrong!");
    }
}
Enter fullscreen mode Exit fullscreen mode

The lambda passed into the ConfigureServices method, now receives two parameters: context of type HostBuilderContext and services of type IServiceCollection. The context parameter has been added because it holds a reference to the configuration that will provide the API key for SendGrid.

services.AddSendGrid will add the SendGrid client to the dependency injection container. A lambda is passed into the AddSendGrid method to configure the ApiKey option. The SendGridApiKey is pulled from the configuration and stored into options.ApiKey.

Now that SendGrid has been added to the dependency injection container, ISendGridClient will be injected into the constructor of SendMailJob and stored in a field.

The Execute method uses the SendGrid APIs to construct an email and submit it to the SendGrid service.

Update the placeholder strings:

  • replace [REPLACE WITH YOUR EMAIL] with the email address you verified with your SendGrid account.
  • replace [REPLACE WITH YOUR NAME] with your name.
  • replace [REPLACE WITH DESIRED TO EMAIL] with the email address you want to send an email towards.
  • replace [REPLACE WITH DESIRED TO NAME] with the recipient's name.

Save the Program.cs file.

When the SendGrid API responds with a successful HTTP status code, that does not necessarily mean that the email was successfully received by the recipient. It does however mean that the SendGrid API has accepted your request and will send the email. If the recipient does not receive the email, you can use the Email Activity Feed to find out why.

The project now depends on the SendGridApiKey configuration element, but it hasn't been configured anywhere. You can use .NET user secrets to configure sensitive configuration such as the API key. ** **

Add the user secrets NuGet packages using the .NET CLI:

dotnet add package Microsoft.Extensions.Configuration.UserSecrets
Enter fullscreen mode Exit fullscreen mode

Initialize user secrets for your project using the .NET CLI:

dotnet user-secrets init
Enter fullscreen mode Exit fullscreen mode

In some cases, user secrets have already been initialized. If that's the case, you will see a warning that your project has already been initialized with a UserSecretsId. You can safely disregard this warning.

Run the following command to set the API key as a user secret:

dotnet user-secrets set SendGridApiKey [PASTE IN YOUR SENDGRID API KEY]
Enter fullscreen mode Exit fullscreen mode

The user secrets configuration will override other configuration sources such as the appsettings.json file, but the user secrets will only be loaded when the environment is set as "Development". To do this, you can set an environment variable "DOTNET_ENVIRONMENT" to "Development". You can also pass it in as a command-line argument like this:

dotnet run --environment Development
Enter fullscreen mode Exit fullscreen mode

Run the project using the command above. The recurring output should look like this:

info: System.Net.Http.HttpClient.InjectableSendGridClient.LogicalHandler[100]
      Start processing HTTP request POST https://api.sendgrid.com/v3/mail/send
info: System.Net.Http.HttpClient.InjectableSendGridClient.ClientHandler[100]
      Sending HTTP request POST https://api.sendgrid.com/v3/mail/send
info: System.Net.Http.HttpClient.InjectableSendGridClient.ClientHandler[101]
      Received HTTP response after 15.3421ms - Accepted
info: System.Net.Http.HttpClient.InjectableSendGridClient.LogicalHandler[101]
      End processing HTTP request after 15.4427ms - Accepted
info: SendMailJob[0]
      Email queued successfully!
Enter fullscreen mode Exit fullscreen mode

The program should now queue an email every 15 seconds and log "Email queued successfully!". Verify that you are receiving the email by opening your mailbox and look for the email with subject "Sending with Twilio SendGrid is Fun".

An email with subject "Sending with Twilio SendGrid is Fun" and body "and easy to do anywhere, especially with C# .NET"

User secrets are a great way to store secrets and load them into your configuration with .NET. User secrets prevent you from accidentally checking in your secrets to source control by storing the secrets outside your project source. It is important to note that these secrets are still stored in plain text, just elsewhere in your personal folder.

Conclusion

Quartz.NET is an open-source job scheduler for .NET. You built a .NET application that sends emails on a recurring schedule by integrating Quartz.NET and the SendGrid API.

Using what you learned here, you can also start integrating SendGrid into your ASP.NET Core applications. Twilio has a lot of other products you can integrate. Check out this tutorial on how to receive text messages using Twilio Programmable SMS and forward them using SendGrid emails with C# and ASP.NET Core.

Additional resources

Check out the following resources for more information on the topics and tools presented in this tutorial:

Quartz.NET website and documentation – You used the minimum amount of code to send emails on a recurring basis using Quartz, but there is a lot more to configure and learn at Quartz' website to get the most out of it.

SendGrid .NET SDK source code on GitHub – The SendGrid .NET SDK you used in this tutorial is open source! This GitHub repository contains helpful documentation on how to integrate with SendGrid using .NET. You can read the source code to better understand what's happening or submit issues if something isn't working as expected.

Worker Services in .NET on Microsoft Docs – Learn more about worker services and how to use it at Microsoft's documentation.

Source Code to this tutorial on GitHub - Use this source code if you run into any issues, or submit an issue on this GitHub repo if you run into problems.

Niels Swimberghe is a Belgian software engineer and technical content creator at Twilio, working from the United States. Get in touch with Niels on Twitter @RealSwimburger and follow Niels’ personal blog on .NET, Azure, and web development at swimburger.net.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .