← Blog

Validate an email address in C#

Three layers — a built-in attribute for syntax, DnsClient.NET for MX, an API for disposable and role accounts — and a plain account of what each one proves.

Contents

When you need to validate email in C#, you're really choosing how many layers deep to go: a regex, the built-in attribute, a live DNS check, or an API. The honest answer is that .NET already covers the first layer for free, and every layer past it costs something concrete — a NuGet package, then a network call. Here's each one, what it actually proves, and where you can stop.

Syntax: use the built-in attribute, add nothing

System.ComponentModel.DataAnnotations.EmailAddressAttribute ships with .NET. No NuGet package, no regex to keep alive.

using System.ComponentModel.DataAnnotations;

static bool IsValidSyntax(string email) =>
    new EmailAddressAttribute().IsValid(email);

Two things to know before you trust it. First, IsValid(null) returns true — DataAnnotations treats absence as valid, so pair the attribute with [Required] on your model or guard null yourself. Second, it is a structural check only: it confirms the string looks like local@domain, and says nothing about whether that domain exists or accepts mail.

Do not reach for System.Net.Mail.MailAddress as a validator instead. It is lenient by design — MailAddress.TryCreate happily accepts user@localhost (no dot, no TLD) and even parses display names — so it says "yes" to strings you never meant to allow.

using System.Net.Mail;

// Returns true for "user@localhost" — too loose for validation.
bool looksOk = MailAddress.TryCreate("user@localhost", out _);

For a form field where you'll send a confirmation link anyway, the attribute is genuinely all you need. Stop here.

Does the domain accept mail? A live MX check with DnsClient.NET

Syntax tells you the string is shaped right. It doesn't tell you that @gnail.com — a typo — can receive anything. For that, look up the domain's MX records. .NET has no first-party DNS client, so add the DnsClient.NET package:

dotnet add package DnsClient
using System.Linq;
using DnsClient;

static async Task<bool> HasMxRecord(string domain)
{
    var lookup = new LookupClient();
    var result = await lookup.QueryAsync(domain, QueryType.MX);
    return result.Answers.MxRecords().Any();
}

There's a synchronous lookup.Query(domain, QueryType.MX) too if you're not in an async context. Either way, an MX lookup tells you the domain publishes mail servers. It does not tell you a specific mailbox exists — that's a heavier, separate check. Treat a missing MX as a strong reject (typo'd or parked domain) and a present MX as "worth proceeding".

Disposable and role accounts: a maintained list

Two things neither syntax nor MX will catch, and both need data that goes stale every week: disposable domains (mailinator.com and thousands of throwaways) and role accounts (info@, support@, admin@ — real inboxes, but not a person you can hold a conversation with). Curating those lists yourself is a standing chore. This is where an API earns its place.

Boundstone's POST /v1/verify/email runs the syntax and MX checks above, plus a disposable-domain list and a role-account list, and returns one JSON contract you can share across every service that touches email — no per-language reimplementation.

using System.Net.Http.Headers;
using System.Net.Http.Json;

var http = new HttpClient { BaseAddress = new Uri("https://api.boundstone.io") };
http.DefaultRequestHeaders.Authorization =
    new AuthenticationHeaderValue("Bearer", "bs_live_YOUR_KEY");

var resp = await http.PostAsJsonAsync("/v1/verify/email",
    new { email = "you@example.com" });
var result = await resp.Content.ReadFromJsonAsync<EmailResult>();

Console.WriteLine($"Checked:     {string.Join(", ", result!.checks.performed)}");
Console.WriteLine($"Not checked: {string.Join(", ", result.checks.not_performed)}");

if (result.disposable || result.role_account)
{
    // A real inbox, likely — but route it to review, don't add it to your main list.
}

record EmailResult(
    bool valid_syntax, string domain, bool mx_found,
    bool disposable, bool role_account, bool free_provider,
    Checks checks);
record Checks(string[] performed, string[] not_performed);

