Obtaining Caller ID Information with ASP .NET Core and Twilio Lookup

February 27, 2020
Written by
AJ Saulsberry
Contributor
Opinions expressed by Twilio contributors are their own

twilio-lookup-asp-dotnet-core-razor-pages.png

Who’s calling? In the era of ubiquitous mobile phone use it’s almost impossible to identify the name associated with a phone number unless you’ve already made that connection in your smartphone’s contact list, an annoyance facilitating “New phone who dis?” memes.

But it’s still possible to get the caller name associated with a telephone number, and you can do it without waiting for a call. Twilio Lookup and the Twilio Helper Library for .NET make it easy to integrate real-time caller information retrieval into web applications built with ASP.NET Core.

Understanding “Caller ID” information

The history of “Caller ID,” as it’s commonly known, is surprisingly complex and has involved a number of standards, protocols, and technologies. While the various systems referred to as Caller ID make it possible to identify a calling number, a caller’s name is provided through a system known as Calling Name Presentation (CNAP) or Caller Name Delivery (CNAM). This system is built on databases telephone carriers use to look up caller names and present them to call recipients.

Understanding Twilio Lookup

Twilio Lookup automates the process of determining if a phone number is valid for a specific country and getting information about valid numbers. You can use Lookup to get the caller information, if any, associated with a phone number. Twilio’s vast network of carrier relationships makes it easy to get information from their databases.

Twilio Lookup is a REST API available through HTTPS. Developers using .NET can take advantage of the Twilio Helper Library for .NET to conveniently integrate the API into their applications by installing the helper library as a NuGet package. This makes it possible to interact with phone numbers as objects in C#.

What you’ll learn in this post

In this post you’ll build an ASP.NET Core 3.1 Razor Pages web application using the standard template. You’ll learn how to collect phone number information on a web form and return results to the same page. Most importantly, you’ll learn how to instantiate and interact with phone number objects using the Twilio Helper Library, including handling errors generated by the API.

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 with the complete source for this tutorial available on GitHub.

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 that 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 TwilioLookupCaller as a 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 TwilioLookupCaller solution/project to source code control.

Add the following NuGet packages to the TwilioLookupCaller 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.2
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.37.5

The version numbers above reflect the current versions of these components as of the writing of this post.

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

Loading your Twilio credentials using the System.Environment class is easy, and it prevents you from inadvertently checking your user secrets into a publicly-accessible source code repository.

Under the TwilioLookupCaller 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 TwilioVerifyRazor.Models
{
    public class TwilioSettings
    {
        public string AccountSid { get; set; }
        public string AuthToken { get; set; }
    }
}

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

using TwilioLookupCaller.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 TwilioVerifyRazor.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 Caller Caller { get; set; }
    }

    public class Caller
    {
        [Display(Name = "Caller Name")]
        public string CallerName { get; set; }

        [Display(Name = "Caller Type")]
        public string CallerType { 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.

Because caller information won’t be returned by the Lookup API if it can’t find any, the Caller object won’t always be created.

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 TwilioLookupCaller.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>Caller</h2>
      <hr />
      <div class="form-group">
        <label asp-for="PhoneNumberInfo.Caller.CallerName" class="control-label"></label>
        <input asp-for="PhoneNumberInfo.Caller.CallerName" class="form-control" readonly="readonly" />
        <span asp-validation-for="PhoneNumberInfo.Caller.CallerName" class="text-danger"></span>
      </div>
      <div class="form-group">
        <label asp-for="PhoneNumberInfo.Caller.CallerType" class="control-label"></label>
        <input asp-for="PhoneNumberInfo.Caller.CallerType" class="form-control" readonly="readonly" />
        <span asp-validation-for="PhoneNumberInfo.Caller.CallerType" 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: 23 times, in fact. This is because the data model isn’t bound to the page yet, so 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 TwilioLookupCaller.Models;

namespace TwilioLookupCaller
{
    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> { "caller-name" }
                    );
                // 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;

                // CallerName is a Dictionary object containing caller_name, caller_type, and error_code.
                // It's created if you asked for it when you called PhoneNumberResource.FetchAsync.
                if (phoneNumber.CallerName != null)
                {

                    phoneNumber.CallerName.TryGetValue("error_code", out string callerErrorCode);
                    if (!String.IsNullOrEmpty(callerErrorCode))
                    {
                        throw new ApiException(int.Parse(callerErrorCode), 000, "caller lookup error", moreInfo: " ");
                    }
                    else
                    {
                        phoneNumber.CallerName.TryGetValue("caller_name", out string callerName);
                        ModelState.FirstOrDefault(x => x.Key == $"{nameof(PhoneNumberInfo)}.{nameof(PhoneNumberInfo.Caller)}.{nameof(PhoneNumberInfo.Caller.CallerName)}")
                            .Value.RawValue = (callerName ?? String.Empty);

                        phoneNumber.CallerName.TryGetValue("caller_type", out string callerType);
                        ModelState.FirstOrDefault(x => x.Key == $"{nameof(PhoneNumberInfo)}.{nameof(PhoneNumberInfo.Caller)}.{nameof(PhoneNumberInfo.Caller.CallerType)}")
                            .Value.RawValue = (callerType ?? 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.

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 at 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 TwilioLookupCaller.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. Caller Lookup is a US-only service.

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 caller 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 caller identification information for the number.

If caller-name (note the hyphen) 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.CallerName.TryGetValue("error_code", out string callerErrorCode);
if (!String.IsNullOrEmpty(callerErrorCode))
{
    throw new ApiException(int.Parse(callerErrorCode), 000, "caller lookup error", moreInfo: " ");
}

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

Testing the Caller ID lookup application

Run the application and try entering your own 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 Caller section you may see information for your number, but only if you didn’t decide to have your phone number be unlisted (“ex-directory”).

A few things to keep in mind as you test the application:

  • Caller Lookup is only available for US numbers.
  • There is a nominal charge of $0.01, which should be covered by the credit in your test account if you signed up using the link above. You can monitor your account balance on your Twilio Console Dashboard home page.

Here are a few other numbers you can try:

Issuing
Country

Phone Number

Caller Name

Caller Type

US

(734) 764-2538

UNIVERSITY MUSICAL SOC

BUSINESS

US

(800) 638-6469

TOLL_FREE_CALL

BUSINESS

US

(281) 483-0123

UNITED STATES GOVERNMENT

BUSINESS

US

(510) 867-5310

  

US

1800LOANYES

TOLL_FREE_CALL

 

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

Twilio Lookup web application screenshot

As you can tell from the responses returned by the examples, sometimes the information isn’t very helpful. The second number on the list is the toll free number for Patagonia and the third is the main number for the NASA Johnson Space Center, not that you can tell from the responses returned.

Summary

This post demonstrates how to use Twilio Lookup to obtain the caller information (if any) 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.

UMS.org – The University Musical Society is one of the oldest performing arts presenters in the United States and a 2014 recipient of the National Medal of the Arts.

AJ Saulsberry is a Technical Editor at Twilio. Get in touch with him if you’d like to whistle up a .NET post of your own for the Twilio Blog.