Validating Phone Numbers Effectively with ASP.NET Core 3.1 Razor Pages

January 23, 2020
Written by
AJ Saulsberry
Contributor
Opinions expressed by Twilio contributors are their own

Validating Phone Numbers Effectively In Razor Pages

Data validation is an essential part of application design and development, and telephone numbers are as tricky to validate as they are ubiquitous. In many cases a phone number will be the primary way your organization communicates with its customers. Whether the communication will be by voice, SMS, or messaging app, having a correct phone number is a requirement.

Developers using .NET Core and the .NET Framework can do validation for a number of different data types, including phone numbers, with the System.ComponentModel.DataAnnotations namespace, but the PhoneAttribute class has its limitations. To learn more, see the .NET Data Validation section of the previous Twilio Blog post on this subject: Validating phone numbers effectively with C# and the .NET frameworks.

Fortunately, the libphonenumber-csharp open source library provides extensive resources for validating and manipulating phone numbers of all types and it’s conveniently available as a NuGet package. This post shows how you can implement libphonenumber-csharp in your APS.NET Core Razor Pages projects and easily leverage capabilities of the library.

To get a better understanding of the complexities of phone number validation, it’s well worth reading Google’s Falsehoods Programmers Believe About Phone Numbers, a distinguished member of the series of “falsehoods programmers believe” articles about names, dates, time, and other aspects of the data universe.

Understanding the case study project

The case study project for this post uses a single ASP.NET Core 3.1 Razor Page to collect a phone number from a user and return information about the phone number to the user. The Razor Page is backed by a model class to hold the phone number entered by the user and information about it. The server-side code uses phonenumberlib-csharp to validate the phone number and provide information about it. It’s a simple demo, not production code, but it includes a number of handy techniques you can use in production.

There is a companion repository available on GitHub under an open source license.

Prerequisites

Initialize the .NET solution and the C# Razor Pages project

You can get everything set up for this tutorial by executing a series of command-line instructions. These instructions will work for the usual Windows, macOS, and Linux shells, and they’ll create a .NET solution that’s compatible with Visual Studio 2019, Visual Studio Code, and text editors.

Alternatively, you can use your IDE UI to create the solution, project, and components identified in the following steps.

Create a solution directory

Open a command prompt window in the directory in which you’d like to create the directory structure for this solution. Create a new directory called BlipPhoneRazor and make it the active directory:

mkdir BlipPhoneRazor
cd BlipPhoneRazor

Initialize a Git repository for the solution

In the BlipPhoneRazor directory, initialize a Git repository:

git init

Add a .gitignore file. If you’re using VS 2019 or VS Code you’ll want to add the standard .gitignore for Visual Studio solutions. The file can be found at:

https://github.com/github/gitignore/blob/master/VisualStudio.gitignore

If you’re using the new open-source PowerShell Core on Windows 10, macOS, or Linux, you can get the remote file with the following Invoke-WebRequest command:

iwr https://github.com/github/gitignore/blob/master/VisualStudio.gitignore -OutFile .gitignore

Alternatively, you can get the file with curl or another command line tool, or just copy the contents to a new .gitignore file in the BlipPhoneRazor solution directory.

Add a project reference to the solution file

Execute the following .NET Core CLI commands to create the solution file and add a Razor project to the solution file:

dotnet new sln
dotnet sln BlipPhoneRazor.sln add BlipPhoneRazor/BlipPhoneRazor.csproj

Note that the second command above configures the solution file, but it doesn’t create the project directory or project files; it just adds a listing for a C# project in the solution file.

Create the BlipPhoneRazor project in its own subdirectory using the ASP.NET Core 3.1 Razor Pages template by executing the following CLI commands in the BlipPhoneRazor solution directory:

mkdir BlipPhoneRazor
cd BlipPhoneRazor
dotnet new razor -n -au None -f netcoreapp3.1

Once you see the “Restore succeeded.” message, check the contents of the directory to ensure you see a BlipPhoneRazor.csproj file along with a few other files and directories.

