What's new in the Twilio helper library for ASP.NET (v6.0.0 - August 2022)

August 17, 2022
Written by
Reviewed by

What's new in the Twilio helper library for ASP.NET (v6.0.0 - August 2022)

The Twilio helper library for ASP.NET (Twilio.AspNet) is a community-driven open-source project to make integrating Twilio with ASP.NET easier, for both ASP.NET Core and ASP.NET MVC on .NET Framework. The library helps you achieve common use cases and with the release of version 6, we're expanding the library's capabilities and improving existing features.

Wondering what was previously introduced? You can read about v5.73.0 and prior releases here.

What's new in Twilio.AspNet v6.0.0

V6.0.0 is a major release of the Twilio.AspNet library because it contains breaking changes. Starting from this release, version numbers do not match the version of the Twilio .NET SDK.

Here's an overview of the changes:

🎉 NEW FEATURES  

  • You can now add the TwilioRestClient to ASP.NET Core's dependency injection (DI) container, using the .AddTwilioClient method. This Twilio client will use an HttpClient provided by the HTTP client factory. You can find more information about this new feature later in the article.

🙌 ENHANCEMENTS

  • Big breaking change to the [ValidateRequest] attribute. The attribute no longer accepts parameters or properties. Instead, you have to configure the request validation elsewhere. In ASP.NET Core, you can configure request validation using .NET Configuration or using code. In ASP.NET MVC on .NET Framework, you can configure request validation in the Web.config file using the twilio/requestValidation configuration element, or using app settings. You can find more information about this enhancement later in the article.
  • We migrated the build process from AppVeyor to GitHub Actions. You can now see how we build, test, package, and push the release to nuget.org using the workflows under the Actions tab in GitHub
  • The README.md file has been updated to document the new features and improvements.

Before v6.0.0, there was another small release, v5.77.0 which included the following changes:

  • Twilio.AspNet.Core and Twilio.AspNet.Common now use .NET Standard 2.0 and dropped older .NET Standard versions.
  • Twilio.AspNet.Mvc now targets .NET 4.6.2.
  • The Microsoft.AspNetCore.Mvc.Core dependency has been updated to a more recent version. For newer versions of .NET, a framework dependency is used instead. This updated dependency also updated the transitive dependency on Microsoft.AspNetCore.Http. This was done because the old version of Microsoft.AspNetCore.Http contained a vulnerability (CVE-2020-1045).
  • Twilio.AspNet.Core and Twilio.AspNet.Mvc now depend on version 5.77.0 of the Twilio package.

Add Twilio API client to the dependency injection container

Background on TwilioClient and TwilioRestClient

The Twilio .NET SDK provides a static TwilioClient class to authenticate:

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

string accountSid = Environment.GetEnvironmentVariable("TWILIO_ACCOUNT_SID");
string authToken = Environment.GetEnvironmentVariable("TWILIO_AUTH_TOKEN");

TwilioClient.Init(accountSid, authToken);

This is the simplest way to authenticate, but it is better to use API keys as documented here.

Once initialized, you can interact with the API resource classes, for example, here's how you can send an SMS using the MessageResource class.

MessageResource.Create(
    body: "Ahoy!",
    from: new PhoneNumber("+15017122661"),
    to: new PhoneNumber("+15558675310")
);

This is the quick and easy approach you will find in most of our documentation, however, there's another way. Instead of using the static TwilioClient class, you can create a TwilioRestClient and pass it in as a parameter when interacting with the API resource classes.

Here's what that would look like for the same SMS example:

var twilioClient = new TwilioRestClient(accountSid, authToken);

MessageResource.Create(
    body: "Ahoy!",
    from: new PhoneNumber("+15017122661"),
    to: new PhoneNumber("+15558675310"),
    client: twilioClient
);

