To validate an email address in C#, most developers reach for
System.Net.Mail.MailAddress and move on. It's a fair first move — the type is
built into the framework and rejects garbage without a hand-rolled regex — but it
answers only the first of the three questions real validation asks. Is the
address shaped correctly? Can its domain receive mail? Does the mailbox actually
exist? The .NET base class library answers the first cleanly, needs one
well-known NuGet package for the second, and leaves the third to a network
problem worth handing off. This guide builds email validation in C# as three
layers, with idiomatic async/await code for each, and shows exactly where
MailAddress stops.
The short answer#
Use MailAddress.TryCreate for syntax, the DnsClient.NET package for the MX
lookup, and a verification API for the SMTP mailbox check — cheapest first,
short-circuiting as soon as one layer is decisive. Don't open raw SMTP sockets
from your app to probe mailboxes yourself: outbound port 25 is blocked on most
hosts, and the answer depends on sending-IP reputation and greylisting you don't
want to reimplement. Each layer rules addresses out more cheaply than the last;
only the final one can rule an address in. If you just want a verdict for a
single address right now, paste it into the
free email checker and skip the code — the rest of this
guide is for wiring the check into an app. For the concepts behind each stage,
see what email verification is.
Layer 1: syntax with MailAddress.TryCreate#
MailAddress is the right tool for layer one. Prefer the TryCreate overload
added in .NET 5: it returns a bool instead of throwing a FormatException on
bad input, so you can gate on it without wrapping every call in a try/catch.
using System.Net.Mail;
public static bool IsValidSyntax(string email)
{
return !string.IsNullOrWhiteSpace(email)
&& email.Length <= 320
&& MailAddress.TryCreate(email, out var parsed)
&& parsed.Address == email;
}
IsValidSyntax("jane@example.com"); // true
IsValidSyntax("not-an-email"); // false
IsValidSyntax("Jane <jane@example.com>"); // false
That last comparison matters. MailAddress is a mail parser, not a strict
validator — it happily accepts display-name forms like Jane <jane@example.com>
and exposes the address portion on parsed.Address. Comparing parsed.Address
back to the raw input rejects those forms and stray whitespace, which is what you
want from a yes/no syntax gate on user input. The 320-character guard is
belt-and-braces: nothing longer can be a real address, and it's cheaper to reject
early than to hand a giant string to anything downstream.
On .NET Framework or any target older than .NET 5, TryCreate doesn't exist, so
fall back to the constructor and catch the exception:
try { _ = new MailAddress(email); /* syntax ok */ }
catch (FormatException) { /* reject */ }
If you're already using data annotations on a model, the
System.ComponentModel.DataAnnotations.EmailAddressAttribute ([EmailAddress])
does the same job declaratively and is a fine choice inside ASP.NET Core model
validation. Whichever you pick, treat a pass as "worth checking properly," never
as "valid." A syntax check knows nothing about DNS or mailboxes — it's the same
reason regex email validation fails:
shape and deliverability are different questions, one a fact about the string and
the other a fact about the internet.
Layer 2: can the domain receive mail?#
A domain with no mail route can't accept mail for anyone, so this one lookup
eliminates dead domains, misspelled company names, and invented TLDs before you
ever touch the network for real. The catch: .NET has no built-in MX resolver.
System.Net.Dns resolves A and AAAA records but not MX, so the idiomatic answer
is DnsClient.NET — a widely used, well-maintained NuGet package that most .NET
projects reach for when they need DNS beyond host lookups. Install it and query the MX record type:
using System.Linq;
using DnsClient; // dotnet add package DnsClient
// LookupClient is thread-safe and caches responses — create one and reuse it.
private static readonly LookupClient Dns = new();
public static async Task<bool> HasMailRouteAsync(string domain)
{
try
{
var response = await Dns.QueryAsync(domain, QueryType.MX);
return response.Answers.MxRecords().Any();
}
catch (DnsResponseException)
{
// No reachable resolver, SERVFAIL, and similar — treat as "check later".
return false;
}
}
await HasMailRouteAsync("gmail.com"); // true
await HasMailRouteAsync("company-that-folded.com"); // false
Two idioms worth copying. Create the LookupClient once and reuse it — it's
thread-safe and caches responses, so a per-request instance just throws away that
cache. And split the domain off the address at the last @ with a range
expression so you never mis-parse an address that (legally) contains more than
one:
var domain = email[(email.LastIndexOf('@') + 1)..];
Some domains accept mail on an A record with no MX (an implicit MX). If you want
to honour that edge case, fall back to QueryType.A when the MX set is empty —
but for the overwhelming majority of real addresses, an MX check is the right
filter. One caution for a signup path: a DNS query blocks on a resolver, so a
transient DNS hiccup shouldn't hard-reject a real customer. Treat a failed lookup
as "check later," not "invalid."
Layer 3: does the mailbox actually exist?#
Layers one and two can only rule an address out. A domain can publish perfect
MX records and still have no mailbox at the address you're holding —
noreply-9f2x@gmail.com is valid syntax on a domain with a live mail route, and
it's still a mailbox that was never created. Confirming that a specific mailbox
exists means the SMTP delivery conversation: connect to the mail host, issue
RCPT TO, read the reply, and disconnect before sending anything.
How email verification works walks that
full pipeline, including catch-all domains that accept every address and defeat a
naive probe.
In principle you can script this in C# with a TcpClient and raw SMTP commands.
In practice you shouldn't run it from your application server. Most cloud
providers block outbound port 25, so a probe that works on your laptop quietly
fails in production. The answer depends on the reputation of the IP you connect
from, not just the address you're checking. And receiving servers greylist and
rate-limit unfamiliar senders, so probing at any volume gets you deferred or
blocklisted. This is the layer worth delegating to infrastructure built for it.
Calling a verification API from C##
Qualisend's verification endpoint runs the whole pipeline — syntax, DNS, and the
SMTP mailbox probe — from reputation-managed infrastructure, and returns a
verdict in real time. HttpClient plus System.Text.Json is all you need; model
the response envelope as records and let the JSON source generator or reflection
deserializer fill them:
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text.Json.Serialization;
public record VerifyResponse(
[property: JsonPropertyName("result")] VerifyResult Result);
public record VerifyResult(
[property: JsonPropertyName("status")] string Status,
[property: JsonPropertyName("score")] int Score,
[property: JsonPropertyName("reason")] string? Reason,
[property: JsonPropertyName("sub_flags")] IReadOnlyDictionary<string, bool> SubFlags);
// One HttpClient for the whole app — don't new one up per request.
private static readonly HttpClient Http = new()
{
BaseAddress = new Uri("https://api.qualisend.com/v1/"),
};
public static async Task<VerifyResult> VerifyAsync(string email)
{
using var request = new HttpRequestMessage(HttpMethod.Post, "verify")
{
Content = JsonContent.Create(new { email }),
};
request.Headers.Authorization = new AuthenticationHeaderValue(
"Bearer", Environment.GetEnvironmentVariable("QUALISEND_API_KEY"));
using var response = await Http.SendAsync(request);
response.EnsureSuccessStatusCode();
var body = await response.Content.ReadFromJsonAsync<VerifyResponse>();
return body!.Result; // status: deliverable | risky | undeliverable | unknown
}
POST https://api.qualisend.com/v1/verify is a placeholder shape — check the
API reference on /developers for the exact endpoint, the scoped
key format, and every field. Read the key from an environment variable rather
than hard-coding YOUR_API_KEY, and reuse a single HttpClient to avoid socket
exhaustion. The response comes back inside a result envelope:
{
"result": {
"status": "deliverable",
"score": 95,
"reason": null,
"sub_flags": { "role": false, "disposable": false, "free": false, "catch_all": false }
}
}
status is your headline verdict — deliverable, risky, undeliverable, or
unknown. score grades confidence, reason explains a negative result, and
sub_flags breaks out signals like role, disposable, free, and catch-all so you
can apply your own policy (reject undeliverable, hold risky for review, flag
disposable at signup).
Putting the layers together to validate an email address in C##
Cheapest first, stop as soon as you have an answer:
public static async Task<VerifyResult> ValidateEmailAsync(string email)
{
if (!IsValidSyntax(email))
return new VerifyResult("undeliverable", 0, "invalid_email",
new Dictionary<string, bool>());
var domain = email[(email.LastIndexOf('@') + 1)..];
if (!await HasMailRouteAsync(domain))
return new VerifyResult("undeliverable", 0, "invalid_domain",
new Dictionary<string, bool>());
return await VerifyAsync(email); // deliverable | risky | undeliverable | unknown
}
The two local layers cost nothing and catch most junk instantly; the API layer runs only on addresses worth the network round-trip. That ordering is the whole trick — the same shape you'll find in the Node.js, Python, and PHP versions of this guide, because the layering, not the language, is what makes validation work.
Frequently asked questions#
Is MailAddress or EmailAddressAttribute enough to validate an email in C#?#
For syntax, yes — both are solid layer-one checks and a better bet than a
hand-rolled regex, with MailAddress.TryCreate giving you a no-throw boolean.
But they validate shape, not deliverability: neither resolves DNS or contacts a
mail server, so a pass means "looks like an email," not "will deliver." Pair the
syntax check with an MX lookup and an SMTP mailbox check before you trust the
address.
Does .NET have a built-in way to look up MX records?#
No. System.Net.Dns resolves A and AAAA host records but has no MX support,
which is why the standard approach is the DnsClient.NET NuGet package. Query
QueryType.MX, reuse a single LookupClient, and treat an empty answer set — or
a DnsResponseException — as no mail route rather than letting it throw.
Can I check whether a mailbox exists in C# without an API?#
Only partly. DnsClient.NET confirms the domain accepts mail, which rules out dead
domains cheaply. Confirming the actual mailbox means an SMTP conversation you
can attempt with a TcpClient but shouldn't run from your app server — outbound
port 25 is widely blocked and the result depends on your sending IP's reputation.
That's the layer a verification service exists to handle, and it's worth
comparing providers before you build
it yourself.
Should I validate emails at signup or when cleaning a list?#
Both, at different depths. Run syntax and the MX lookup synchronously at signup — they're fast enough to block the request and give instant feedback — and act on the immediate API verdict there too; the serverless signup guide shows the pattern end to end. Reserve deeper, batched verification for cleaning an existing list, where latency doesn't matter and thoroughness does.
Ready to add the SMTP layer? The API reference has the exact
/verify shape, scoped keys, and copy-paste examples, or drop one address into
the free email checker to watch a MailAddress-approved
string come back undeliverable.