Tutorial · C# & ASP.NET Core

C# email validation: from regex to mailbox check

Four checks, each with working .NET 8 code: a format regex, the built-in parsers and their surprises, an MX lookup, and a mailbox check wired into ASP.NET Core model validation.

Updated October 10, 2026 · 10 min read

C# email validation: the [EmailAddress] attribute only checks for one @, and only the regex rejects every malformed address in the test

C# email validation usually starts and ends with new MailAddress(input) or the [EmailAddress] attribute, and both accept far more than a sign-up form should. This guide shows what each built-in check really does, with results from running the code, and adds the two checks that catch real bounces: an MX lookup and a mailbox check. All code below was tested on .NET 8.

1. C# email validation with a regex

A regex is the fastest first check. This is the tested pattern from our email regex guide, written for .NET:

using System.Text.RegularExpressions;

public static class EmailFormat
{
    // \A and \z anchor the whole string: with ^ and $, "jane@example.com\n" would match.
    public const string Pattern =
        @"\A(?=.{1,254}\z)(?=.{1,64}@)[A-Za-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[A-Za-z0-9!#$%&'*+/=?^_`{|}~-]+)*"
        + @"@(?:[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?\.)+(?:[A-Za-z]{2,63}|xn--[A-Za-z0-9-]{2,59})\z";

    private static readonly Regex Regex = new(Pattern, RegexOptions.CultureInvariant);

    public static bool IsValid(string? value) => value is not null && Regex.IsMatch(value);
}

It enforces the length limits (64 characters before the @, 254 in total), allows plus tags and apostrophes, and rejects dots at the edges of the local part, double dots, and domains without a top-level domain. The rules behind it are in the guide to the email address format.

Use \A and \z, not ^ and $. In .NET, $ also matches before a final \n, so a version of the same pattern written with ^ and $ accepted "jane@example.com\n" in our test. Pattern is a const so the ASP.NET Core model in section 5 can reuse it in an attribute.

Two details before you call IsValid:

2. Validate an email address in C# with MailAddress and [EmailAddress]

.NET has two built-in candidates, and neither is a format validator in the sense a sign-up form needs.

MailAddress.TryCreate

System.Net.Mail.MailAddress parses an address the way it appears in an email header. Its documentation says the input “can contain a display name and the associated email address if you enclose the address in angle brackets”. So Jane Doe <jane@example.com> is valid input, and so, in our test, was jane doe@example.com: MailAddress read jane as the display name and returned the address doe@example.com.

If you use it, use TryCreate (no exception on bad input) and accept only input that is exactly the parsed address:

using System.Net.Mail;

public static class MailAddressCheck
{
    // MailAddress also parses "Jane Doe <jane@example.com>", so compare the result with the input.
    public static bool IsBareAddress(string input) =>
        MailAddress.TryCreate(input, out var address) && address.Address == input;
}

That removes the display-name cases, but MailAddress still accepts double dots, a trailing dot, hyphens at the start of the domain and jane@localhost (table below).

The [EmailAddress] attribute

System.ComponentModel.DataAnnotations.EmailAddressAttribute looks like a validator, but its .NET 8 source checks very little:

null                      -> valid
contains \r or \n         -> invalid
exactly one @, not first,
not last                  -> valid

There is no regex behind it. !@# and jane@ example.com both passed in our test, and an empty string fails while null passes (pair it with [Required]).

What each check accepts

Every address below went through the four checks on .NET 8. ✓ means the check accepts the address, ✗ that it rejects it.

AddressShould pass?RegexMailAddressIsBareAddress[EmailAddress]
jane@example.comYes✓✓✓✓
jane.doe+news@example.comYes✓✓✓✓
o'brien@example.ieYes✓✓✓✓
jane@exampleNo✗✓✓✓
jane@example.cNo✗✓✓✓
jane.@example.comNo✗✓✓✓
.jane@example.comNo✗✗✗✓
jane..doe@example.comNo✗✓✓✓
jane@example.com.No✗✓✓✓
jane@-example.comNo✗✓✓✓
jane@exa_mple.comNo✗✓✓✓
jane doe@example.comNo✗✓✗✓
"jane doe"@example.comNo (RFC-valid)✗✓✓✓
jane@[192.0.2.10]No (RFC-valid)✗✓✓✓
josé@example.comNo (needs SMTPUTF8)✗✓✓✓
jane@bücher.example.comAfter IDN conversion✗✓✓✓
jane@xn--bcher-kva.example.comYes✓✓✓✓
Jane Doe <jane@example.com>No (display name)✗✓✗✓
jane@example.com with spaces around itAfter trimming✗✓✗✓
jane@example.com + line breakNo✗✗✗✗
aaa…a@example.com (65 a’s)No✗✓✓✓
jane@localhostNo✗✓✓✓

The regex is the only check that rejects every broken address in the list. MailAddress and [EmailAddress] are fine for parsing a header or for a quick “has an @” hint, not for deciding whether to store an address.

3. MX lookup with DnsClient

