libphonenumber-csharp
, that provides extensive resources for validating and manipulating phone numbers of all types. It’s derived from an open source library created by Google. This post shows how you can implement libphonenumber-csharp
in your .NET projects and easily leverage this powerful functionality.[Required]
and [StringLength(100)]
are validation attributes used to decorate the Title
property of the Movie
class.public class Movie { public int Id { get; set; } [Required] [StringLength(100)] public string Title { get; set; } // Additional properties and methods of the Movie class }
[DataType(DataType.PhoneNumber)] public string MobilePhone { get; set; }
DataType.PhoneNumber
enumeration provides the DataTypeAttribute
class with the specific type of data to validate from the list of standard data types the class supports. Because it’s an enumeration, you get the standard functionality of the DataTypeAttribute
class.Phone
class derives from the DataTypeAttribute
class and can be overridden and extended to provide additional validation behavior.[Phone] public string MobilePhone { get; set; }
Phone
class and use it to validate a object property:[Phone] public Landline WorkPhone { get; set; }
[Phone]
and [DataType(DataType.PhoneNumber)]
used interchangeably in C# code, and since the first usage is shorter many developers turn to it as a matter of habit. Although both approaches will work identically in the vast majority of cases, the derived class format does make your code more complex behind the scenes and provide a path for introducing bugs, so it’s better to use enumeration format unless your requirements call for the derived class.PhoneAttribute.cs
determines if a submitted number is "phone number-ish" That's ok, because there is readily available code that performs much more extensive phone number validation.PM> Install-Package libphonenumber-csharp -Version 8.9.10
dotnet add package libphonenumber-csharp --version 8.9.10
Models
folder of the project, create a class file, PhoneNumberCheckViewModel.cs
.CountryCodeSelected
field as a plain text field and enter the country codes manually.PhoneNumberCheckViewModel.cs
file is as follows:using System.ComponentModel.DataAnnotations; namespace PhoneCheck.Models { public class PhoneNumberCheckViewModel { private string _countryCodeSelected; [Required] [Display(Name = "Issuing Country")] public string CountryCodeSelected { get => _countryCodeSelected; set => _countryCodeSelected = value.ToUpperInvariant(); } [Required] [Display(Name = "Number to Check")] public string PhoneNumberRaw { get; set; } // Holds the validation response. Not for data entry. [Display(Name = "Valid Number")] public bool Valid { get; set; } // Holds the validation response. Not for data entry. [Display(Name = "Has Extension")] public bool HasExtension { get; set; } // Optionally, add more fields here for returning data to the user. } }
Views
folder, create a Phone
subfolder, then add a new view, Check
, using the Create template and the PhoneNumberCheckViewModel.cs
viewmodel. When the MVC tooling finishes scaffolding the new view the Razor markup generated for the PhoneNumberRaw
field in the Check.cshtml
file should look like the following.... <span asp-validation-for="CountryCodeSelected" class="text-danger"></span> </div> <div class="form-group"> <label asp-for="PhoneNumberRaw" class="control-label"></label> <input asp-for="PhoneNumberRaw" class="form-control" /> <span asp-validation-for="PhoneNumberRaw" class="text-danger"></span> </div> <div class="form-group"> <div class="checkbox"> <label> ...
<form>
element, change the first <div>
element so the asp-validation-attribute
appears as shown below:<div asp-validation-summary="All" class="text-danger"></div>
PhoneNumberRaw
field in the https://localhost:44383/Phone/Check
web page looks like this:... <span class="text-danger field-validation-valid" data-valmsg-for="CountryCodeSelected" data-valmsg-replace="true"></span> </div> <div class="form-group"> <label class="control-label" for="PhoneNumberRaw">Number to Check</label> <input class="form-control" type="text" data-val="true" data-val-required="The Number to Check field is required." id="PhoneNumberRaw" name="PhoneNumberRaw" value="" /> <span class="text-danger field-validation-valid" data-valmsg-for="PhoneNumberRaw" data-valmsg-replace="true"></span> </div> <div class="form-group"> <div class="checkbox"> <label> ...
<input>
field is automatically marked as required and the data validation attributes are wired up.ModelState
object along with the fields in the view model to provide validation and error information to the user.Controllers
folder using the default tooling: PhoneController
.PhoneNumber
and Models
namespaces to the PhoneController.cs
file:using PhoneNumbers; using PhoneCheck.Models;
namespace PhoneCheck.Controllers { public class PhoneController : Controller { private static PhoneNumberUtil _phoneUtil; public PhoneController() { _phoneUtil = PhoneNumberUtil.GetInstance(); } ...
PhoneNumberUtil
is created with the GetInstance()
method, rather than the typical class instance = new class();
syntax in C# to create an instance from a class constructor. Use of the GetInstance()
method results from libphonenumber-csharp
being a port from the original Java library.Index()
method for an empty controller. Change the name of the method to Check
.public IActionResult Check() { return View(); }
PhoneNumberUtil
instance created above in the constructor to do phone number validation and manipulation.[HttpPost] [ValidateAntiForgeryToken] public IActionResult Check(PhoneNumberCheckViewModel model) { if (model == null) { throw new ArgumentNullException(nameof(model)); } if (ModelState.IsValid) { try { // Parse the number to check into a PhoneNumber object. PhoneNumber phoneNumber = _phoneUtil.Parse(model.PhoneNumberRaw, model.CountryCodeSelected); ModelState.FirstOrDefault(x => x.Key == nameof(model.Valid)).Value.RawValue = _phoneUtil.IsValidNumberForRegion(phoneNumber, model.CountryCodeSelected); ModelState.FirstOrDefault(x => x.Key == nameof(model.HasExtension)).Value.RawValue = phoneNumber.HasExtension; return View(model); } catch (NumberParseException npex) { ModelState.AddModelError(npex.ErrorType.ToString(), npex.Message); } } ModelState.SetModelValue(nameof(model.CountryCodeSelected), model.CountryCodeSelected, model.CountryCodeSelected); ModelState.SetModelValue(nameof(model.PhoneNumberRaw), model.PhoneNumberRaw, model.PhoneNumberRaw); ModelState.SetModelValue(nameof(model.Valid), false, null); model.Valid = false; ModelState.SetModelValue(nameof(model.HasExtension), false, null); model.HasExtension = false; return View(model); }
try...catch
block is important to handling phone number errors, so don’t skip it.PhoneNumber
object. Note that there are two required arguments, at minimum, for creating a phone number:model.PhoneNumberRaw
the raw number to be parsed, as the user enters it andmodel.CountryCodeSelected
the country in which the number is assigned.Try { // Parse the number to check into a PhoneNumber object. PhoneNumber phoneNumber = _phoneUtil.Parse(model.PhoneNumberRaw, model.CountryCodeSelected);
PhoneNumberUtil
to determine a variety of information about it:phoneNumber
object we just created and model.CountryCodeSelected
to the IsValidNumberForRegion
method of the _phoneUtil
object.ModelState
object to return the results of the IsValidNumberForRegion
method:ModelState.FirstOrDefault(x => x.Key == nameof(model.Valid)).Value.RawValue = _phoneUtil.IsValidNumberForRegion(phoneNumber, model.CountryCodeSelected);
PhoneNumber
object itself, such as:ModelState.FirstOrDefault(x => x.Key == nameof(model.HasExtension)).Value.RawValue = phoneNumber.HasExtension;
try
block completes successfully we return the updated view model to the view:return View(model);
PhoneNumberUtil
encounters a error, such as a raw phone number that's too long, it will raise a NumberParseException
. In ASP.NET MVC you can trap these and add them to the ModelState
, which can be used to display error messages to the user.catch (NumberParseException npex) { ModelState.AddModelError(npex.ErrorType.ToString(), npex.Message); }
ModelState
level, rather than the individual field level, the validation summary in the Check.cshtml
view has to be set to display all errors, rather than just field-level errors in the model. This is why we made the change when we created the view.ModelState.IsValid
is false we need to reset the values on the form before returning it, so add the following lines after the if block to complete the HttpPost
action method:ModelState.SetModelValue(nameof(model.CountryCodeSelected), model.CountryCodeSelected, model.CountryCodeSelected); ModelState.SetModelValue(nameof(model.PhoneNumberRaw), model.PhoneNumberRaw, model.PhoneNumberRaw); ModelState.SetModelValue(nameof(model.Valid), false, null); model.Valid = false; ModelState.SetModelValue(nameof(model.HasExtension), false, null); model.HasExtension = false; return View(model);
PhoneNumber
object or the PhoneNumberUtil
class in the Controller you can write them to output or add more fields to the view model and the view. The BlipPhone sample project shows some commonly used additional fields./Phone/Check
page on the home page or the top navigation bar with Razor code like the following:<a asp-controller="Phone" asp-action="Check">Check phone numbers</a>
Issuing Country | ISO Code | Number | Notes |
United States | US | 1-800-LOAN-YES | Alphanumeric data |
Switzerland | CH | 446681800 | International from US |
United States | US | 617-229-1234 x1234 | Extension |
United States | US | 212-439-12345678901 | Too long (>16 digits) |