How to better configure C# and .NET applications for Twilio

Niels Swimburger.NET 🍔 - Jun 27 '22 - - Dev Community

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

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. Though, in some scenarios configuration isn't pulled from files, but instead is pulled from a service like 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 text messages using Twilio Programmable SMS, 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:

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.

Get Your Twilio Configuration

You'll need some configuration elements from Twilio to send text messages from the C# application. Open the Twilio Console in your browser and take note of your Twilio Account SID , Auth Token , and Twilio phone number located at the bottom left of the page.

Account Info box holding 3 read-only fields: Account SID field, Auth Token field, and Twilio phone number field.

For any of the upcoming commands and code, replace the following placeholders like this:

  • Replace [TWILIO_ACCOUNT_SID], [TWILIO_AUTH_TOKEN], and [YOUR_TWILIO_PHONE_NUMBER] with the Account SID , Auth Token , and your Twilio Phone Number you took note of earlier.
  • Replace [RECIPIENT_PHONE_NUMBER] with the phone number you want to text. If you are using a trial Twilio account, you can only send text messages to Verified Caller IDs. Verify your phone number or the phone number you want to message if it isn't on the list of Verified Caller IDs.

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:

git clone https://github.com/Swimburger/TwilioOptionsPattern.git --branch Step1
Enter fullscreen mode Exit fullscreen mode

Then navigate into the TwilioOptionsPattern folder using cd TwilioOptionsPattern. This project is a console application with only one C# file, Program.cs, which has the following code:

using Twilio;
using Twilio.Rest.Api.V2010.Account;

var twilioAccountSid = Environment.GetEnvironmentVariable("TwilioAccountSid");
var twilioAuthToken = Environment.GetEnvironmentVariable("TwilioAuthToken");
var fromPhoneNumber = Environment.GetEnvironmentVariable("FromPhoneNumber");
var toPhoneNumber = Environment.GetEnvironmentVariable("ToPhoneNumber");

TwilioClient.Init(
    username: twilioAccountSid, 
    password: twilioAuthToken
);

MessageResource.Create(
    from: fromPhoneNumber,
    to: toPhoneNumber,
    body: "Ahoy!"
);
Enter fullscreen mode Exit fullscreen mode

The application pulls the Twilio Account SID, Auth Token, Twilio Phone Number (FromPhoneNumber), and the recipient phone number (ToPhoneNumber) from the environment variables, then uses the credentials to authenticate with the Twilio API, and then sends a text message saying "Ahoy!".

The application uses the Twilio SDK for C# and .NET to authenticate with Twilio and send SMS. The Twilio NuGet package has been added into the project as part of the source code you cloned. You can find this dependency in the csproj-file.

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:

export TwilioAccountSid=[TWILIO\_ACCOUNT\_SID]
export TwilioAuthToken=[TWILIO\_AUTH\_TOKEN]
export FromPhoneNumber=[YOUR\_TWILIO\_PHONE\_NUMBER]
export ToPhoneNumber=[RECIPIENT\_PHONE\_NUMBER]
Enter fullscreen mode Exit fullscreen mode

If you're using PowerShell:

$Env:TwilioAccountSid = "[TWILIO\_ACCOUNT\_SID]"
$Env:TwilioAuthToken = "[TWILIO\_AUTH\_TOKEN]"
$Env:FromPhoneNumber = "[YOUR\_TWILIO\_PHONE\_NUMBER]"
$Env:ToPhoneNumber = "[RECIPIENT\_PHONE\_NUMBER]"
Enter fullscreen mode Exit fullscreen mode

If you're using CMD:

set "TwilioAccountSid=[TWILIO\_ACCOUNT\_SID]"
set "TwilioAuthToken=[TWILIO\_AUTH\_TOKEN]"
set "FromPhoneNumber=[YOUR\_TWILIO\_PHONE\_NUMBER]"
set "ToPhoneNumber=[RECIPIENT\_PHONE\_NUMBER]"
Enter fullscreen mode Exit fullscreen mode