A correct format says nothing about the domain. System.Net.Dns resolves host names to IP addresses; for MX records most projects use the DnsClient package (dotnet add package DnsClient). (The .NET 11 previews add Dns.ResolveMxAsync, which Microsoft’s API reference marks as prerelease, checked October 2026.)

using DnsClient;

public static class MailDomain
{
    // False only when DNS says the domain takes no email: NXDOMAIN, a null MX,
    // or no MX and no A/AAAA record. DNS errors (SERVFAIL, timeout) count as "maybe".
    public static async Task<bool> CanReceiveMailAsync(ILookupClient dns, string domain)
    {
        try
        {
            var response = await dns.QueryAsync(domain, QueryType.MX);
            if (response.Header.ResponseCode == DnsHeaderResponseCode.NotExistentDomain)
                return false;
            if (response.HasError)
                return true;

            var mx = response.Answers.MxRecords().ToList();
            if (mx.Count == 1 && mx[0].Exchange.Value == ".")
                return false; // null MX (RFC 7505)
            if (mx.Count > 0)
                return true;

            // No MX record: mail goes to the A/AAAA record (implicit MX).
            var a = await dns.QueryAsync(domain, QueryType.A);
            var aaaa = await dns.QueryAsync(domain, QueryType.AAAA);
            return a.Answers.ARecords().Any() || aaaa.Answers.AaaaRecords().Any();
        }
        catch (DnsResponseException)
        {
            return true; // timeout: unknown, don't reject
        }
    }
}

Create one LookupClient for the app and reuse it, since it caches answers for their TTL (DnsClient README). Its defaults (5 seconds per query, 2 retries) are slow for a form, so shorten them:

var dns = new LookupClient(new LookupClientOptions { Timeout = TimeSpan.FromSeconds(2), Retries = 1 });
bool ok = await MailDomain.CanReceiveMailAsync(dns, "example.org");

Three cases need care, and the code handles each:

We ran the function against a local test DNS server: a domain with two MX records, a null MX, a domain with only an A record, one with only an AAAA record, a non-existent domain and a SERVFAIL answer returned True, False, True, True, False and True; a timeout also returned True. More on how MX records work in the MX record guide, or look up a domain by hand with the MX lookup tool.

4. Check that the mailbox exists

An MX record only proves that the domain takes email. Whether anna@example.org exists is something only the receiving mail server knows, and asking it from your own server is unreliable: outbound port 25 is often blocked, and catch-all domains accept every address. The email validation API runs the syntax, DNS, MX and mailbox checks for you and returns one JSON result:

using System.Net.Http.Json;
using System.Text.Json;

public sealed record EmailResult(
    string Email, bool FormatValid, bool MxFound, bool SmtpCheck, bool Disposable,
    bool Role, bool Free, double Score, string State, string Reason, string DidYouMean);

public sealed class EmailValidationClient(HttpClient http)
{
    private static readonly JsonSerializerOptions Json = new(JsonSerializerDefaults.Web)
    {
        PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower, // format_valid -> FormatValid
    };

    public async Task<EmailResult> ValidateAsync(string email, CancellationToken ct = default)
    {
        var url = "https://api.emailvalidation.io/v1/info?email=" + Uri.EscapeDataString(email);
        using var request = new HttpRequestMessage(HttpMethod.Get, url);
        request.Headers.Add("apikey", Environment.GetEnvironmentVariable("EMAILVALIDATION_API_KEY"));

        using var response = await http.SendAsync(request, ct);
        response.EnsureSuccessStatusCode(); // 401, 422, 429 ... throw HttpRequestException
        return (await response.Content.ReadFromJsonAsync<EmailResult>(Json, ct))!;
    }
}

Keep the key in the EMAILVALIDATION_API_KEY environment variable (or your secret store), never in client-side code. Uri.EscapeDataString matters: an unescaped + in jane+news@example.com can be read as a space.

What to do with the result:

stateTypical reasonWhat to do
deliverablevalid_mailboxAccept.
deliverablelow_deliverabilityCatch-all domain: the server accepts every address, so the mailbox can’t be confirmed. Accept, or flag it.
undeliverableinvalid_mailbox, invalid_mx, invalid_formatReject and ask the user to check the address.
unknowntimeout, no_connectThe mail server gave no clear answer. Accept; don’t block a real customer.

Also check disposable (throwaway inboxes) and show did_you_mean when it isn’t empty, for example gmail.com for a typed gmial.com. The field list is on the API page; catch-all email explains why those domains can’t be verified per mailbox. The free plan includes 100 checks a month, and only successful calls count.

5. ASP.NET Core email validator: attributes plus API check

In ASP.NET Core, the format checks belong on the model and the mailbox check in the action. With [ApiController], the framework returns an automatic HTTP 400 when the attributes fail, before your action runs.

using System.ComponentModel.DataAnnotations;

public sealed class SignupRequest
{
    private string _email = "";

    [Required]
    [EmailAddress]
    [RegularExpression(EmailFormat.Pattern, ErrorMessage = "Please enter a valid email address.")]
    public string Email
    {
        get => _email;
        set => _email = value?.Trim() ?? ""; // people paste addresses with spaces
    }
}

