Verifying Phone Number Messaging Capabilities with ASP.NET Core and Twilio Lookup

March 25, 2020
Written by
AJ Saulsberry
Contributor
Opinions expressed by Twilio contributors are their own

verifying-phone-number-capabilities.png

The ability to send and receive SMS and MMS messages programmatically is great, but how do you ensure you’re sending messages to a phone that’s capable of receiving them? If a customer provides you with a phone number when they sign up for an account, is it possible to verify the phone number’s capabilities while you’re verifying they own the phone number?

With Twilio Lookup it is, and it’s easy to integrate this capability into your ASP.NET Core projects. One of the best ways you can use this resource is to use it to determine whether your application should be sending SMS messages or voice messages using the text-to-speech capabilities of the TwiML <Say> verb and Answering Machine Detection. With these tools you can communicate with customers asynchronously using their phone’s capabilities regardless of whether it’s a mobile or landline phone.

In this post you’ll learn how to use the Twilio helper library for .NET to connect to Twilio and the Twilio Lookup API to get information about a phone number in the controller actions of an ASP.NET Core Razor Pages application. You’ll also see how to get carrier information for mobile phone numbers and how to obtain the national and mobile dialing formats for each number.

Prerequisites

You’ll need the following tools and resources to complete the steps in this tutorial:

Some exposure to ASP.NET Core Razor pages and C# will also be helpful. You should also know how to create ASP.NET Core projects using the Visual Studio 2019 user interface or the .NET CLI.

There is a companion repository for this post available on GitHub. It contains the complete source code for the completed project.

Getting and setting your Twilio account credentials

If you don’t already have a Twilio account use the link above to create one. Go to the Twilio Console Dashboard and get the Account SID and Auth Token for the ExplorationOne project (or the project of your choice, if you’ve created additional projects). You can find these credentials on the upper right-hand side of the Dashboard. These are user secrets, so handle them securely.

The following instructions assume you’ve stored your Account SID and Auth Token as environment variables. If you’d like to learn more about doing that you can find instructions in the post: Setting Twilio Environment Variables in Windows 10 with PowerShell and .NET Core 3.0. If you’re using macOS or a Linux distribution, see the associated section of the post How To Set Environment Variables.

Setting up the ASP.NET Core 3.1 Razor Pages project

Create a new ASP.NET Core Web Application called TwilioLookupCarrier as an ASP.NET Core 3.1 Web Application. You won’t need any user authentication for this project. Configure the project for HTTPS. You can create a solution file, or not, according to your own preferences and standard practices.

Add the TwilioLookupCarrier solution/project to source code control.

Add the following NuGet packages to the TwilioLookupCarrier project with the UI or the .NET CLI instructions provided:

Microsoft.Extensions.Logging.Debug
Microsoft.VisualStudio.Web.CodeGeneration.Design
System.Configuration.ConfigurationManager
Twilio

dotnet add package Microsoft.Extensions.Logging.Debug --version 3.1.3
dotnet add package Microsoft.VisualStudio.Web.CodeGeneration.Design --version 3.1.1
dotnet add package System.Configuration.ConfigurationManager --version 4.7.0
dotnet add package Twilio --version 5.38.0

The version numbers above reflect the current versions of these components as of the writing of this post. To get the most recent version of each package, run the command without the --version argument.

Verify the project is working by running it. You should see the minimalist welcome page for Razor Pages Web Applications.

Using your Twilio credentials in the project

Once you’ve signed up for a Twilio account, loading your Twilio credentials using the System.Environment class is easy. Using environment variables to store your credentials helps prevent you from inadvertently checking your user secrets into a publicly-accessible source code repository.

Under the TwilioLookupCarrier project, create a new folder called Models. In the Models folder, create a new C# class file called TwilioSettings.cs and replace the entire contents with the following code:

namespace TwilioLookupCarrier.Models
{
    public class TwilioSettings
    {
        public string AccountSid { get; set; }
        public string AuthToken { get; set; }
    }
}

Since this is a POCO, a “plain ol’ class object”, you won’t need any using directives at all.

Open the Startup.cs file in the project root folder and add the following using declaration at the bottom of the existing list:

using TwilioLookupCarrier.Models;

Add the following code to the ConfigureServices method below the services.AddRazorPages(); statement:

            services.Configure<TwilioSettings>(settings =>
            {
                settings.AccountSid = Environment.GetEnvironmentVariable("TWILIO_ACCOUNT_SID");
                settings.AuthToken = Environment.GetEnvironmentVariable("TWILIO_AUTH_TOKEN");
            });

The Twilio Helper Library that you added as a NuGet package will now be able to access your user secrets from your local machine environment.