Every response states exactly what it checked and what it didn't:

Checked:     syntax, mx, disposable_list, role_list
Not checked: smtp_mailbox, catch_all

Boundstone does not open an SMTP session to the mailbox, and does not probe catch-all domains — those two stay not_performed, plainly, on every response. That's the point, not a footnote to bury: a valid_syntax: true with mx_found: true means the domain can receive mail and isn't a known throwaway — it does not mean a human will read it. Nothing in the payload pretends to prove more than it did. (The response also carries a free_provider flag — true for gmail.com and the like — if you want to treat consumer addresses differently. It isn't part of the checks.performed list; it's just a returned property.)

The free tier is 250 checks a month, no card, and credits never expire. If you'd rather paste a few addresses in a browser before writing any code, the free email validator runs the same checks with no signup.

Validating a whole list

For a CSV of addresses, POST the raw file to /v1/bulk/email and poll for results:

curl -X POST https://api.boundstone.io/v1/bulk/email \
  -H "Authorization: Bearer bs_live_YOUR_KEY" \
  -H "Content-Type: text/csv" \
  --data-binary @emails.csv
# -> {"job_id":"..."}   (HTTP 202)

curl https://api.boundstone.io/v1/bulk/JOB_ID/results.csv \
  -H "Authorization: Bearer bs_live_YOUR_KEY"

The free tier caps a job at 250 rows (paid tiers go to 10,000). A credit is reserved per row and refunded for any row that errors, so a malformed line never quietly costs you. There's also a paste box and CSV upload in the dashboard if you'd rather not script it.

The short version

  • Syntax only, form field: new EmailAddressAttribute().IsValid(email). No package. Don't use MailAddress — it's too lenient, and remember IsValid(null) returns true.
  • Catch typo'd or dead domains: add DnsClient.NET, look up MX records, reject when there are none.
  • Catch disposable and role addresses, one contract across services: POST /v1/verify/email, and read checks.not_performed so you always know what a "valid" does and doesn't cover.
  • No mailbox-level or catch-all check exists here. If a vendor claims per-mailbox certainty, ask how — because Boundstone won't, and says so on every call.

Same layering in other languages: validate an email in Python and validate an email in Java.

Frequently asked questions

How do I validate an email address in C#?

The simplest built-in option in C# is the System.Net.Mail.MailAddress class or a regular expression, which confirms the string is well-formed syntax but nothing about the domain. To go further without running your own mail server, you can call an API from C# with HttpClient. Boundstone's email endpoint, for example, returns valid_syntax plus a live MX-record check (mx_found), a disposable-domain flag, and a role-account flag, and every response lists checks.performed and checks.not_performed so you know exactly what was tested.

Does a valid email address in C# mean the inbox actually exists?

No. A valid result from a syntax check, or from Boundstone, confirms the format is correct and, in Boundstone's case, that the domain publishes MX records (mx_found) and is not on a disposable or role-account list. It does not prove that a specific mailbox exists or will accept a message. Boundstone is explicit about this limit: smtp_mailbox and catch_all appear under checks.not_performed, so a well-formed, deliverable-looking address is never mistaken for a guaranteed-to-land one.

How can I check MX records and catch disposable emails in C# without building it myself?

You can query DNS for MX records from C#, but there is no built-in library for it, and you would still have to source and maintain your own disposable-domain and role-account lists. Boundstone's email endpoint does this in a single HTTP call: it returns mx_found (checked live over DNS), disposable, role_account, and free_provider. Note that it does not perform SMTP mailbox probing or catch-all detection; it reports those honestly as not_performed rather than guessing at an answer it cannot verify.

Thomas Tsui

Founder of Boundstone — building phone, email and IP validation you can actually verify.

One honest API for email, phone and IP — every response lists what it checked and what it didn't claim to. Free tier: 250 credits/month, no card, credits never expire.

More from Boundstone — API documentation · Benchmark methodology · The benchmark series · Buyer's checklist