Add the libphonenumber-csharp NuGet package to the Razor Pages project

Add the libphonenumber-csharp dependency by executing the following .NET Core CLI command in the BlipPhoneRazor project directory:

 dotnet add package libphonenumber-csharp

Create project components

You can add new components to a .NET project from the command line.

To add a new Razor page to the project, execute the following .NET Core CLI command in the BlipPhoneRazor project directory:

dotnet new page -n PhoneCheck -o Pages

Create a new directory, Models, under the project directory and create a new file, PhoneNumberCheck.cs, for a C# class in the Models directory. For PowerShell users, the command to create the class file is:

New-Item -Path "./Models" -Name "PhoneNumberCheck.cs" -ItemType "file"

Note the casing for the new files: getting this right will ensure the object names conform to the convention for namespaces and classes. Also note that C# is the default language for new class libraries, but it pays to be specific.

Run the app to validate the configuration

You can check to be sure what you’ve created thus far compiles and runs with a .NET Core CLI command from the BlipRazorPhone directory:

dotnet run

Go to https://localhost:5001 with your browser. You should see the new, minimalist default “Welcome” page for ASP.NET Core Razor Pages projects with the project name on the left side of the banner at the top.

Go to https://localhost:5001/PhoneCheck. There’s no content on this page yet, so you should only see the banner.

That’s it for the setup! Now would be a good time to check in your code. If you’re still working from the command line in the BlipPhoneRazor project directory(not the parent BlipPhoneRazor solution directory), execute the following command-line instructions:

cd ..
git add .
git commit -m "Add project files and dependencies"

You should see your project files being added to your Git repo.

Code the app

There are three project components for you to code:

PhoneNumberCheck.cs – the data model to hold the phone number data,

PhoneCheck.cshtml.cs – the Razor Page Model that mechanizes the page, and

PhoneCheck.cshtml – the Razor Page markup for the page content.

The following sections provide the code and explanations of some of the important features of each file, along with links to more information.

Create the data model

Open the PhoneNumberCheck.cs file in the BlipPhoneRazor\Models directory and replace the contents, if any, with the following C# code:

using Microsoft.AspNetCore.Mvc.Rendering;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;

namespace BlipPhoneRazor.Models
{
    public class PhoneNumberCheck
    {
        private string _countryCodeSelected;

        [Required]
        [Display(Name = "Issuing Country")]
        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 = "Validated Type")]
        public string PhoneNumberType { get; set; }

        [Display(Name = "Region Code")]
        public string RegionCode { get; set; }

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

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

The first two properties, CountryCodeSelected and PhoneNumberRaw are used to store information about the phone number provided by the user. The remainder of the properties are used to hold information returned by libphonenumber-csharp.

The model uses .NET Core 3.1 Data Annotations to provide additional information about the data in each property, including the field title to use in user interfaces. This feature enables you to provide information about the data once and have it reflected automatically in places where it’s used, an especially helpful feature when you make changes.

The maximum length of a phone number is a highly contextual thing and depends, in part, on whether your application will allow Phonewords, numbers that enable reaching a specific extension after the connection is made, E.164 formatting, or other characteristics.

Create the Razor PageModel

For developers used to the MVC and MVVM design paradigms, an ASP.NET Core Razor PageModel combines some of the features of a data model, a view model, and a controller. In conjunction with a Razor Page, the

Open the PhoneCheck.cshtml.cs file in the BlipPhoneRazor\Pages directory and replace the existing contents with the following C# code:

using System.Diagnostics;
using System.Linq;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using PhoneNumbers;
using BlipPhoneRazor.Models;

namespace BlipPhoneRazor
{
    public class PhoneCheckModel : PageModel
    {
        private static PhoneNumberUtil _phoneUtil;
        
        [BindProperty(SupportsGet = true)]
        public PhoneNumberCheck PhoneNumberCheck { get; set; }

