Skip to content
Empieza con 100 créditos de verificación gratis
Qualisend
Todos los artículos
Ingeniería / 8 de mayo de 2026

Validar una dirección de correo electrónico en ASP.NET Core

8 minutes read

Qualisend team
Ventana de código titulada Program.cs validando un correo en ASP.NET Core a través de tres capas que van filtrando —sintaxis, MX, buzón— hasta una insignia verde de entregable.

Para validar una dirección de correo electrónico en ASP.NET Core, la mayoría de las aplicaciones se apoyan en la anotación de datos integrada [EmailAddress], dejan que el enlace de modelos la ejecute y comprueban ModelState.IsValid. Es un buen primer paso —el atributo viene con el framework y rechaza basura sin una expresión regular hecha a mano—, pero solo responde a la primera de las tres preguntas que plantea una validación real. ¿Tiene la dirección la forma correcta? ¿Puede su dominio recibir correo? ¿Existe realmente el buzón? ASP.NET Core responde la primera de fábrica, necesita un paquete NuGet conocido para la segunda y deja la tercera en manos de un problema de red que conviene delegar. Esta guía construye la comprobación como tres capas conectadas a la propia canalización de validación del framework y muestra exactamente dónde se detiene [EmailAddress]. Es el complemento para frameworks web de la guía de validación de correo en C#, que cubre las mismas primitivas fuera de la canalización de peticiones.

La respuesta corta#

Usa [EmailAddress] (o el .EmailAddress() de FluentValidation) para la sintaxis, DnsClient.NET para la búsqueda de MX y una API de verificación para la comprobación del buzón por SMTP: primero lo más barato, cortocircuitando en cuanto una capa sea decisiva. Las capas de DNS y de API son E/S asíncronas, así que no encajan en un ValidationAttribute síncrono; su hogar idiomático es un validador de FluentValidation con los servicios inyectados mediante DI. No abras sockets SMTP en crudo desde tu aplicación para sondear buzones por tu cuenta: el puerto 25 de salida está bloqueado en la mayoría de los hosts, y la respuesta depende de la reputación de la IP emisora y del greylisting que no querrás reimplementar. Cada capa descarta direcciones de forma más barata que la anterior; solo la última puede dar por buena una dirección. Si la canalización es nueva para ti, qué es la verificación de correo cubre primero los términos.

Capa 1: validación de formato con DataAnnotations#

La comprobación integrada de primera capa es el atributo [EmailAddress] de System.ComponentModel.DataAnnotations. Ponlo en tu modelo de petición junto a [Required], y el enlace de modelos lo ejecuta en cada enlace:

using System.ComponentModel.DataAnnotations;

public class SignupRequest
{
    [Required]
    [EmailAddress]
    public string Email { get; init; } = string.Empty;
}

En un controlador MVC o de API marcado con [ApiController], un atributo fallido cortocircuita en una respuesta 400 ValidationProblemDetails antes de que se ejecute el cuerpo de tu acción: nunca tienes que inspeccionar ModelState a mano:

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();
    }
}

Ten claro exactamente qué te aporta esto. EmailAddressAttribute comprueba la forma —de hecho solo exige una única @ con algo a cada lado— y nada más. Nunca resuelve DNS, nunca abre un socket y no tiene ni idea de si el dominio existe. definitely-fake@gmail.com pasa. info@company-that-folded.com pasa. typo@gmial.com pasa. Las tres son inentregables, y ningún atributo que solo lea la cadena te lo dirá jamás: es la misma razón por la que la validación de correo con regex falla: la forma y la entregabilidad son preguntas distintas, una es un hecho sobre la cadena y la otra un hecho sobre internet.

Por qué las dos capas siguientes no pueden ser atributos#

Aquí está el detalle que da forma al resto de esta guía: la validación con DataAnnotations es síncrona. ValidationAttribute.IsValid devuelve un ValidationResult, no un Task, así que no hay una forma limpia de hacer await de una consulta DNS o de una llamada HTTP dentro de uno. Bloquear con .Result o .GetAwaiter().GetResult() para forzar una llamada asíncrona dentro de un atributo síncrono invita a la inanición del grupo de subprocesos y a interbloqueos bajo carga: exactamente lo que no quieres en una ruta de registro.