You may be wondering, why would you want to use the TwilioRestClient instead of the static TwilioClient?

  • You cannot have multiple instances or any instances of a static class. So if your application needs to authenticate with multiple Twilio accounts, like in a multi-tenant app, you cannot do that with the static TwilioClient, but you can support this scenario using the TwilioRestClient.
  • .NET developers generally don't like using static classes because it makes their code less maintainable and harder to test. By using the TwilioRestClient class and the ITwilioRestClient interface, you can use the inversion of control (IoC) principle and more easily mock the clients which makes it easier to test.

The static TwilioClient class uses the TwilioRestClient class itself, but it stores it as a singleton.

New: Add TwilioRestClient as a service

Now that you know how to use the TwilioRestClient and why you'd want to, let's take a look at the new feature that is introduced in v6.0.0.

You can now add the TwilioRestClient to ASP.NET Core's services using the .AddTwilioClient method:

using Twilio.AspNet.Core;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddTwilioClient();

Once the client is added, you can request  ITwilioRestClient  and TwilioRestClient via dependency injection. For example, here's how you can receive the client via the constructor of a controller, and then send the same SMS as before:

using Microsoft.AspNetCore.Mvc;
using Twilio.Clients;
using Twilio.Rest.Api.V2010.Account;
using Twilio.Types;

public class SmsController : Controller
{
    private readonly ITwilioRestClient twilioClient;

    public SmsController(ITwilioRestClient twilioClient)
    {
        this.twilioClient = twilioClient;
    }
    
    public async Task<IActionResult> Send()
    {
        await MessageResource.CreateAsync(
            body: "Ahoy!",
            from: new PhoneNumber("+15017122661"),
            to: new PhoneNumber("+15558675310"),
            client: twilioClient
        );

        return Ok();
    }
}

Constructors aren't the only place you can inject dependencies. You can inject dependencies into controller actions, views, endpoints, and more.

This feature is only for ASP.NET Core and will not be added into ASP.NET on .NET Framework because there's no built-in DI container in the old framework.

Before you can run this code, you need to configure the Twilio client, which is done through standard .NET configuration. Here's what the configuration would look like in JSON:

{
  "Twilio": {
    "Client": {
      "AccountSid": "[YOUR_ACCOUNT_SID]",
      "AuthToken": "[YOUR_AUTH_TOKEN]",
      "ApiKeySid": "[YOUR_API_KEY_SID]",
      "ApiKeySecret": "[YOUR_API_KEY_SECRET]",
      "CredentialType": "[Unspecified|AuthToken|ApiKey]",
      "Region": null,
      "Edge": null,
      "LogLevel": null
    }
  }
}

A couple of notes:

  • You can configure Twilio:AuthToken which Twilio:Client:AuthToken will fall back to.
  • Twilio:Client:CredentialType has the following valid values: Unspecified, AuthToken, or ApiKey.
    • When using AuthToken, you only need to configure the AccountSid and the AuthToken.
    • When using ApiKey, you only need to configure the ApiKeySid and the ApiKeySecret.
  • Twilio:Client:CredentialType is optional and defaults to Unspecified.
    • If Unspecified, whether you configured an API Key or an Auth Token will be detected.
    • If Unspecified and the API Key and Auth Token are both configured, the API Key will be used as it is the preferred way to authenticate.

While the example above shows the JSON representation of the configuration, you can use any configuration provider plugged into your application. The AuthToken and ApiKeySecret in particular should not be stored in your appsettings.json file, but in your user-secrets, in a vault service, or in environment variables instead.

If you do not wish to configure the Twilio client using configuration, you can do so through code:

builder.Services
  .AddTwilioClient((serviceProvider, options) =>
  {
    options.AccountSid = Environment.GetEnvironmentVariable("TWILIO_ACCOUNT_SID");    
    options.AuthToken = Environment.GetEnvironmentVariable("TWILIO_AUTH_TOKEN");
    options.ApiKeySid = Environment.GetEnvironmentVariable("TWILIO_API_KEY_SID");
    options.ApiKeySecret = Environment.GetEnvironmentVariable("TWILIO_API_KEY_SECRET");
    options.Edge = null;
    options.Region = null;
    options.LogLevel = null;
    options.CredentialType = CredentialType.Unspecified;
  });