        public PhoneCheckModel()
        {
            _phoneUtil = PhoneNumberUtil.GetInstance();
        }

        public IActionResult OnGet()
        {
            PhoneNumberCheck.CountryCodeSelected = $"US";
            return Page();
        }

        public IActionResult OnPost()
        {
            if (!ModelState.IsValid)
            {
                return Page();
            }

            try
            {
                PhoneNumber phoneNumber = _phoneUtil.Parse(PhoneNumberCheck.PhoneNumberRaw, PhoneNumberCheck.CountryCodeSelected);

                var valid = _phoneUtil.IsValidNumberForRegion(phoneNumber, PhoneNumberCheck.CountryCodeSelected);
                Debug.Print($"{phoneNumber} is {valid} for region {PhoneNumberCheck.CountryCodeSelected}");

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

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

                ModelState.FirstOrDefault(x => x.Key == $"{nameof(PhoneNumberCheck)}.{nameof(PhoneNumberCheck.Valid)}").Value.RawValue =
                    _phoneUtil.IsValidNumberForRegion(phoneNumber, PhoneNumberCheck.CountryCodeSelected);

                ModelState.FirstOrDefault(x => x.Key == $"{nameof(PhoneNumberCheck)}.{nameof(PhoneNumberCheck.PhoneNumberType)}").Value.RawValue =
                    _phoneUtil.GetNumberType(phoneNumber);

                ModelState.FirstOrDefault(x => x.Key == $"{nameof(PhoneNumberCheck)}.{nameof(PhoneNumberCheck.RegionCode)}").Value.RawValue =
                    _phoneUtil.GetRegionCodeForNumber(phoneNumber);

                ModelState.FirstOrDefault(x => x.Key == $"{nameof(PhoneNumberCheck)}.{nameof(PhoneNumberCheck.PhoneNumberFormatted)}").Value.RawValue =
                    _phoneUtil.FormatOutOfCountryCallingNumber(phoneNumber, PhoneNumberCheck.CountryCodeSelected);

                ModelState.FirstOrDefault(x => x.Key == $"{nameof(PhoneNumberCheck)}.{nameof(PhoneNumberCheck.PhoneNumberMobileDialing)}").Value.RawValue =
                    _phoneUtil.FormatNumberForMobileDialing(phoneNumber, PhoneNumberCheck.CountryCodeSelected, true);

                return Page();
            }
            catch (NumberParseException npex)
            {
                ModelState.AddModelError(npex.ErrorType.ToString(), npex.Message);
            }
            return Page();
        }
    }
}

The following code snippets illustrate particular features of note in this file.

using PhoneNumbers;

This using directive includes the libphonenumber-csharp package. The actual assembly name for the library is PhoneNumbers.dll, hence the name.

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

The PhoneNumberCheck data model is bound to the page model using the [BindProperty] attribute. It’s also configured to bind the model to support HTTP GET requests so it can be used to populate data entry fields with default values. This is a particularly useful technique when you want to include dropdown lists on your data entry form.

        public PhoneCheckModel()
        {
            _phoneUtil = PhoneNumberUtil.GetInstance();
        }

The constructor for PhoneCheckModel creates a new instance of PhoneNumberUtil with a method instead of the more conventional C# new keyword because the source project for libphonenumber-csharp is Google’s libphonenumber, which is a Java, C++, and JavaScript library. That’s how they roll.

                PhoneNumber phoneNumber = _phoneUtil.Parse(PhoneNumberCheck.PhoneNumberRaw, PhoneNumberCheck.CountryCodeSelected);

To convert the raw phone number to a PhoneNumber number object, parse the raw number using a specific country code. The validity of a phone number can only be established in conjunction with a country code unless the phone number is presented in E.164 format. (For a more concise explanation, see E.164.)

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

To return the same Razor page to the browser with new or updated data the ModelState has to be updated. The ModelState must be updated for input values to be returned to the user interface as well as for the new data derived from the PhoneNumberUtil object.