Once the environment variables have been set, you can run the project using the following .NET CLI command:

dotnet run
Enter fullscreen mode Exit fullscreen mode

The application will send an SMS from your Twilio Phone Number to the recipient phone number saying "Ahoy!".

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 Auth Token, 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 Twilio configuration into the environment variables and run it.

This is why in Twilio 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 Twilio products.

But 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.

Not only can you use the new Configuration APIs on .NET (Core), you can even use them on .NET Framework. .NET Framework has its own configuration APIs, but they are less intuitive and dated compared to the new Configuration APIs.

Back in your shell, run the following .NET CLI commands to add NuGet packages for the configuration extensions and some of the configuration providers:

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

To start using the configuration APIs, update the Program.cs file with the following code:

using System.Reflection;
using Microsoft.Extensions.Configuration;
using Twilio;
using Twilio.Rest.Api.V2010.Account;

IConfiguration config = new ConfigurationBuilder()
    .AddJsonFile("appsettings.json", optional: true, reloadOnChange: false)
    .AddUserSecrets(Assembly.GetExecutingAssembly(), optional: true, reloadOnChange: false)
    .AddEnvironmentVariables()
    .AddCommandLine(args)
    .Build();

var twilioAccountSid = config["Twilio:AccountSid"];
var twilioAuthToken = config["Twilio:AuthToken"];

var fromPhoneNumber = config["Message:From"];
var toPhoneNumber = config["Message:To"];
var messageBody = config["Message:Body"];

TwilioClient.Init(
    username: twilioAccountSid,
    password: twilioAuthToken
);

MessageResource.Create(
    from: fromPhoneNumber,
    to: toPhoneNumber,
    body: messageBody
);
Enter fullscreen mode Exit fullscreen mode

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.

The optional parameter specifies whether or not the application should throw an exception if the underlying source is missing. The reloadOnChange parameter specifies whether or not to update the configuration when the underlying source changes. For example, if there's an issue in your production application, but for performance reasons, the logging level is set to Warning in appsettings.json, you can change the logging level to Debug. Once you save the file, the application begins writing logs from the Debug level and up, without having to restart your application!

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: Twilio: and Message:.

You can organize your configuration into hierarchical sections by adding colons : as separators. So in this case, AccountSid and AuthToken are part of the Twilio section, and From, To, and Body are part of the Message section. How you organize your configuration will depend 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:

{
  "Message": {
    "From": "[YOUR_TWILIO_PHONE_NUMBER]",
    "Body": "Ahoy from JSON!"
  }
}
Enter fullscreen mode Exit fullscreen mode

The appsettings.json file will provide the Message:From and Message: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:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>...</PropertyGroup>
  <ItemGroup>...</ItemGroup>

  <ItemGroup>
    <Content Include="appsettings.json" CopyToOutputDirectory="Always" />
  </ItemGroup>
</Project>
Enter fullscreen mode Exit fullscreen mode

To start using the Secret Manager, you first need to initialize it for your project using the following command:

dotnet user-secrets init
Enter fullscreen mode Exit fullscreen mode

Now you can set the Twilio:AccountSid and Twilio:AuthToken configuration like this:

dotnet user-secrets set Twilio:AccountSid [TWILIO_ACCOUNT_SID]
dotnet user-secrets set Twilio:AuthToken [TWILIO_AUTH_TOKEN]
Enter fullscreen mode Exit fullscreen mode

It's likely you would configure the Account SID and Auth Token as environment variables 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.

There's only one configuration element left to configure, Message: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:

dotnet run --Message:To [RECIPIENT_PHONE_NUMBER]
Enter fullscreen mode Exit fullscreen mode

Just like before, the recipient will receive a text message, but now saying "Ahoy from JSON!".

