Skip to content
Inizia con 100 crediti di verifica gratuiti
Qualisend
Tutti gli articoli
Ingegneria / 8 maggio 2026

Validare un indirizzo email in ASP.NET Core

8 min di lettura

Qualisend team
Validare un indirizzo email in ASP.NET Core

Per validare un indirizzo email in ASP.NET Core, la maggior parte delle app si affida alla data annotation integrata [EmailAddress], lascia che sia il model binding a eseguirla e controlla ModelState.IsValid. È un buon primo passo: l'attributo è incluso nel framework e scarta la spazzatura senza una regex scritta a mano. Ma risponde solo alla prima delle tre domande che pone una vera validazione. L'indirizzo ha la forma corretta? Il suo dominio può ricevere posta? La casella esiste davvero? ASP.NET Core risponde alla prima già di serie, per la seconda ha bisogno di un noto pacchetto NuGet, mentre la terza resta un problema di rete che conviene delegare. Questa guida costruisce il controllo in tre livelli collegati alla pipeline di validazione del framework e mostra esattamente dove si ferma [EmailAddress]. È il complemento per il framework web della guida alla validazione delle email in C#, che tratta le stesse primitive fuori dalla pipeline delle richieste.

In breve#

Usa [EmailAddress] (o .EmailAddress() di FluentValidation) per la sintassi, DnsClient.NET per la ricerca MX e un'API di verifica per il controllo SMTP della casella: si parte dal più economico e ci si ferma non appena un livello è risolutivo. I livelli DNS e API sono I/O asincrono, quindi non stanno in un ValidationAttribute sincrono; il loro posto naturale è un validatore FluentValidation con i servizi iniettati tramite DI. Non aprire socket SMTP grezzi dalla tua app per testare direttamente le caselle: la porta 25 in uscita è bloccata dalla maggior parte degli host e la risposta dipende dalla reputazione dell'IP di invio e dal greylisting, cose che non vuoi reimplementare. Ogni livello esclude indirizzi a un costo minore del successivo; solo l'ultimo può confermarne uno. Se la pipeline è una novità per te, che cos'è la verifica email spiega prima i termini.

Livello 1: validazione del formato con DataAnnotations#

Il controllo integrato del primo livello è l'attributo [EmailAddress] di System.ComponentModel.DataAnnotations. Mettilo sul modello della richiesta accanto a [Required] e il model binding lo eseguirà a ogni binding:

using System.ComponentModel.DataAnnotations;

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

In un controller MVC o API marcato con [ApiController], un attributo non superato interrompe tutto con una risposta 400 ValidationProblemDetails prima che venga eseguito il corpo dell'action, quindi non devi mai controllare 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();
    }
}

Sappi esattamente che cosa ottieni. EmailAddressAttribute controlla la forma (in realtà richiede solo una singola @ con qualcosa su entrambi i lati) e nient'altro. Non risolve mai il DNS, non apre mai un socket e non ha idea se il dominio esista. definitely-fake@gmail.com passa. info@company-that-folded.com passa. typo@gmial.com passa. Tutti e tre sono non recapitabili, e nessun attributo che si limita a leggere la stringa te lo dirà mai, per lo stesso motivo per cui la validazione delle email con le regex non funziona: forma e deliverability sono domande diverse. Una è un fatto che riguarda la stringa, l'altra un fatto che riguarda internet.

Perché i due livelli successivi non possono essere attributi#

Ecco il problema che condiziona il resto di questa guida: la validazione con DataAnnotations è sincrona. ValidationAttribute.IsValid restituisce un ValidationResult, non un Task, quindi non c'è un modo pulito di fare await su una query DNS o una chiamata HTTP al suo interno. Bloccarsi su .Result o .GetAwaiter().GetResult() per forzare una chiamata asincrona in un attributo sincrono espone a thread-pool starvation e deadlock sotto carico: proprio ciò che non vuoi nel flusso di iscrizione.

Per questo la risposta naturale di ASP.NET Core per i livelli asincroni è FluentValidation: le sue regole MustAsync sono validatori a tutti gli effetti che restituiscono un Task, e risolve le dipendenze tramite lo stesso container DI del resto della tua app, quindi un validatore può ricevere nel costruttore un HttpClient o un servizio DNS. Puoi tenere [EmailAddress] per il primo livello e aggiungere FluentValidation per il secondo e il terzo oppure, come vedi sotto, lasciare che .EmailAddress() di FluentValidation copra anche la sintassi e tenere tutti e tre i livelli in un unico posto.

