Skip to content
Comece com 100 créditos de verificação grátis
Qualisend
Todos os artigos
Engenharia / 8 de maio de 2026

Como validar um endereço de e-mail no ASP.NET Core

9 minutes read

Qualisend team
Janela de código intitulada Program.cs validando um e-mail no ASP.NET Core por três camadas que vão afunilando — sintaxe, MX, caixa postal — até um selo verde de entregável.

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.

Your reputation, protected.

Clean your first list in minutes. 100 free credits, no card required.

Get started