Run the command again, but overwrite the Message:Body configuration using a command-line argument:

dotnet run --Message:To [RECIPIENT_PHONE_NUMBER] --Message:Body "Ahoy from the CLI"
Enter fullscreen mode Exit fullscreen mode

Now the recipient will receive an SMS saying "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.

Some operating systems and shells may not allow you to use colons. In that case you can use a double underscore __ as a separator, and .NET will replace it with the colon for you.

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 increase. 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:

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

Update the Program.cs file with the code below:

using System.Reflection;
using Microsoft.Extensions.Configuration;
using Twilio;
using Twilio.Rest.Api.V2010.Account;

IConfiguration config = new ConfigurationBuilder()
    .AddJsonFile("appsettings.json", optional: true, reloadOnChange: false)
    .AddUserSecrets(Assembly.GetExecutingAssembly(), optional: true, reloadOnChange: false)
    .AddEnvironmentVariables()
    .AddCommandLine(args)
    .Build();

var twilioOptions = config.GetSection("Twilio").Get<TwilioOptions>();
TwilioClient.Init(
    username: twilioOptions.AccountSid,
    password: twilioOptions.AuthToken
);

var messageOptions = config.GetSection("Message").Get<MessageOptions>();
MessageResource.Create(
    from: messageOptions.From,
    to: messageOptions.To,
    body: messageOptions.Body
);

public class TwilioOptions
{
    public string AccountSid { get; set; }
    public string AuthToken { get; set; }
}