Livello 2: il dominio può ricevere posta?#

Un dominio senza instradamento della posta non può accettare posta per nessuno, quindi una sola ricerca elimina domini inattivi, nomi aziendali scritti male e TLD inventati. Il problema: ASP.NET Core non ha un resolver MX integrato. System.Net.Dns risolve i record A e AAAA ma non gli MX, quindi l'approccio standard è DnsClient.NET, un pacchetto NuGet molto diffuso. Mettilo dietro una piccola interfaccia, così il validatore dipende da un'astrazione e non dalla libreria:

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 il dominio all'ultima @ con un'espressione di intervallo, così non interpreti mai male un indirizzo che legittimamente ne contiene più di una. Registra un solo LookupClient come singleton: è thread-safe e mette in cache le risposte, quindi un'istanza per richiesta non fa che buttare via quella cache. Alcuni domini accettano posta su un record A senza MX; se vuoi gestire questo caso limite, ripiega su QueryType.A quando l'insieme degli MX è vuoto.

Livello 3: la casella esiste davvero?#

I livelli uno e due possono solo escludere un indirizzo. Un dominio può pubblicare record MX perfetti e non avere comunque alcuna casella all'indirizzo che hai in mano: noreply-9f2x@gmail.com ha una sintassi valida e un instradamento della posta attivo, eppure è una casella che non è mai stata creata. Confermare una casella specifica richiede la conversazione SMTP di consegna: connettersi all'host di posta, inviare RCPT TO, leggere la risposta e disconnettersi prima di spedire qualsiasi cosa. Come funziona la verifica email illustra l'intera pipeline, domini catch-all compresi.

In linea di principio puoi scriverlo in C# con un TcpClient e comandi SMTP grezzi. In pratica non dovresti eseguirlo dal tuo server applicativo: la maggior parte dei provider cloud blocca la porta 25 in uscita, la risposta dipende dalla reputazione dell'IP da cui ti connetti e i server riceventi applicano il greylisting e limitano la frequenza dei mittenti sconosciuti. Questo è il livello da delegare a un'infrastruttura costruita apposta.

L'endpoint di verifica di Qualisend esegue l'intera pipeline da un'infrastruttura con reputazione gestita e restituisce un esito in tempo reale. Modellalo come un HttpClient tipizzato, così URL di base, timeout e chiave stanno in un unico posto, e deserializza l'involucro della risposta 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)
    {
        try
        {
            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";
        }
        catch (HttpRequestException)
        {
            return true; // network failure: fail open too
        }
        catch (TaskCanceledException) when (!ct.IsCancellationRequested)
        {
            return true; // the 10-second client timeout fired: fail open too
        }
    }
}

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 risposta arriva dentro un involucro result:

{
  "job_id": "6f1c2e0a-9b3d-4a1e-8c77-1a2b3c4d5e6f",
  "result": {
    "status": "unknown",
    "score": 61,
    "reason": null,
    "smtp": "pending",
    "sub_flags": { "role": false, "disposable": false, "free_mail": false, "catch_all": false }
  }
}

status è il tuo esito principale: deliverable, risky, undeliverable o unknown. L'helper qui sopra fa fallire solo undeliverable e lascia passare tutto il resto, ma score, reason e sub_flags (role, disposable, free_mail, catch_all) ci sono proprio perché tu possa applicare la tua policy: tenere risky da parte per una revisione, segnalare disposable all'iscrizione. L'endpoint è POST https://app.qualisend.com/api/v1/verify; consulta il riferimento API su /developers per il formato delle chiavi con scope e per tutti i campi. Non inventare campi partendo dall'esempio qui sopra: leggili dalla documentazione.

Questa prima risposta è l'esito immediato. Finché result.smtp è "pending", il test SMTP della casella è ancora in corso e reason resta null; l'esito definitivo arriva su GET https://app.qualisend.com/api/v1/jobs/{job_id}. Usa l'esito immediato all'iscrizione e, se vuoi quello definitivo, interroga quel job da un task in background, mai dentro la richiesta e mai richiamando /verify.

