How to better configure C# and .NET applications for SendGrid
Time to read: 11 minutes
There are a hundred different ways to provide configuration to your applications. For almost any programming language, you can use environment variables and .env files, but configuration can also be stored in other file formats like JSON, YAML, TOML, XML, INI, and the list keeps on going. However, in some scenarios configuration isn't pulled from files, but instead is pulled from Azure Key Vault, HashiCorp Vault, or a similar vault service. It is rare that your configuration will come from a single source. Luckily, there are APIs in .NET that can help you grab configuration from multiple sources and merge them together.
In this tutorial, you'll start with an application that sends emails using Twilio SendGrid, where the configuration is fetched directly from the environment variables. You'll then refactor the app to
- fetch configuration from multiple sources, specifically, JSON files, user-secrets, environment variables, and command-line arguments
- bind the configuration to strongly-typed objects
- inject configuration using dependency injection following the options pattern
Prerequisites
You will need these things to follow along with this tutorial:
- An OS that supports .NET (Windows/macOS/Linux)
- Git CLI
- .NET 6 SDK
- A code editor or IDE (I recommend VS Code with the C# plugin, Visual Studio, or JetBrains Rider)
- A free or paid Twilio account (If you register here, you'll receive $10 in Twilio credit when you upgrade to a paid account!)
- A Twilio SendGrid account. Sign up here to send up to 100 emails per day completely free of charge.
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.
Configure 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
To quickly get started, you will use Single Sender Verification for this tutorial. This verifies that you own the email address that the application will send emails from. Single Sender Verification is great for testing purposes, but it is not recommended for production.
To set up Single Sender Verification, click the Settings tab in the left menu. Once the settings tab opens, click Sender Authentication.
Then, click Get Started under the Single Sender Verification section.
This opens a form in the right-side panel. Fill out the form with your information and email address.
Click Create after filling out the form. Another panel will appear on the right, asking you to confirm your email address in your email inbox.
Go to your personal email inbox, open the email from SendGrid, and click Verify Single Sender.
Your email address has been verified. You can now use it to send emails!
Create a SendGrid API Key to send emails
Back on the SendGrid website, click API Keys under the Settings tab, then click Create API Key in the top right-hand corner. This opens another form in the right-side panel.
Give your API Key a meaningful 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.
Next, click Restricted Access.
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.
Scroll to the bottom of the form and click Create & View. The API key will now be displayed on your screen.
With the sender verified and the API Key created, you’re ready to write some code!
Get configuration directly from environment variables
To get the project up and running quickly on your machine, open your preferred shell, and use the following git command to clone the source code:
Then navigate into the SendGridOptionsPattern folder using cd SendGridOptionsPattern
. This project is a console application with only one C# file, Program.cs, which has the following code:
The application pulls the SendGrid API Key, as well as the email address and name for the sender and recipient from the environment variables. The code then creates a new SendGridClient
with the API key, creates an email message using SendGridMessage
, and then sends the email.
Before you can run this application, you'll need to configure the environment variables that the project depends on. Back in your shell, set these environment variables using the following commands.
If you're using Bash or a similar shell:
If you're using PowerShell:
If you're using CMD:
Once the environment variables have been set, you can run the project using the following .NET CLI command:
The application will send an email from your verified email address to the recipient.
Pulling the configuration from the environment variables works great, but instead of using environment variables, you could've simply hardcoded all the configuration, right?
Many of these configuration elements, especially the API Key, are sensitive secrets which you do not want to share with others. By pulling them from external configuration like the environment variables, you ensure that you don't accidentally check them into your public source code for everyone to see. And whenever someone else wants to run the same code, they can configure their own API Key configuration into the environment variables and run it.
This is why in SendGrid samples, we will always use environment variables. Although it's an extra step compared to hard coding configuration, it is the fastest way to get you up and running with SendGrid without compromising your API Key by accident.
However, in .NET, there's a better way to retrieve external configuration, using the ConfigurationBuilder
.
Build your Configuration with the .NET configuration builder
There are many ways to configure .NET applications, and typically the configuration is composed from multiple sources. ASP.NET Core originally introduced APIs to easily load configuration from a bunch of different sources using configuration providers and merge them all together with the ConfigurationBuilder
. These APIs were moved into the Microsoft.Extensions.Hosting NuGet package so it could be used in any .NET application.
Back in your shell, run the following .NET CLI commands to add NuGet packages for the configuration extensions and some of the configuration providers:
- The Microsoft.Extensions.Configuration package contains the main APIs for building configuration and also the JSON configuration provider.
- The Microsoft.Extensions.Configuration.CommandLine package adds a configuration provider that builds configuration from command-line arguments.
- The Microsoft.Extensions.Configuration.EnvironmentVariables package adds a configuration provider that builds configuration from environment variables.
- The Microsoft.Extensions.Configuration.UserSecrets package adds a configuration provider that builds configuration from the Secret Manager.
To start using the configuration APIs, update the Program.cs file with the following code:
The resulting app is the same as before but is now loading configuration from multiple sources instead of directly from the environment variables. The ConfigurationBuilder
is a class that uses the builder pattern so you can chain together multiple configuration providers using the Add…
extension methods. When configuration elements coming from different providers have the same key, the configuration added later to the chain will overwrite the configuration added earlier in the chain. This means that the order in which you add configuration providers matters! In this case:
- User Secrets overwrites configuration from JSON
- Environment Variables overwrites configuration from User Secrets and JSON
- Command-Line Arguments overwrites configuration from User Secrets, JSON, and Environment Variables.
After the configuration is built, the app will grab the configuration elements from the configuration, but notice how the configuration keys now use two different prefixes: SendGrid:
and Email:
.
You can organize your configuration into hierarchical sections by adding colons :
as separators. So in this case, ApiKey
is part of the SendGrid
section, and all the other configuration is part of the Email
section. You can also nest sections as shown in the Email
section. The Email
section contains child sections From
and To
which hold on to the email address and name of the sender and recipient. There are a lot of different ways you could structure your configuration, but how you organize your configuration all depends on your needs.
Now that the application loads configuration from multiple sources, in which source should you store configuration? It depends on the configuration, your use-case, and personal preference:
- Use JSON for configuration that does not contain secrets or other sensitive information.
- Use User Secrets only for local development to configure secrets and other sensitive information.
- Use Environment Variables for environment specific configuration including secrets or other sensitive information. However, User Secrets is preferred for local development. Environment Variables are a powerful source of configuration because all operating systems, container technology, and cloud infrastructure supports them.
- Use Command-Line Arguments to configure execution specific settings.
Create a new file called appsettings.json and add the following JSON:
The appsettings.json file will provide the FromEmailAddress
, FromName
, Subject
, and Body
configuration, even though it would also make sense to pass this configuration in as command-line arguments.
By default, in a console app, the appsettings.json file will not be copied to the output of the project. As a result the configuration provider will not find the underlying source, which means the configuration in your JSON will not be loaded. To make sure the the appsettings.json file is copied to the output, add a Content
node to the csproj-file as shown below:
To start using the Secret Manager, you first need to initialize it for your project using the following command:
Now you can set the SendGrid:ApiKey
configuration like this:
It's likely you would configure the API Key as an environment variable in other environments, but since you're running this locally, the Secret Manager is preferred. So, in this case, you don't need to configure any environment variables.
The application now has configuration for who the email will be sent from, but not who the email will be sent to. Instead of configuring it using one of the previous sources, run the project using the .NET CLI and pass it in as a command-line argument:
Just like before, the recipient will receive an email, but now the subject will be "Ahoy from JSON!".
Run the command again, but overwrite the Email:Subject
configuration using a command-line argument:
Now the recipient will receive an email with the subject "Ahoy from the CLI".
The end result may be the same, but your application is a lot more flexible now because it can be configured in many different ways. However, these are not the only configuration providers. Microsoft has 5 more configuration providers, and if those don't meet your needs, you can also develop your own provider or use someone else's.
Bind configuration to strongly-typed objects
You just learned how to use the configuration builder to get configuration from multiple sources, but the amount of configuration can quickly get out of hand. A more manageable solution to this is to bind your configuration to strongly-typed objects.
To starting binding configuration, add the Microsoft.Extensions.Configuration.Binder NuGet package with the following command:
Update the Program.cs file with the code below:
At the bottom of Program.cs, there is a new class which has properties with names that match your configuration. By matching the name of the properties and the keys of the configuration, you will be able to bind the configuration to instances of said class. You can grab sections from the configuration using config.GetSection("YourSectionName")
and then bind the section to strongly-typed objects using .Get<YourClass>()
. This way, the Email
section is bound to instances of EmailOptions
which is then used to send the email as before.
For the EmailOptions
class, the From
and To
properties are defined using the EmailAddress
class provided by the SendGrid library. The EmailAddress
class contains two properties: Email
and Name
. Because these properties names match the names of your configuration, even these nested objects will be bound.
If you run the project now, you will continue to get the same result:
Swap configuration builder with the default host builder
If you've used the ASP.NET templates, you may notice that the configuration has already been set up without any ConfigurationBuilder
code. That's because ASP.NET templates use the default Web Host which sets up the default configuration, logging, dependency injection, web related functionality, and more.
If you want the same configuration, logging, and dependency injection from the web host builder, but not the web related functionality, you can use the Generic Host instead of the Web Host.
Add the Microsoft.Extensions.Hosting NuGet package using the .NET CLI:
In Program.cs, Replace the using
statements and the ConfigurationBuilder
code with the following code:
Instead of building the configuration yourself, the project now uses the defaults that come with the Generic Host. The code then retrieves the configuration from the dependency injection container (host.Services
), and continues using the configuration object as before.
The default Generic Host builds configuration slightly differently. More specifically, your user secrets will only be loaded if the Environment
is configured as Development
. There are multiple ways to configure the environment, but for this tutorial, pass in the Environment
argument when you run your project, like this:
Get configuration from dependency injection using the options pattern
Now that you're using the Generic Host, you can start using the dependency injection (DI) that comes with it, just like you can on ASP.NET applications.
Replace your existing code in Program.cs with the following code:
You can use the ConfigureServices
method on the host builder and pass in a lambda to add more services to the DI container. In this lambda, the Email
configuration section is added as configuration using services.Configure<YourOptions>
. As a result, your configuration will be injected wherever DI is supported by adding a parameter of type IOptions<YourOptions>
to your constructor or method signature. Microsoft calls this the Options pattern.
After configuring the options, the program will add the EmailSender
class to the CI container. EmailSender
is a new class that will be responsible for sending Emails. Now that the DI container has been built, you can retrieve the services from it. So now you have your EmailOptions
injected anywhere that supports DI.
Now, the code will retrieve an instance of EmailSender
from the DI container, which is used to send an email using the SendEmail
method.
When EmailSender
is created by the DI container, the container injects an instance of IOptions<EmailOptions>
into the constructor of EmailSender
. EmailSender
grabs the message options using the .Value
property and uses it in the SendEmail
method to send an email.
Add SendGridClient to the Dependency Injection Container
You've made your application more flexible and configurable, but you can also further integrate SendGrid into your application. The SendGrid SDK for .NET has an extra NuGet package that will add the SendGrid client to the dependency injection container.
Run the following command to add the SendGrid DI package:
Then update the Program.cs file like this:
The application adds the SendGrid client to the DI container using the services.AddSendGrid
method.
To configure the options of SendGrid, another lambda is used and passed into AddSendGrid
. The lambda-inception can be tricky, so make sure to verify all your parentheses match the code above.
Instead of constructing the SendGridClient
as you did before, the ISendGridClient
will now be injected into the constructor of the EmailSender
class, and then used to send an email as before.
Why so complicated?
You went from approximately 12 straightforward lines of code, to approximately 26 lines of code with classes, interfaces, lambda expressions, generic types, and more.
Why complicate the code so much?
For a small sample where you want to go from point A to point B as fast as possible, you can use environment variables and be done with it, but for real-world applications, although it requires extra setup up-front, the techniques from this tutorial will decrease the amount of code you need to write and increase the flexibility and maintainability of your solution.
Better .NET configuration
After following this tutorial, you have a learned how to use .NET's configuration APIs to:
- securely configure secrets and sensitive information
- retrieve configuration from multiple sources
- override configuration from one source with another source
- add configuration to the dependency inject container
- inject configuration into your classes
Congratulations on making it to the end of a long post about configuration! 👏
You can start including these techniques for SendGrid, Twilio, or really any .NET applications! If you run into any problems, you can refer to the source code for this tutorial on GitHub, or you can submit an issue on the GitHub repository. In addition to the end result, you can find each step in a separate branch (Step 1, 2, 3, 4, 5, and 6)
Let me know what you're working on. I can't wait to see what you build!
Niels Swimberghe is a Belgian American software engineer and technical content creator at Twilio. Get in touch with Niels on Twitter @RealSwimburger and follow Niels’ personal blog on .NET, Azure, and web development at swimburger.net.
Related Posts
Related Resources
Twilio Docs
From APIs to SDKs to sample apps
API reference documentation, SDKs, helper libraries, quickstarts, and tutorials for your language and platform.
Resource Center
The latest ebooks, industry reports, and webinars
Learn from customer engagement experts to improve your own communication.
Ahoy
Twilio's developer community hub
Best practices, code samples, and inspiration to build communications and digital engagement experiences.