Por eso la respuesta idiomática de ASP.NET Core para las capas asíncronas es FluentValidation: sus reglas MustAsync son validadores de primera clase que devuelven Task, y resuelve dependencias a través del mismo contenedor de DI que el resto de tu aplicación, de modo que un validador puede recibir un HttpClient o un servicio de DNS en su constructor. Puedes conservar [EmailAddress] para la primera capa y añadir FluentValidation para la segunda y la tercera, o —como abajo— dejar que el propio .EmailAddress() de FluentValidation cubra también la sintaxis y mantener las tres capas en un solo lugar.

Capa 2: ¿puede el dominio recibir correo?#

Un dominio sin ruta de correo no puede aceptar correo para nadie, así que una búsqueda elimina dominios muertos, nombres de empresa mal escritos y TLD inventados. El detalle: ASP.NET Core no tiene un resolvedor de MX integrado. System.Net.Dns resuelve registros A y AAAA, pero no MX, así que el enfoque estándar es DnsClient.NET, un paquete NuGet muy usado. Envuélvelo tras una pequeña interfaz para que el validador dependa de una abstracción, no de la 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;
        }
    }
}

Separa el dominio en la última @ con una expresión de rango para no analizar mal nunca una dirección que legalmente contenga más de una. Registra un único LookupClient como singleton: es seguro para subprocesos y almacena en caché las respuestas, así que una instancia por petición simplemente tira esa caché a la basura. Algunos dominios aceptan correo en un registro A sin MX; si quieres respetar ese caso límite, recurre a QueryType.A cuando el conjunto de MX esté vacío.

Capa 3: ¿existe realmente el buzón?#

Las capas uno y dos solo pueden descartar una dirección. Un dominio puede publicar registros MX perfectos y aun así no tener buzón en la dirección que tienes entre manos: noreply-9f2x@gmail.com es sintaxis válida en una ruta de correo activa, y sigue siendo un buzón que nunca se creó. Confirmar un buzón concreto significa la conversación de entrega SMTP: conectar con el host de correo, emitir RCPT TO, leer la respuesta y desconectar antes de enviar nada. Cómo funciona la verificación de correo recorre esa canalización completa, dominios catch-all incluidos.

En principio puedes programar esto en C# con un TcpClient y comandos SMTP en crudo. En la práctica no deberías ejecutarlo desde tu servidor de aplicaciones: la mayoría de los proveedores en la nube bloquean el puerto 25 de salida, la respuesta depende de la reputación de la IP desde la que conectas, y los servidores receptores aplican greylisting y limitan la tasa a remitentes desconocidos. Esta es la capa que vale la pena delegar en una infraestructura construida para ello.

El endpoint de verificación de Qualisend ejecuta toda la canalización desde una infraestructura con reputación gestionada y devuelve un veredicto en tiempo real. Modélalo como un HttpClient tipado para que la URL base, el tiempo de espera y la clave vivan en un solo lugar, y deserializa el sobre de la respuesta con 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);

La respuesta vuelve dentro de un sobre result:

{
  "result": {
    "status": "deliverable",
    "score": 95,
    "reason": null,
    "sub_flags": { "role": false, "disposable": false, "free": false, "catch_all": false }
  }
}

status es tu veredicto principal: deliverable, risky, undeliverable o unknown. El ayudante de arriba solo falla en firme con undeliverable y deja pasar todo lo demás, pero score, reason y sub_flags (role, disposable, free, catch-all) están ahí para que apliques tu propia política: retén risky para revisión, marca disposable en el registro. POST https://api.qualisend.com/v1/verify es una forma de ejemplo; consulta la referencia de la API en /developers para conocer el endpoint exacto, el formato de clave con alcance y todos los campos. No inventes campos a partir del ejemplo de arriba: léelos de la documentación.