Date elements in the ModelState object are key-value pairs which can be located with the.FirstOrDefault method. This is preferable to using the .SetModelValue method in most cases because .SetModelValue will create a new ModelState record that won’t be bound to an <input> field if the value used to locate the key is different than the key name.

The ModelState keys can be matched to the names of the PhoneNumberCheck model properties using the C# nameof operator, which will return the unqualified name of the property. However, the name of the key uses the qualified name of the property, which includes the class name. Because there is no variant of nameof that returns qualified names, the value to use in searching for the matching key must be constructed from an interpolated string that combines the name of the PhoneNumberCheck class with the name of the desired property, as follows:

$"{nameof(PhoneNumberCheck)}.{nameof(PhoneNumberCheck.CountryCodeSelected)}"

The PhoneNumberUtil object with throw specific exceptions when the phone number and country code passed to it as arguments exceed specific boundaries. These errors can be trapped and added to the ModelState object as model errors so the Razor page can display them automatically when it’s redisplayed.

            catch (NumberParseException npex)
            {
                ModelState.AddModelError(npex.ErrorType.ToString(), npex.Message);
            }
            return Page();

Note that this catch block won’t trap all errors, so your production code is likely to need to account for other types of errors.

Create the Razor Page

It all begins with the @page directive.

Open the PhoneCheck.cshtml file in the Pages directory and replace the existing contents with the following Razor markup:

@page
@model BlipPhoneRazor.PhoneCheckModel
@{
  ViewData["Title"] = "PhoneCheck";
}

<h1>PhoneCheck</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="PhoneNumberCheck.CountryCodeSelected" class="control-label"></label>
        <input asp-for="PhoneNumberCheck.CountryCodeSelected" class="form-control" autofocus="autofocus"/>
        <span asp-validation-for="PhoneNumberCheck.CountryCodeSelected" class="text-danger"></span>
      </div>
      <div class="form-group">
        <label asp-for="PhoneNumberCheck.PhoneNumberRaw" class="control-label"></label>
        <input asp-for="PhoneNumberCheck.PhoneNumberRaw" class="form-control" />
        <span asp-validation-for="PhoneNumberCheck.PhoneNumberRaw" class="text-danger"></span>
      </div>
      <div class="form-group">
        <input type="submit" value="Check" class="btn btn-default" />
      </div>

      <br />
      <h2>Results</h2>
      <hr />
      <div class="form-group">
        <div class="checkbox">
          <label>
            <input asp-for="PhoneNumberCheck.Valid" /> @Html.DisplayNameFor(model => model.PhoneNumberCheck.Valid)
          </label>
        </div>
      </div>
      <div class="form-group">
        <label asp-for="PhoneNumberCheck.PhoneNumberType" class="control-label"></label>
        <input asp-for="PhoneNumberCheck.PhoneNumberType" class="form-control" readonly="readonly" />
        <span asp-validation-for="PhoneNumberCheck.PhoneNumberType" class="text-danger"></span>
      </div>
      <div class="form-group">
        <label asp-for="PhoneNumberCheck.RegionCode" class="control-label"></label>
        <input asp-for="PhoneNumberCheck.RegionCode" class="form-control" readonly="readonly" />
        <span asp-validation-for="PhoneNumberCheck.RegionCode" class="text-danger"></span>
      </div>
      <div class="form-group">
        <label asp-for="PhoneNumberCheck.PhoneNumberFormatted" class="control-label"></label>
        <input asp-for="PhoneNumberCheck.PhoneNumberFormatted" class="form-control" readonly="readonly" />
        <span asp-validation-for="PhoneNumberCheck.PhoneNumberFormatted" class="text-danger"></span>
      </div>
      <div class="form-group">
        <label asp-for="PhoneNumberCheck.PhoneNumberMobileDialing" class="control-label"></label>
        <input asp-for="PhoneNumberCheck.PhoneNumberMobileDialing" class="form-control" readonly="readonly" />
        <span asp-validation-for="PhoneNumberCheck.PhoneNumberMobileDialing" 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");}
}