Mettere insieme i livelli per validare un indirizzo email in ASP.NET Core#

Ora componi tutti e tre in un validatore FluentValidation. Concatena le regole partendo dalla più economica e imposta CascadeMode.Stop in modo che si fermi al primo livello non superato. Le regole asincrone DNS e API vengono così eseguite solo sugli indirizzi che hanno già superato il controllo di sintassi:

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

Collega i servizi in Program.cs: il LookupClient come singleton, il verificatore come client tipizzato con la chiave letta dalla configurazione (una variabile d'ambiente o gli user-secrets in produzione, mai scritta nel codice) e il validatore tramite scansione dell'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://app.qualisend.com/api/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();

Iniettare IValidator<SignupRequest> e chiamare ValidateAsync nell'endpoint è lo schema oggi raccomandato: esegue correttamente le regole asincrone, cosa che la vecchia integrazione automatica con MVC non ha mai fatto. Lo stesso validatore si inserisce invariato in un controller MVC: risolvilo dal costruttore e chiama ValidateAsync prima di toccare il database.

Tutto il segreto sta in quest'ordine: la regola di sintassi elimina la spazzatura gratis, il controllo DNS locale esclude i domini inattivi a costo zero e l'API viene chiamata solo per gli indirizzi che hanno superato entrambi. È la stessa struttura a tre livelli delle guide per Node.js e C#: a rendere affidabile la validazione è la stratificazione, non il framework. Esegui questa pipeline veloce in modo sincrono all'iscrizione e riserva la verifica più approfondita e in batch alla pulizia di una lista esistente, dove la latenza non conta e la completezza sì.

Domande frequenti

01L'attributo EmailAddress basta per validare un'email in ASP.NET Core?

Per la sintassi, sì: è il controllo giusto per il primo livello e una scelta migliore di una regex scritta a mano. Ma EmailAddressAttribute valida la forma (in pratica solo una singola @ con del testo su entrambi i lati): non risolve mai il DNS e non contatta mai un server di posta, quindi superare ModelState.IsValid significa "sembra un'email", non "verrà recapitata". Abbinalo a una ricerca MX e a un controllo SMTP della casella prima di fidarti dell'indirizzo.

02Come eseguo una validazione email asincrona in ASP.NET Core?

Gli attributi DataAnnotations sono sincroni, quindi una query DNS o una chiamata API non vanno messe lì dentro: bloccarsi sulla chiamata asincrona rischia di causare deadlock. Usa invece FluentValidation: scrivi regole MustAsync, inietta i tuoi servizi DNS e di verifica nel validatore e chiama await validator.ValidateAsync(request) dal tuo endpoint o controller. È l'approccio oggi raccomandato, dato che la vecchia integrazione automatica con MVC non ha mai eseguito i validatori asincroni.

03ASP.NET Core ha un modo integrato per cercare i record MX?

No. System.Net.Dns risolve i record host A e AAAA ma non supporta gli MX, ed è per questo che l'approccio standard è il pacchetto NuGet DnsClient.NET. Interroga QueryType.MX, registra un solo LookupClient come singleton e considera una risposta vuota, o una DnsResponseException, come "nessun instradamento della posta" invece di lasciar propagare l'eccezione.

04Devo validare le email all'iscrizione o quando pulisco una lista?

In entrambi i casi, con profondità diverse. Esegui sintassi e ricerca MX in modo sincrono all'iscrizione (sono abbastanza veloci da bloccare la richiesta e dare un riscontro immediato) e usa lì anche l'esito immediato dell'API; la guida all'iscrizione serverless mostra lo schema dall'inizio alla fine. Riserva la verifica più approfondita e in batch alla pulizia di una lista esistente, dove la latenza non conta.


Vuoi aggiungere il livello SMTP? Il riferimento API contiene la forma esatta di /verify, le chiavi con scope ed esempi pronti da copiare e incollare, oppure inserisci un indirizzo nella verifica email gratuita per vedere una stringa approvata da [EmailAddress] tornare undeliverable.

La tua reputazione, protetta.

Pulisci la tua prima lista in pochi minuti. 100 crediti gratuiti, senza carta di credito.

Inizia