Creating a model for phone number info

In the Models folder you just created, add another C# class file called PhoneNumberInfo.cs and replace the entire contents with the following code:

using System.ComponentModel.DataAnnotations;

namespace TwilioLookupCarrier.Models
{
    public class PhoneNumberInfo
    {
        private string _countryCodeSelected;

        [Display(Name = "Issuing Country")]
        [Required]
        public string CountryCodeSelected
        {
            get => _countryCodeSelected;
            set => _countryCodeSelected = value?.ToUpperInvariant();
        }

        [Required]
        [Display(Name = "Phone Number")]
        [MaxLength(18)]
        public string PhoneNumberRaw { get; set; }

        [Display(Name = "Valid Number")]
        public bool Valid { get; set; }
        
        [Display(Name = "Country Code")]
        public string CountryCode { get; set; }

        [Display(Name = "National Dialing Format")]
        public string PhoneNumberFormatted { get; set; }

        [Display(Name = "Mobile Dialing Format")]
        public string PhoneNumberMobileDialing { get; set; }

        public Carrier Carrier { get; set; }
    }

    public class Carrier
    {
        [Display(Name = "Mobile Country Code")] 
        public string MobileCountryCode { get; set; }

        [Display(Name = "Mobile Network Code")] 
        public string MobileNetworkCode { get; set; }

        [Display(Name = "Carrier Name")] 
        public string CarrierName { get; set; }

        [Display(Name = "Carrier Type")] 
        public string CarrierType { get; set; }

        public string ErrorCode { get; set; }
    }
}

This class will be bound to the Razor page used to collect user input and return data to the user, so the System.ComponentModel.DataAnnotations namespace is used to provide the text for the labels on the form and some input constraints.

A phone number provided to the Lookup API without a country code will be assumed by the API to be a US number, but it’s a much better practice to require a country code along with the phone number so the verification can be done explicitly for the intended country. In a production app you’ll probably want to do this with by providing a list of countries conforming to the ISO 3166-1 alpha-2 standard used by the API for 2-character country codes.

Creating a Razor Page for phone number lookup

In the Pages folder, create a new Razor Page called TwilioLookup. Whether you’re using the VS 2019 user interface or the .NET CLI this should create a TwilioLookup.cshtml file and a TwilioLookup.cshtml.cs PageModel file, which will be nested underneath it in the VS 2019 Solution Explorer.

Open the TwilioLookup.cshtml file and replace the entire contents with the following markup:

@page
@model TwilioLookupCarrier.Pages.TwilioLookupModel
@{
  ViewData["Title"] = "TwilioLookup";
}

<h1>TwilioLookup</h1>