public class MessageOptions
{
    public string From { get; set; }
    public string To { get; set; }
    public string Body { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

At the bottom of Program.cs, there are two new classes which have 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 .Bind<YourClass>(). This way, the Twilio and Message sections are bound to instances of TwilioOptions and MessageOptions, which are then used to authenticate and send the text message as before.

If you run the project now, you will continue to get the same result:

dotnet run --Message:To [RECIPIENT_PHONE_NUMBER] --Message:Body "Ahoy from the CLI"
Enter fullscreen mode Exit fullscreen mode

If reloadOnChange is enabled on the configuration providers, and the configuration changes in the configuration source, the configuration object will be updated, but any strongly-typed object that has already been bound will not be updated accordingly.

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:

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

In Program.cs, Replace the using statements and the ConfigurationBuilder code with the following code:

using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Twilio;
using Twilio.Rest.Api.V2010.Account;

using IHost host = Host.CreateDefaultBuilder(args).Build();
var config = host.Services.GetRequiredService<IConfiguration>();
Enter fullscreen mode Exit fullscreen mode

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 will build configuration similarly to how you did it yourself, but there are some differences. You can find an overview of the Generic Host defaults in the Microsoft Documentation.

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:

dotnet run --Environment Development --Message:To [RECIPIENT\_PHONE\_NUMBER] --Message:Body "Ahoy from the CLI"
Enter fullscreen mode Exit fullscreen mode

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:

using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Twilio;
using Twilio.Rest.Api.V2010.Account;

using IHost host = Host.CreateDefaultBuilder(args)
    .ConfigureServices((context, services) =>
    {
        services.Configure<TwilioOptions>(context.Configuration.GetSection("Twilio"));
        services.Configure<MessageOptions>(context.Configuration.GetSection("Message"));
        services.AddTransient<MessageSender>();
    })
    .Build();

var twilioOptions = host.Services.GetRequiredService<IOptions<TwilioOptions>>().Value;
TwilioClient.Init(
    username: twilioOptions.AccountSid,
    password: twilioOptions.AuthToken
);

var messageSender = host.Services.GetRequiredService<MessageSender>();
messageSender.SendMessage();

public class MessageSender
{
    private readonly MessageOptions messageOptions;

    public MessageSender(IOptions<MessageOptions> messageOptions)
    {
        this.messageOptions = messageOptions.Value;
    }

    public void SendMessage()
    {
        MessageResource.Create(
            from: messageOptions.From,
            to: messageOptions.To,
            body: messageOptions.Body
        );
    }
}

public class TwilioOptions
{
    public string AccountSid { get; set; }
    public string AuthToken { get; set; }
}

public class MessageOptions
{
    public string From { get; set; }
    public string To { get; set; }
    public string Body { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

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 Twilio and Message configuration sections are 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.

In addition to IOptions<T>, there's also IOptionsSnapshot<T>, IOptionsMonitor<T>, and IOptionsFactory<TOptions> which each have different behavior and use-cases. Learn more about the differences between these options interfaces at Microsoft's documentation.

After configuring the options, the program will add the MessageSender class to the CI container. MessageSender is a new class that will be responsible for sending SMS.

Now that the DI container has been built, you can retrieve the services from it. So now you can get the TwilioOptions using host.Services.GetRequiredService<IOptions<TwilioOptions>>().Value. You can also have your TwilioOptions injected anywhere that supports DI. After retrieving the TwilioOptions object, it is used to authenticate with Twilio.

Then, the code will retrieve an instance of MessageSender from the DI container, which is used to send an SMS using the ​​SendMessage method.

When MessageSender is created by the DI container, the container injects an instance of IOptions<MessageOptions> into the constructor of MessageSender. MessageSender grabs the message options using the .Value property and uses it in the SendMessage method to send an SMS.

You're currently using the static TwilioClient class to authenticate and send messages, but you could add the TwilioRestClient to your DI container and start using that client to make Twilio API calls. Your projects will be easier to test when you avoid using static classes and methods and also use DI.

Better Twilio Authentication

The fastest way to get started with the Twilio API, is to use your Account SID and Auth Token, but there's a better way to authenticate. Twilio recommends creating an API Key, and using the API Key SID and Secret to authenticate. Learn how to strengthen your Twilio Authentication by using sub accounts and API Keys in this tutorial. Once you have created an API key, update the authentication code and TwilioOptions in the Program.cs file like this:

// existing code omitted for brevity

var twilioOptions = host.Services.GetRequiredService<IOptions<TwilioOptions>>().Value;
if (twilioOptions.CredentialType == CredentialType.ApiKey)
{
    TwilioClient.Init(
        username: twilioOptions.ApiKeySid,
        password: twilioOptions.ApiKeySecret,
        accountSid: twilioOptions.AccountSid
    );
}
else
{
    TwilioClient.Init(
        username: twilioOptions.AccountSid,
        password: twilioOptions.AuthToken
    );
}

// existing code omitted for brevity

public enum CredentialType
{
    AuthToken,
    ApiKey
}

public class TwilioOptions
{
    public CredentialType CredentialType { get; set; } 
        = CredentialType.AuthToken;

    public string AccountSid { get; set; }
    public string AuthToken { get; set; }
    public string ApiKeySid { get; set; }
    public string ApiKeySecret { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

Now your application supports authentication with both Auth Tokens and API Keys, and you can easily switch between the two types of credentials. You do have to configure these new options, which you can do like you've done before with the Auth Token and Account SID. To specify the CredentialType option, you can use any configuration source and set the configuration value to "ApiKey". For example, this is how you specify the CredentialType in appsettings.json:

{
  "Twilio": {
    "CredentialType": "ApiKey"
  },
  "Message": {
    "From": "[YOUR_TWILIO_PHONE_NUMBER]",
    "Body": "Ahoy from JSON!"
  }
}
Enter fullscreen mode Exit fullscreen mode

After configuring the ApiKeySid, ApiKeySecret, and CredentialType, the application should continue to work as before using the recommended API Key credentials.

Why so complicated?

You went from approximately 6 straightforward lines of code, to approximately 25 lines of code with classes, interfaces, lambda expressions, generic types, and more. (Count excludes usings, brackets, whitespace, and API Key code.)

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, 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 Twilio, SendGrid, 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!

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