Uniendo las capas para validar una dirección de correo en ASP.NET Core#

Ahora compón las tres en un validador de FluentValidation. Encadena las reglas empezando por la más barata y establece CascadeMode.Stop para que se detenga en la primera capa que falle: las reglas asíncronas de DNS y de API solo se ejecutan en direcciones que ya superaron la sintaxis:

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.");
    }
}

Conecta los servicios en Program.cs: el LookupClient como singleton, el verificador como cliente tipado con la clave leída desde la configuración (una variable de entorno o user-secrets en producción, nunca escrita en el código) y el validador mediante escaneo de ensamblado:

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();

Inyectar IValidator<SignupRequest> y llamar a ValidateAsync en el endpoint es el patrón recomendado actualmente: ejecuta las reglas asíncronas correctamente, allí donde la antigua integración automática de MVC nunca lo hizo. El mismo validador encaja sin cambios en un controlador MVC; resuélvelo desde el constructor y llama a ValidateAsync antes de tocar la base de datos.

Ese orden es todo el truco: la regla de sintaxis cortocircuita la basura gratis, la comprobación local de DNS descarta dominios muertos por nada, y la API solo se consulta para las direcciones que superaron ambas. Es la misma estructura de tres capas que las guías de Node.js y de C#: lo que hace fiable la validación es la estratificación, no el framework. Ejecuta esta canalización rápida de forma síncrona en el registro y reserva la verificación más profunda y por lotes para limpiar una lista existente, donde la latencia no importa y la exhaustividad sí.

Preguntas frecuentes#

¿Basta con el atributo EmailAddress para validar un correo en ASP.NET Core?#

Para la sintaxis, sí: es la comprobación adecuada de primera capa y una apuesta mejor que una expresión regular hecha a mano. Pero EmailAddressAttribute valida la forma (en realidad solo una única @ con texto a ambos lados); nunca resuelve DNS ni contacta con un servidor de correo, así que que ModelState.IsValid dé el visto bueno significa "parece un correo", no "se entregará". Combínalo con una búsqueda de MX y una comprobación del buzón por SMTP antes de fiarte de la dirección.

¿Cómo ejecuto la validación asíncrona de correo en ASP.NET Core?#

Los atributos de DataAnnotations son síncronos, así que una consulta DNS o una llamada a una API no encajan ahí: bloquear a la espera de la llamada asíncrona arriesga interbloqueos. Usa FluentValidation en su lugar: escribe reglas MustAsync, inyecta tus servicios de DNS y de verificación en el validador y llama a await validator.ValidateAsync(request) desde tu endpoint o controlador. Ese es el enfoque recomendado actualmente, ya que la antigua integración automática de MVC nunca ejecutaba validadores asíncronos.

¿Tiene ASP.NET Core una forma integrada de buscar registros MX?#

No. System.Net.Dns resuelve registros de host A y AAAA, pero no admite MX, y por eso el enfoque estándar es el paquete NuGet DnsClient.NET. Consulta QueryType.MX, registra un único LookupClient como singleton y trata un conjunto de respuestas vacío —o una DnsResponseException— como "sin ruta de correo" en lugar de dejar que lance una excepción.

¿Debo validar los correos en el registro o al limpiar una lista?#

Ambos, con distinta profundidad. Ejecuta la sintaxis y la búsqueda de MX de forma síncrona en el registro —son lo bastante rápidas para bloquear la petición y dar retroalimentación instantánea— y actúa también allí sobre el veredicto inmediato de la API; la guía de registro serverless muestra el patrón de principio a fin. Reserva la verificación más profunda y por lotes para limpiar una lista existente, donde la latencia no importa.


¿Listo para añadir la capa SMTP? La referencia de la API tiene la forma exacta de /verify, las claves con alcance y ejemplos para copiar y pegar, o introduce una dirección en el verificador de correo gratuito para ver cómo una cadena aprobada por [EmailAddress] vuelve como undeliverable.

Your reputation, protected.

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

Get started