<div class="row">
  <div class="col-md-4">
    <form method="post">
      <div asp-validation-summary="ModelOnly" class="text-danger"></div>
      <div class="form-group">
        <label asp-for="PhoneNumberInfo.CountryCodeSelected" class="control-label"></label>
        <input asp-for="PhoneNumberInfo.CountryCodeSelected" class="form-control" autofocus="autofocus" />
        <span asp-validation-for="PhoneNumberInfo.CountryCodeSelected" class="text-danger"></span>
      </div>
      <div class="form-group">
        <label asp-for="PhoneNumberInfo.PhoneNumberRaw" class="control-label"></label>
        <input asp-for="PhoneNumberInfo.PhoneNumberRaw" class="form-control" />
        <span asp-validation-for="PhoneNumberInfo.PhoneNumberRaw" class="text-danger"></span>
      </div>
      <div class="form-group">
        <input type="submit" value="Check" class="btn btn-default" />
      </div>

      <br />
      <h2>Phone Number</h2>
      <hr />
      <div class="form-group">
        <div class="checkbox">
          <label>
            <input asp-for="PhoneNumberInfo.Valid" /> @Html.DisplayNameFor(model => model.PhoneNumberInfo.Valid)
          </label>
        </div>
      </div>
      <div class="form-group">
        <label asp-for="PhoneNumberInfo.CountryCode" class="control-label"></label>
        <input asp-for="PhoneNumberInfo.CountryCode" class="form-control" readonly="readonly" />
        <span asp-validation-for="PhoneNumberInfo.CountryCode" class="text-danger"></span>
      </div>
      <div class="form-group">
        <label asp-for="PhoneNumberInfo.PhoneNumberFormatted" class="control-label"></label>
        <input asp-for="PhoneNumberInfo.PhoneNumberFormatted" class="form-control" readonly="readonly" />
        <span asp-validation-for="PhoneNumberInfo.PhoneNumberFormatted" class="text-danger"></span>
      </div>
      <div class="form-group">
        <label asp-for="PhoneNumberInfo.PhoneNumberMobileDialing" class="control-label"></label>
        <input asp-for="PhoneNumberInfo.PhoneNumberMobileDialing" class="form-control" readonly="readonly" />
        <span asp-validation-for="PhoneNumberInfo.PhoneNumberMobileDialing" class="text-danger"></span>
      </div>
      <br />
      <h2>Carrier</h2>
      <hr />
      <div class="form-group">
        <label asp-for="PhoneNumberInfo.Carrier.MobileCountryCode" class="control-label"></label>
        <input asp-for="PhoneNumberInfo.Carrier.MobileCountryCode" class="form-control" readonly="readonly" />
        <span asp-validation-for="PhoneNumberInfo.Carrier.MobileCountryCode" class="text-danger"></span>
      </div>
      <div class="form-group">
        <label asp-for="PhoneNumberInfo.Carrier.MobileNetworkCode" class="control-label"></label>
        <input asp-for="PhoneNumberInfo.Carrier.MobileNetworkCode" class="form-control" readonly="readonly" />
        <span asp-validation-for="PhoneNumberInfo.Carrier.MobileNetworkCode" class="text-danger"></span>
      </div>
      <div class="form-group">
        <label asp-for="PhoneNumberInfo.Carrier.CarrierName" class="control-label"></label>
        <input asp-for="PhoneNumberInfo.Carrier.CarrierName" class="form-control" readonly="readonly" />
        <span asp-validation-for="PhoneNumberInfo.Carrier.CarrierName" class="text-danger"></span>
      </div>
      <div class="form-group">
        <label asp-for="PhoneNumberInfo.Carrier.CarrierType" class="control-label"></label>
        <input asp-for="PhoneNumberInfo.Carrier.CarrierType" class="form-control" readonly="readonly" />
        <span asp-validation-for="PhoneNumberInfo.Carrier.CarrierType" class="text-danger"></span>
      </div>

    </form>
  </div>
</div>

<div>
  <a asp-controller="Home" asp-action="Index">Back to Home</a>
</div>

