Skip to content

Validate phone numbers in ASP.NET Core

Phone number validation in ASP.NET Core that knows the national numbering plans: what people type is read in the plans you pick, a number no plan accepts gets ASP.NET Core's usual error, and what you store is its E.164 form. The reading is done by one file with no dependency, from the C# guide; the rest is ASP.NET Core's own validation hook, a few lines long.

1. The file

Take Phone.cs from the C# guide, with the plans of the countries you serve (the link opens it for the United Kingdom, the United States and France, the plans the tests below use). It needs .NET 6 or later; base library only. Its Parse reads a number as typed and returns the number with its E.164 form, region and line type, or null when no plan accepts it.

# DataAnnotations is part of .NET: nothing to install

2. The ASP.NET Core side

PhoneNumberAttribute.cs

The attribute: the language guide's Parse in a ValidationAttribute.

using System;
using System.ComponentModel.DataAnnotations;

/// <summary>A phone number readable in the plans of Phone.cs; national numbers are read in DefaultRegion.</summary>
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter)]
public sealed class PhoneNumberAttribute : ValidationAttribute
{
    public PhoneNumberAttribute() : base("The {0} field is not a valid phone number.") { }

    public string? DefaultRegion { get; init; }

    // null is [Required]'s question
    public override bool IsValid(object? value) =>
        value is null || (value is string text && Phone.Parse(text, DefaultRegion) is not null);
}

Signup.cs

A model that uses it.

using System.ComponentModel.DataAnnotations;

public sealed class Signup
{
    [Required]
    public string Name { get; set; } = "";

    [Required, PhoneNumber(DefaultRegion = "GB")]
    public string Mobile { get; set; } = "";

    /// <summary>What to store: the E.164 form (after validation).</summary>
    public string MobileE164 => Phone.Parse(Mobile, "GB")!.E164;
}

3. The tests

SignupTests.cs: The tests, with xUnit and the validator model binding uses.

using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using Xunit;

public class SignupTests
{
    static List<ValidationResult> Validate(Signup signup)
    {
        var results = new List<ValidationResult>();
        Validator.TryValidateObject(signup, new ValidationContext(signup), results, validateAllProperties: true);
        return results;
    }

    [Fact]
    public void NationalNumberInTheDefaultRegion()
    {
        var signup = new Signup { Name = "Ada", Mobile = "07400 123456" };
        Assert.Empty(Validate(signup));
        Assert.Equal("+447400123456", signup.MobileE164);
    }

    [Fact]
    public void InternationalNumber() =>
        Assert.Empty(Validate(new Signup { Name = "Ada", Mobile = "+1 201-555-0123" }));

    [Fact]
    public void ErrorOnTheField()
    {
        var result = Assert.Single(Validate(new Signup { Name = "Ada", Mobile = "+44 7400 12345" }));
        Assert.Equal("The Mobile field is not a valid phone number.", result.ErrorMessage);
        Assert.Equal(new[] { "Mobile" }, result.MemberNames);
    }

    [Fact]
    public void TheBuiltInPhoneAttributeChecksCharactersOnly() =>
        Assert.True(new PhoneAttribute().IsValid("+44 7400 12345"));
}
dotnet test

Run before publishing with .NET 10, System.ComponentModel.DataAnnotations and the file generated for GB, US, FR, as 2026-10-25.

What is specific to ASP.NET Core

  • The built-in [Phone] attribute checks characters, not plans: it accepts +44 7400 12345, a number one digit short (the last test). Use it for the browser's input type, not for validation.
  • Controllers marked [ApiController] answer 400 with the errors when a bound model fails these attributes, before the action runs.
  • The property is called Mobile because a property named Phone inside the class would hide the static class Phone in that class's code.