Do not hard-code your Auth Token or API Key secret into code and do not check them into source control. We recommend using the Secrets Manager for local development. Alternatively, you can use environment variables, a vault service, or other more secure techniques.

Improved HTTP client

TwilioClient and TwilioRestClient use the HttpClient class to send HTTP requests to the Twilio APIs.
Unless you pass in your own HTTP client to the TwilioRestClient constructor, it will create a new HttpClient for every instance. Since the TwilioClient holds on to a singleton of TwilioRestClient, TwilioClient will use a single HttpClient until you restart your application which can lead to DNS stalenes. On the other hand, if you create a new TwilioRestClient for every API call, a new HttpClient will also be created for every API call which could lead to socket exhaustion.

By using TwilioRestClient instead of the static TwilioClient, you already avoid the DNS staleness issues, but you could still run out of network sockets. To avoid this issue and improve performance,  .AddTwilioClient uses the HTTP client factory APIs to retrieve an HttpClient and supplies it to the TwilioRestClient constructor.

In short, using the TwilioRestClient configured by .AddTwilioClient is more reliable and performant.

HttpClient objects created by the HTTP client factory are also configured to log HTTP requests. You can control the log level via configuration using the following log categories: System.Net.Http.HttpClient.Twilio.LogicalHandler and System.Net.Http.HttpClient.Twilio.ClientHandler.

Here's how you set the log level to Information using appsettings.json:

{
  "Logging": {
    "LogLevel": {
      …,
      "System.Net.Http.HttpClient.Twilio.LogicalHandler": "Information",
      "System.Net.Http.HttpClient.Twilio.ClientHandler": "Information"
    }
  },
  …
}

If for some reason you do need to change the way the HttpClient is supplied to TwilioRestClient, you can pass in a callback method to .AddTwilioClient. You need this if you want to use a forward proxy.

Here's an example that creates a new HttpClient for every instance of TwilioRestClient that is requested using DI:

using Twilio.AspNet.Core;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddTwilioClient(provider => new HttpClient())

The above code is for the sake of demonstration and could lead to port exhaustion issues again. When you supply your own HttpClient, we still recommend you follow the best practices by using the HTTP client factory APIs.

Validate HTTP requests coming from Twilio

Your web application has to be publicly available for Twilio to send webhook requests to it. However, this means that anyone can send HTTP requests to your application which could be exploited. Luckily, you can secure your webhooks by validating the X-Twilio-Signature HTTP header.

To make request validation easier and more closely integrated with MVC, Twilio.AspNet provides the [ValidateRequest] attribute. Before v6.0.0, you could apply the attribute in places like controllers and actions like this:

using Microsoft.AspNetCore.Mvc;
using Twilio.AspNet.Core;

public class SmsController : Controller
{
    // ⚠️ old way of validating from before pre-v6.0.0, do not use 
    [ValidateRequest("[YOUR_AUTH_TOKEN]", urlOverride: "https://??????.ngrok.io/sms")]
    public void Index() {}
}

Now any requests made to the SmsController.Index action would first be validated by the ValidateRequest attribute and if the request did not originate from Twilio, an HTTP 403 Forbidden status code would be responded with.

This worked great, but you had to configure the request validation in code, which introduced some challenges:

  • Hard-coding the auth token into code meant that this sensitive secret would be embedded in the DLL-files and also tracked in source control systems, both of which increased the chance of leaking the auth token.
  • Depending on where you're deploying your application (dev/staging/production environments), your configuration would be hard-coded and could not be changed. Each environment may require a different configuration, but there's no easy way to do that.

To resolve these issues, we had to remove the constructor parameters and properties from the ValidateRequest attribute and use external configuration to configure request validation.

Going forward from v6.0.0, use the ValidateRequest attribute without any parameters.