Note that the asp-validation-summary attribute of the form is set to ModelOnly. This prevents the model validation rules from being invoked when the form loads because the PhoneNumberCheck data model bound to the PageModel is set to support HTTP GET requests.

Also note that fully qualified names like PhoneNumberCheck.CountryCodeSelected are used to refer to the data model properties bound to form fields. Including the class name is necessary whenever a separate class is bound to the PageModel object. If you were using properties created directly in the PhoneCheckModel class, a technique that isn’t very reusable, you’d use just the property name.

At this point you’ve completed all the coding, so look for linter issues that might reveal typos. When you’re ready, check in your code.

Note for Visual Studio Code users: If you’ve installed project dependencies, like libphonenumber-csharp, from the command line the OmniSharp debugger can get confused and tell you it can’t find a namespace in a using statement. This can usually be resolved by closing and reopening VS Code.

Run and test the application

If you are using Visual Studio 2019 you can, of course, press F5 to get things rolling. Visual Studio Code users will need to create a launch profile first. There’s an example in the companion repository if you need one. From the command line, execute dotnet run and go to the indicated URL in a browser.

Go to the new PhoneCheck page at: https://localhost:<port>/PhoneCheck.

You should see that Issuing Country is prepopulated with “US”.

Enter “800-flowers” in Phone Number and click/tap Check.

You should see results indicating that “800-flowers” is a valid, toll-free number in the US and that it converts to the digits “800 356-9377”, as illustrated in the screenshot below.

BlipPhoneRazor app PhoneCheck web page showing completed entries

Converting phonewords to digits is a handy feature of libphonenumber-csharp. [It’s also worth noting that this author has been very satisfied with 1-800-flowers.com.]

The library includes a number of other features beyond the five demonstrated here, so experiment with more of the properties and methods of PhoneNumberUtil and the PhoneNumber object.

Sample Phone Numbers

Here are some numbers that will demonstrate different aspects of the library:

Issuing Country

ISO Code

Number

Notes

Vatican City

VA

+39 06 69812345

E.164 format

Switzerland

CH

446681800

International from US

United States

US

617-229-1234 x1234

Extension

United States

US

212-439-12345678901

Too long (>16 digits)

Possible enhancements

There are a number of ways you can expand on the functionality provided in the case study app:

  • Convert the phone number validation page to a shared component so it can be reused.
  • Add a dropdown list so users can select the Issuing Country from a canonical ISO standard list.
  • Use the JavaScript version of libphonenumber to perform validation on the client side before sending data to the web server.
  • Add a data persistence layer so you can store validated phone numbers.
  • Use Twilio Verify to validate phone numbers more comprehensively and determine their capabilities. Verify handles variables you don’t see, like carrier regulations and device-specific capabilities, Verify spots and solves for mission critical communication variables, ensuring your message is always delivered.

Summary

This post explains why validating phone numbers is important and demonstrates how you can do it effectively in ASP.NET Core 3.1 Razor Pages projects with the libphonenumber-csharp open source library. It also shows you how to bind a separate data model class to a Razor PageModel, use the data model to prepopulate data entry fields, and how to return data to the same HTML page used for data entry. It also shows how to use the ModelState object to return errors from the libphonenumber-csharp PhoneNumberUtil object to the user interface using the built-in capabilities of Razor Pages.

Additional resources

Here are some references to more background information and additional learning:

Introduction to Razor Pages in ASP.NET Core – If you’re new to Razor Pages this is the place to start.

What's new in ASP.NET Core 3.1 – Go here if you need to get caught up on the latest features. Be sure to check the What’s new entry for v3.0 as well, since most of the significant new features were released in 3.0.

TwilioQuest – An action-adventure game for learning programming. How cool is that?

AJ Saulsberry is a Technical Editor at Twilio. Reach out to him if you’ve been “there and back again” with a .NET development topic and want to get paid to write about it for the Twilio blog.