[RegularExpression] only passes when the pattern matches the whole value (source), and the setter trims before validation runs. The controller then asks the API and reports problems through ModelState, so every error reaches the client in the same format:

using Microsoft.AspNetCore.Mvc;

[ApiController]
[Route("signup")]
public sealed class SignupController(EmailValidationClient emails, ILogger<SignupController> log) : ControllerBase
{
    [HttpPost]
    public async Task<IActionResult> Post(SignupRequest request, CancellationToken ct)
    {
        // [ApiController] has already returned 400 if the attributes failed.
        try
        {
            var result = await emails.ValidateAsync(request.Email, ct);
            if (result.State == "undeliverable")
                ModelState.AddModelError(nameof(request.Email), "This email address doesn't exist. Please check it.");
            else if (result.Disposable)
                ModelState.AddModelError(nameof(request.Email), "Please use a permanent email address.");
        }
        catch (Exception ex) when (ex is HttpRequestException or TaskCanceledException)
        {
            log.LogWarning(ex, "Email check skipped"); // API down or slow: don't block the sign-up
        }

        if (!ModelState.IsValid)
            return ValidationProblem(ModelState);

        // create the account ...
        return Ok();
    }
}

A missing or wrong key (401) also ends up in this catch block, so alert on that warning.

Register the typed HttpClient with a timeout in Program.cs:

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddHttpClient<EmailValidationClient>(c => c.Timeout = TimeSpan.FromSeconds(10));

var app = builder.Build();
app.MapControllers();
app.Run();

We ran the app against a local mock of the API and posted JSON to /signup:

Posted emailResponse
anna@example.org200
anna@example.org with spaces around it200 (trimmed)
jane@example400, “Please enter a valid email address.”
.jane@example.com400, “Please enter a valid email address.”
An address the API reports as undeliverable400, “This email address doesn’t exist. Please check it.”
An address the API reports as disposable400, “Please use a permanent email address.”
Any address while the API answers 429200 (check skipped, warning logged)

The 400 responses are the standard validation problem details, with the messages under errors.Email. For Razor Pages or MVC views, the same attributes drive the form; check ModelState.IsValid yourself, since the automatic 400 only applies to [ApiController].

  1. In the browser: type="email" for instant feedback (the server still decides).
  2. On the server: trim, then the regex via [RegularExpression].
  3. On submit: the API check. Block undeliverable and disposable addresses, accept the rest, and accept everything when the API is unreachable.
  4. For existing lists, check the whole file instead of one address at a time with the bulk email verifier.

To see the API’s answer for a single address before you write code, paste it into the free email validator. The same checks in other languages: Java email validation and email validation in Go.

Sources

Checked October 10, 2026: MailAddress constructor and Dns class (Microsoft Learn); EmailAddressAttribute and RegularExpressionAttribute source (dotnet/runtime, release/8.0); Anchors in regular expressions; Model validation in ASP.NET Core; DnsClient.NET; RFC 7505 (null MX). Code tested with the .NET 8 SDK (8.0.131) and DnsClient 1.8.0 against a local DNS server and a local mock of the API.

Validate email in other languages

Frequently asked questions

How do I validate an email address in C#?

Trim the input, check the format with a regex anchored with \A and \z, and if the address matters (sign-ups, imports), check the mailbox on the server with an email validation API. MailAddress and [EmailAddress] alone let through addresses such as jane..doe@example.com and jane@example.

Is the [EmailAddress] attribute enough in ASP.NET Core?

No. In .NET 8 it only checks that the value contains exactly one @ that is neither the first nor the last character and no line break. Combine it with a [RegularExpression] attribute for the format and an API check for the mailbox.

Does MailAddress validate email addresses?

MailAddress parses the address header syntax, so it accepts display names (Jane Doe <jane@example.com>), double dots and domains without a dot. Use MailAddress.TryCreate and compare the parsed Address with the input if you use it as a check at all.

Can C# check if an email address exists?

Not reliably on its own. An SMTP probe from your server is often blocked on port 25, greylisted or answered by a catch-all domain. An email validation API runs the DNS, MX and mailbox checks and returns one result per address.

Email validation API

Validate emails in your app

emailvalidation.io checks syntax, MX records and the mailbox over SMTP, flags disposable, role and free addresses and returns a quality score, in one request.

/v1/info Email validation API Read the documentation

100 free validations every month. No credit card required.

GET https://api.emailvalidation.io/v1/info?email=support@emailvalidation.io

{
  "email": "support@emailvalidation.io",
  "user": "support",
  "tag": "",
  "domain": "emailvalidation.io",
  "format_valid": true,
  "mx_found": true,
  "smtp_check": true,
  "catch_all": null,
  "role": true,
  "disposable": false,
  "free": false,
  "score": 0.64,
  "state": "deliverable",
  "reason": "valid_mailbox",
  "did_you_mean": ""
}

Keep reading

Start using our email validation software today!

Get 100 validations per month for free