using Microsoft.AspNetCore.Mvc;
using Twilio.AspNet.Core;

public class SmsController : Controller
{
    [ValidateRequest]
    public void Index() {}
}

Next, you'll need to configure request validation, but this is done differently depending on whether you're using ASP.NET Core or ASP.NET MVC on .NET Framework.

On ASP.NET Core, add .AddTwilioRequestValidation() to your startup.

using Twilio.AspNet.Core;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddTwilioRequestValidation();

Then use .NET configuration to configure the request validation. Here's the JSON representation of the configuration:

{
  "Twilio": {
    "RequestValidation": {
      "AuthToken": "[YOUR_AUTH_TOKEN]",
      "AllowLocal": true,
      "BaseUrlOverride": "https://??????.ngrok.io"
    }
  }
}

A couple of notes about the configuration:

  • You can configure Twilio:AuthToken which Twilio:RequestValidation:AuthToken will fall back to.
  • AllowLocal will skip validation when the HTTP request originated from localhost.
  • Use BaseUrlOverride in case your app is behind a reverse proxy or a tunnel like ngrok. The path of the current request will be appended to the BaseUrlOverride for request validation.

While the example above shows the JSON representation of the configuration, you can use any configuration provider plugged into your application. The AuthToken in particular should not be stored in your appsettings.json file. Instead, use user-secrets, a vault service, or environment variables instead.

You can also configure request validation using code:

builder.Services
  .AddTwilioRequestValidation((serviceProvider, options) =>
  {
    options.AuthToken = Environment.GetEnvironmentVariable("TWILIO_AUTH_TOKEN");
    options.AllowLocal = true;
    options.BaseUrlOverride = "https://??????.ngrok.io";
  });

Do not hard-code your Auth Token into code and do not check them into source control. We recommend using the Secrets Manager for local development. Alternatively, you can use environment variables, a vault service, or other secure techniques.

That's it! Now you can use external configuration like JSON files, environment variables, user-secrets, etc. to configure request validation. This makes your application easier to deploy to other environments.

For ASP.NET MVC on .NET Framework, you can configure request validation using the Web.config file using the twilio/requestValidation configuration element:

<?xml version="1.0" encoding="utf-8"?>
<configuration>
    <configSections>
      <sectionGroup name="twilio" type="Twilio.AspNet.Mvc.TwilioSectionGroup,Twilio.AspNet.Mvc">
        <section name="requestValidation" type="Twilio.AspNet.Mvc.RequestValidationConfigurationSection,Twilio.AspNet.Mvc"/>
      </sectionGroup>
    </configSections>
    <twilio>
       <requestValidation 
         authToken="[YOUR_AUTH_TOKEN]"
         baseUrlOverride="https://??????.ngrok.io"
         allowLocal="true"
       />
    </twilio>
</configuration>

Alternatively, you can configure request validation using app settings:

<?xml version="1.0" encoding="utf-8"?>
<configuration>
    <appSettings>
      <add key="twilio:requestValidation:authToken" value="[YOUR_AUTH_TOKEN]"/>
      <add key="twilio:requestValidation:baseUrlOverride" value="https://??????.ngrok.io"/>
      <add key="twilio:requestValidation:allowLocal" value="true"/>
    </appSettings>
</configuration>

Now that request validation is configured, you can apply the ValidateRequest attribute to your MVC apps globally, regions, controllers, and actions to protect your endpoints.

Go use the shiny new bits

You can take advantage of these new features and enhancements now by installing the latest version of the Twilio helper library for ASP.NET. You can find the installation instructions in the readme of the Twilio.AspNet GitHub repository. If you like this library, consider giving it a star on the GitHub repo. Also you are welcome to submit an issue if you run into problems.

We can't wait to see what you'll build with Twilio.AspNet. Let us know on social media and don't forget to mention @TwilioDevs and @RealSwimburger on Twitter or LinkedIn.

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.