@section Scripts {
  @{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}

You’ll see red lint all over the place. This is because the data model isn’t bound to the page yet. You can ignore the lint for the moment.

Open the TwilioLookup.cshtml.cs file and take a quick look at the nominal functional code for a Razor Page PageModel. Then replace the entire contents of the file with the following C# code:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Options;
using Twilio;
using Twilio.Exceptions; // Needed to catch PhoneLookup.DLL errors.
using Twilio.Rest.Lookups.V1;
using TwilioLookupCarrier.Models;

namespace TwilioLookupCarrier.Pages
{
    public class TwilioLookupModel : PageModel
    {
        readonly TwilioSettings _twilioSettings;

        [BindProperty(SupportsGet = true)]
        public PhoneNumberInfo PhoneNumberInfo { get; set; }

        public TwilioLookupModel(IOptions<TwilioSettings> twilioSettings)
        {
            _twilioSettings = twilioSettings?.Value
                ?? throw new ArgumentNullException(nameof(twilioSettings));
        }
        public IActionResult OnGet()
        {
            // For demonstration purposes, preset. You'll want a dropdown on the UI.
            PhoneNumberInfo.CountryCodeSelected = $"US";
            return Page();

        }

        public async Task<IActionResult> OnPostAsync()
        {
            if (!ModelState.IsValid)
            {
                return Page();
            }

            try
            {
                TwilioClient.Init(_twilioSettings.AccountSid, _twilioSettings.AuthToken);

                // Return the input values to the ModelState.
                ModelState.FirstOrDefault(x => x.Key == $"{nameof(PhoneNumberInfo)}.{nameof(PhoneNumberInfo.CountryCodeSelected)}").Value.RawValue =
                    PhoneNumberInfo.CountryCodeSelected;

                ModelState.FirstOrDefault(x => x.Key == $"{nameof(PhoneNumberInfo)}.{nameof(PhoneNumberInfo.PhoneNumberRaw)}").Value.RawValue =
                    PhoneNumberInfo.PhoneNumberRaw;

                // Try getting information for the CountryCodeSelected and PhoneNumberRaw values.
                var phoneNumber = await PhoneNumberResource.FetchAsync(
                        countryCode: PhoneNumberInfo.CountryCodeSelected,
                        pathPhoneNumber: new Twilio.Types.PhoneNumber(PhoneNumberInfo.PhoneNumberRaw),
                        type: new List<string> { "carrier" }
                    );
                // If PhoneNumberResource.FetchAsync can't resolve the pathPhoneNumber and countryCode into a valid phone number,
                // Twilio.dll throws Twilio.Exceptions.ApiException. You can catch the error by 1) using Twilio.Exceptions,
                // 2) catching the error, and 3) updating the ModelState with the error information.

                // If you've gotten to this point, the Twilio Helper Library was able to determine that the supplied country code and phone number are valid,
                // but not necessarily that the phone number has been assigned.
                ModelState.FirstOrDefault(x => x.Key == $"{nameof(PhoneNumberInfo)}.{nameof(PhoneNumberInfo.Valid)}").Value.RawValue = true;

                // You can return the validated phone number info to the UI via the model state.
                ModelState.FirstOrDefault(x => x.Key == $"{nameof(PhoneNumberInfo)}.{nameof(PhoneNumberInfo.CountryCode)}").Value.RawValue =
                    phoneNumber.CountryCode;

                ModelState.FirstOrDefault(x => x.Key == $"{nameof(PhoneNumberInfo)}.{nameof(PhoneNumberInfo.PhoneNumberFormatted)}").Value.RawValue =
                    phoneNumber.NationalFormat;

                ModelState.FirstOrDefault(x => x.Key == $"{nameof(PhoneNumberInfo)}.{nameof(PhoneNumberInfo.PhoneNumberMobileDialing)}").Value.RawValue =
                    phoneNumber.PhoneNumber;

                if (phoneNumber.Carrier != null)
                {
                    phoneNumber.Carrier.TryGetValue("error_code", out string carrierErrorCode);
                    if (!String.IsNullOrEmpty(carrierErrorCode))
                    {
                        throw new ApiException(int.Parse(carrierErrorCode), 000, "carrier lookup error", moreInfo: " ");
                    }
                    else
                    {
                        phoneNumber.Carrier.TryGetValue("mobile_country_code", out var mobileCountryCode);
                        ModelState.FirstOrDefault(x => x.Key == $"{nameof(PhoneNumberInfo)}.{nameof(PhoneNumberInfo.Carrier)}.{nameof(PhoneNumberInfo.Carrier.MobileCountryCode)}")
                            .Value.RawValue = (mobileCountryCode ?? String.Empty);

                        phoneNumber.Carrier.TryGetValue("mobile_network_code", out var mobileNetworkCode);
                        ModelState.FirstOrDefault(x => x.Key == $"{nameof(PhoneNumberInfo)}.{nameof(PhoneNumberInfo.Carrier)}.{nameof(PhoneNumberInfo.Carrier.MobileNetworkCode)}")
                            .Value.RawValue = (mobileNetworkCode ?? String.Empty);

                        phoneNumber.Carrier.TryGetValue("name", out var carrierName);
                        ModelState.FirstOrDefault(x => x.Key == $"{nameof(PhoneNumberInfo)}.{nameof(PhoneNumberInfo.Carrier)}.{nameof(PhoneNumberInfo.Carrier.CarrierName)}")
                            .Value.RawValue = (carrierName ?? String.Empty);

                        phoneNumber.Carrier.TryGetValue("type", out var carrierType);
                        ModelState.FirstOrDefault(x => x.Key == $"{nameof(PhoneNumberInfo)}.{nameof(PhoneNumberInfo.Carrier)}.{nameof(PhoneNumberInfo.Carrier.CarrierType)}")
                            .Value.RawValue = (carrierType ?? String.Empty);
                    }
                }
            }
            catch (ApiException apiex)
            {
                ModelState.AddModelError($"{nameof(PhoneNumberInfo)}.{nameof(PhoneNumberInfo.PhoneNumberRaw)}", $"Twilio API Error {apiex.Code}: {apiex.Message}");
            }
            catch (Exception ex)
            {
                ModelState.AddModelError(ex.GetType().ToString(), ex.Message);
            }
            return Page();
        }
    }
}

You’ll note that the addition of this code resolved all the object reference errors in the TwilioLookup.cshtml file. If is didn’t, be sure you’ve saved all the files, then try Build > Build Solution from the VS 2019 menu bar.

Understanding how to use the Twilio Helper Library for .NET with the Twilio Lookup API

All the back-and-forth between this application and the Twilio Lookup REST API takes place in the code you added to TwilioLookup.cshtml.cs. Starting from the top of the file you can see all the aspects of the process.

There are three using declarations required for the Lookup API:

using Twilio; – includes the helper library itself
using Twilio.Exceptions; – is used to catch API errors passed back through the helper library

using Twilio.Rest.Lookups.V1; – provides the namespace for the Lookup API functionality

using TwilioLookupCarrier.Models; makes the PhoneNumberInfo class available.

The PhoneNumberInfo class is bound to the TwilioLookupModel PageModel by making it a property of the class. The property is decorated with [BindProperty(SupportsGet = true)] attribute so you can pre-populate the CountryCodeSelected field with “US” as a default value.

Looking at the top of the class you can see that your Twilio credentials are provided to the class through dependency injection.

If the model state of the values submitted through the HTTP POST action are valid you can perform a try … catch operation with the helper library and the API. The first step is to initialize an instance of the Twilio client, which is done with the .Init static method using your Twilio credentials as values. If the login doesn’t work this is the first place the helper library will throw an error.

Once your code has logged in, you can use the PhoneNumberResource class to make an asynchronous call to the API to validate the phone number and get carrier information about it. Note that you specify the type of lookup you’re doing with the type parameter.

The Lookup API will use libphonenumber to determine if the input value for PhoneNumberRaw is valid. If it isn’t, Twilio.Exceptions will throw an ApiException, which you can catch and return to the user interface:

 catch (ApiException apiex)
 {
       ModelState.AddModelError($"{nameof(PhoneNumberInfo)}.{nameof(PhoneNumberInfo.PhoneNumberRaw)}", $"Twilio API Error {apiex.Code}: {apiex.Message}");
}

Developers using .NET can learn more about the C# version of libphonenumber in this Twilio Blog post: Validating phone numbers effectively with C# and the .NET frameworks. Twilio Lookup takes care of a lot of what you’d otherwise need to do separately with libphonenumber-csharp.

If the value of PhoneNumberRaw can be validated for CountryCodeSelected the API will return the phone number formatted in the appropriate national format and the E.164 format. It will do this regardless of whether or not it can find carrier identification information for the number.

If carrier is requested from the API it will return a Dictionary object that may have a value set for the element error_code (note the underscore). This error value won’t be raised through Twilio.Exceptions, so if you want to use it as a model state error you’ll need to raise it as done here:

phoneNumber.Carrier.TryGetValue("error_code", out string carrierErrorCode);
if (!String.IsNullOrEmpty(carrierErrorCode))
{
      throw new ApiException(int.Parse(carrierErrorCode), 000, "carrier lookup error", moreInfo: " ");
}

If the API hasn’t returned any errors you can return the carrier information to the user interface.

Testing the carrier lookup application

Run the application and go to https://localhost:44326/TwilioLookup (The port number assigned by IIS Express or your web server may be different.) Try entering your own mobile phone number and the appropriate country code, then click Check. You should, at minimum, see these results:

  • Valid: checked
  • Country Code: 2-character country code where the phone number was issued
  • National Dialing Format: as appropriate for your country
  • Mobile Dialing Format: the E.164 formatting for your phone number

In the Carrier section you should see information for your mobile number.

Here are a few other numbers you can try:

Issuing
Country

Phone Number
(510) 867-5310

Mobile
Country Code

Mobile
Network
Code

Carrier
Name

Carrier
Type

US

(510) 867-5310

310

120

Sprint Spectrum, L.P.

mobile

US

(800) 638-6469

  

[API error]

 

US

(281) 483-0123

  

Teleport Communications America, LLC

landline

US

(734) 764-2538

  

Ameritech - PSTN

landline

Successful completion of the first example should look like the following screenshot:

Twilio Lookup application screenshot

As you can tell from the responses returned by the second example, numbers that can’t be located in a carrier database will return an API error. Keep in mind that mobile carriers rebrand frequently and the network code for a number can change if the owner ports it to another carrier.

Summary

This post demonstrates how to use Twilio Lookup to obtain the carrier information associated with a phone number. It shows how to use ASP.NET Core Razor Pages to build an application to collect information through a web form and return retrieved information on the same form. The code demonstrates how to interact with the Lookup API using the Twilio Helper Library for .NET, including handling errors returned through the API and in error codes included in the retrieved information.

Additional resources

The following links provide information which may be helpful to you as you expand on the knowledge you gained from this post:

Introduction to Razor Pages in ASP.NET Core – If you’re relatively new to Razor Pages the Microsoft documentation includes a nice tutorial that gives you a great overview.

Andrew Lock | .NET Escapades – For deeper dives into Razor-related topics, and all things .NET, Andrew Lock’s blog is a great resource.

Twilio Docs Lookup API – The official documentation includes information on using the API directly as well as through the Twilio Helper Library for .NET.

TwilioQuest – Learn programming skills while vanquishing the evil Legacy Systems in this cool retro game. Free.

AJ Saulsberry is a Technical Editor at Twilio. Get in touch with him if you have a helpful .NET programming tip you’d like to demonstrate with your own post on the Twilio Blog.