Para validar um endereço de e-mail no ASP.NET Core, a maioria dos apps se apoia
na data annotation [EmailAddress] nativa, deixa o model binding executá-la e
confere o ModelState.IsValid. É um bom primeiro passo — o atributo já vem com o
framework e rejeita lixo sem precisar de um regex feito à mão —, mas ele responde
apenas à primeira das três perguntas que a validação de verdade faz. O endereço
está no formato correto? O domínio dele consegue receber e-mails? A caixa postal
realmente existe? O ASP.NET Core responde à primeira já de fábrica, precisa de um
pacote NuGet bem conhecido para a segunda e deixa a terceira como um problema de
rede que vale a pena delegar. Este guia constrói a verificação como três camadas
integradas ao próprio pipeline de validação do framework e mostra exatamente onde
o [EmailAddress] para. É o companheiro voltado a web framework do
guia de validação de e-mail em C#, que
cobre as mesmas primitivas fora do pipeline de requisição.
A resposta curta#
Use [EmailAddress] (ou o .EmailAddress() do FluentValidation) para a sintaxe,
o DnsClient.NET para a consulta de MX e uma API de verificação para a checagem da
caixa postal por SMTP — o mais barato primeiro, curto-circuitando assim que uma
camada for decisiva. As camadas de DNS e de API são I/O assíncrono, então não
cabem em um ValidationAttribute síncrono; o lar idiomático delas é um validador
do FluentValidation com os serviços injetados via DI. Não abra sockets SMTP
brutos do seu app para sondar caixas postais por conta própria: a porta 25 de
saída é bloqueada na maioria dos provedores, e a resposta depende da reputação do
IP de envio e de greylisting que você não vai querer reimplementar. Cada camada
descarta endereços para fora de forma mais barata do que a anterior; só a última
consegue aprovar um endereço para dentro. Se o pipeline for novidade para você,
o que é verificação de e-mail apresenta os
termos antes.
Camada 1: validação de formato com DataAnnotations#
A verificação nativa da primeira camada é o atributo [EmailAddress] de
System.ComponentModel.DataAnnotations. Coloque-o no seu modelo de requisição ao
lado de [Required], e o model binding o executa em cada bind:
using System.ComponentModel.DataAnnotations;
public class SignupRequest
{
[Required]
[EmailAddress]
public string Email { get; init; } = string.Empty;
}
Em um controller de MVC ou de API marcado com [ApiController], um atributo que
falha entra em curto-circuito para uma resposta 400 ValidationProblemDetails
antes do corpo da sua action rodar — você nunca precisa inspecionar o
ModelState na mão:
using Microsoft.AspNetCore.Mvc;
[ApiController]
[Route("signup")]
public class SignupController : ControllerBase
{
[HttpPost]
public IActionResult Register(SignupRequest request)
{
// With [ApiController], an invalid [EmailAddress] already returned 400.
// Reaching here means the string is well-formed.
return Ok();
}
}
Saiba exatamente o que isso lhe dá. O EmailAddressAttribute confere o formato
— na verdade, ele só exige um único @ com algo de cada lado — e nada mais. Ele
nunca resolve DNS, nunca abre um socket e não faz ideia se o domínio existe.
definitely-fake@gmail.com passa. info@company-that-folded.com passa.
typo@gmial.com passa. Os três são não entregáveis, e nenhum atributo que apenas
lê a string vai lhe dizer isso — o mesmo motivo pelo qual
a validação de e-mail por regex falha:
formato e entregabilidade são perguntas diferentes, uma é um fato sobre a string e
a outra é um fato sobre a internet.
Por que as duas próximas camadas não podem ser atributos#
Aqui está a pegadinha que molda o resto deste guia: a validação de DataAnnotations
é síncrona. O ValidationAttribute.IsValid retorna um ValidationResult, não uma
Task, então não há um jeito limpo de dar await em uma consulta de DNS ou em
uma chamada HTTP dentro dele. Bloquear em .Result ou
.GetAwaiter().GetResult() para forçar uma chamada assíncrona em um atributo
síncrono convida à falta de threads no pool e a deadlocks sob carga — exatamente o
que você não quer em um fluxo de cadastro.
É por isso que a resposta idiomática do ASP.NET Core para as camadas assíncronas é
o FluentValidation: suas regras MustAsync são validadores de primeira classe que
retornam Task, e ele resolve dependências pelo mesmo container de DI do resto do
seu app, então um validador pode receber um HttpClient ou um serviço de DNS no
construtor. Você pode manter o [EmailAddress] para a primeira camada e adicionar
o FluentValidation para a segunda e a terceira, ou — como abaixo — deixar o
próprio .EmailAddress() do FluentValidation cobrir a sintaxe também e manter as
três camadas em um só lugar.
Camada 2: o domínio consegue receber e-mails?#
Um domínio sem rota de e-mail não consegue aceitar e-mail para ninguém, então uma
consulta elimina domínios mortos, nomes de empresas escritos errado e TLDs
inventados. A pegadinha: o ASP.NET Core não tem um resolvedor de MX nativo. O
System.Net.Dns resolve registros A e AAAA, mas não MX, então a abordagem padrão
é o DnsClient.NET — um pacote NuGet amplamente usado. Envolva-o atrás de uma
pequena interface, para que o validador dependa de uma abstração, não da
biblioteca:
using DnsClient; // dotnet add package DnsClient
using System.Linq;
public interface IMailRouteChecker
{
Task<bool> HasMailRouteAsync(string email, CancellationToken ct = default);
}
public sealed class DnsMailRouteChecker : IMailRouteChecker
{
private readonly ILookupClient _dns;
public DnsMailRouteChecker(ILookupClient dns) => _dns = dns;
public async Task<bool> HasMailRouteAsync(string email, CancellationToken ct = default)
{
var domain = email[(email.LastIndexOf('@') + 1)..];
try
{
var response = await _dns.QueryAsync(domain, QueryType.MX, cancellationToken: ct);
return response.Answers.MxRecords().Any();
}
catch (DnsResponseException)
{
// No reachable resolver, SERVFAIL, and similar — treat as "check later".
return false;
}
}
}
Separe o domínio no último @ com uma expressão de range, para nunca analisar
errado um endereço que legalmente contém mais de um. Registre um único
LookupClient como singleton — ele é thread-safe e faz cache das respostas, então
uma instância por requisição simplesmente joga esse cache fora. Alguns domínios
aceitam e-mail em um registro A sem MX; se você quiser honrar esse caso extremo,
faça um fallback para QueryType.A quando o conjunto de MX estiver vazio.
Camada 3: a caixa postal realmente existe?#
As camadas um e dois só conseguem descartar um endereço para fora. Um domínio
pode publicar registros MX perfeitos e ainda assim não ter caixa postal no
endereço que você tem em mãos — noreply-9f2x@gmail.com é sintaxe válida em uma
rota de e-mail ativa, e ainda assim é uma caixa postal que nunca foi criada.
Confirmar uma caixa postal específica significa a conversa de entrega por SMTP:
conectar ao host de e-mail, emitir RCPT TO, ler a resposta e desconectar antes
de enviar qualquer coisa. Como funciona a verificação de
e-mail percorre esse pipeline completo,
incluindo domínios catch-all.
Em princípio, você pode escrever isso em C# com um TcpClient e comandos SMTP
brutos. Na prática, você não deveria rodar isso a partir do seu servidor de app: a
maioria dos provedores de nuvem bloqueia a porta 25 de saída, a resposta depende
da reputação do IP de onde você conecta, e os servidores de recebimento aplicam
greylisting e limitam a taxa de remetentes desconhecidos. Esta é a camada que vale
a pena delegar a uma infraestrutura construída para isso.
O endpoint de verificação da Qualisend roda o pipeline inteiro a partir de uma
infraestrutura com reputação gerenciada e retorna um veredito em tempo real.
Modele-o como um HttpClient tipado, para que a URL base, o timeout e a chave
fiquem em um só lugar, e desserialize o envelope da resposta com
System.Text.Json:
using System.Net.Http.Json;
using System.Text.Json.Serialization;
public interface IEmailVerifier
{
Task<bool> IsDeliverableAsync(string email, CancellationToken ct = default);
}
public sealed class QualisendVerifier : IEmailVerifier
{
private readonly HttpClient _http;
public QualisendVerifier(HttpClient http) => _http = http;
public async Task<bool> IsDeliverableAsync(string email, CancellationToken ct = default)
{
using var response = await _http.PostAsJsonAsync("verify", new { email }, ct);
// On our own outage or a rate limit, don't block a real signup.
if (!response.IsSuccessStatusCode)
return true;
var body = await response.Content.ReadFromJsonAsync<VerifyResponse>(ct);
return body?.Result.Status != "undeliverable";
}
}
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);
A resposta volta dentro de um envelope result:
{
"result": {
"status": "deliverable",
"score": 95,
"reason": null,
"sub_flags": { "role": false, "disposable": false, "free": false, "catch_all": false }
}
}
status é o seu veredito principal — deliverable, risky, undeliverable ou
unknown. O helper acima reprova de forma dura apenas o undeliverable e deixa
todo o resto passar, mas score, reason e sub_flags (role, disposable, free,
catch-all) estão ali para que você aplique sua própria política — segure o risky
para revisão, sinalize disposable no cadastro.
POST https://api.qualisend.com/v1/verify é um formato de exemplo; confira a
referência da API em /developers para o endpoint exato, o formato
da chave com escopo e cada campo. Não invente campos a partir do exemplo acima —
leia-os na documentação.
Juntando as camadas para validar um endereço de e-mail no ASP.NET Core#
Agora componha as três em um validador do FluentValidation. Encadeie as regras do
mais barato primeiro e defina CascadeMode.Stop para que ele pare na primeira
camada que falhar — as regras assíncronas de DNS e de API só rodam em endereços
que já passaram na sintaxe:
using FluentValidation;
public class SignupRequestValidator : AbstractValidator<SignupRequest>
{
public SignupRequestValidator(IMailRouteChecker dns, IEmailVerifier verifier)
{
RuleFor(x => x.Email)
.Cascade(CascadeMode.Stop) // bail at the first failing layer
.NotEmpty()
.EmailAddress() // layer 1: syntax
.MustAsync((email, ct) => dns.HasMailRouteAsync(email, ct))
.WithMessage("Enter an email on a domain that can receive mail.")
.MustAsync((email, ct) => verifier.IsDeliverableAsync(email, ct))
.WithMessage("We couldn't confirm a mailbox at this address.");
}
}
Ligue os serviços no Program.cs — o LookupClient como singleton, o verificador
como um cliente tipado com a chave lida da configuração (uma variável de ambiente
ou user-secrets em produção, nunca escrita direto no código) e o validador por
varredura de assembly:
using DnsClient;
using FluentValidation;
using System.Net.Http.Headers;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<ILookupClient>(new LookupClient());
builder.Services.AddScoped<IMailRouteChecker, DnsMailRouteChecker>();
builder.Services.AddHttpClient<IEmailVerifier, QualisendVerifier>(client =>
{
client.BaseAddress = new Uri("https://api.qualisend.com/v1/");
client.Timeout = TimeSpan.FromSeconds(10);
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(
"Bearer", builder.Configuration["Qualisend:ApiKey"]);
});
builder.Services.AddValidatorsFromAssemblyContaining<SignupRequestValidator>();
var app = builder.Build();
app.MapPost("/signup", async (
SignupRequest request,
IValidator<SignupRequest> validator,
CancellationToken ct) =>
{
var result = await validator.ValidateAsync(request, ct);
if (!result.IsValid)
return Results.ValidationProblem(result.ToDictionary());
// Every layer passed — safe to persist.
return Results.Ok();
});
app.Run();
Injetar o IValidator<SignupRequest> e chamar ValidateAsync no endpoint é o
padrão recomendado atualmente — ele roda as regras assíncronas corretamente, algo
que a antiga integração automática do MVC nunca fez. O mesmo validador se encaixa
em um controller de MVC sem alteração; resolva-o pelo construtor e chame
ValidateAsync antes de tocar no banco de dados.
Essa ordenação é o truque inteiro: a regra de sintaxe curto-circuita o lixo de graça, a checagem local de DNS descarta domínios mortos por nada, e a API só é acionada para endereços que passaram nas duas. É o mesmo formato de três camadas dos guias de Node.js e de C# — é a estratificação, não o framework, que torna a validação confiável. Rode este pipeline rápido de forma síncrona no cadastro e reserve a verificação mais profunda e em lote para a limpeza de uma lista existente, onde a latência não importa e o rigor sim.
Perguntas frequentes#
O atributo EmailAddress basta para validar um e-mail no ASP.NET Core?#
Para a sintaxe, sim — é a verificação certa para a primeira camada e uma aposta
melhor do que um regex feito à mão. Mas o EmailAddressAttribute valida o
formato (na prática, apenas um único @ com texto de cada lado); ele nunca
resolve o DNS nem contata um servidor de e-mail, então um ModelState.IsValid
aprovado significa "parece um e-mail", não "vai ser entregue". Combine-o com uma
consulta de MX e uma verificação da caixa postal por SMTP antes de confiar no
endereço.
Como executo validação de e-mail assíncrona no ASP.NET Core?#
Os atributos de DataAnnotations são síncronos, então uma consulta de DNS ou uma
chamada de API não cabem em um deles — bloquear na chamada assíncrona arrisca
causar deadlocks. Use o FluentValidation no lugar: escreva regras MustAsync,
injete seus serviços de DNS e de verificação no validador e chame
await validator.ValidateAsync(request) a partir do seu endpoint ou controller.
Essa é a abordagem recomendada atualmente, já que a antiga integração automática
do MVC nunca executou validadores assíncronos.
O ASP.NET Core tem uma forma nativa de consultar registros MX?#
Não. O System.Net.Dns resolve registros de host A e AAAA, mas não tem suporte a
MX, e é por isso que a abordagem padrão é o pacote NuGet DnsClient.NET. Consulte
QueryType.MX, registre um único LookupClient como singleton e trate um
conjunto de respostas vazio — ou uma DnsResponseException — como "sem rota de
e-mail", em vez de deixar que a exceção seja lançada.
Devo validar e-mails no cadastro ou ao limpar uma lista?#
Nos dois momentos, com profundidades diferentes. Rode a sintaxe e a consulta de MX de forma síncrona no cadastro — elas são rápidas o suficiente para bloquear a requisição e dar retorno imediato — e aja também sobre o veredito imediato da API ali mesmo; o guia de cadastro serverless mostra o padrão de ponta a ponta. Reserve a verificação mais profunda e em lote para a limpeza de uma lista existente, onde a latência não importa.
Pronto para adicionar a camada de SMTP? A referência da API tem o
formato exato do /verify, as chaves com escopo e exemplos para copiar e colar,
ou jogue um endereço no verificador de e-mail gratuito para
ver uma string aprovada pelo [EmailAddress] voltar como undeliverable.