Pour valider une adresse e-mail dans ASP.NET Core, la plupart des applications
s'appuient sur l'annotation de données intégrée [EmailAddress], laissent la
liaison de modèle l'exécuter et vérifient ModelState.IsValid. C'est un bon
premier réflexe — l'attribut est livré avec le framework et rejette les
absurdités sans expression régulière codée à la main — mais il ne répond qu'à la
première des trois questions que pose une vraie validation. L'adresse a-t-elle la
bonne forme ? Son domaine peut-il recevoir du courrier ? La boîte aux lettres
existe-t-elle réellement ? ASP.NET Core répond à la première de série, a besoin
d'un paquet NuGet bien connu pour la deuxième, et laisse la troisième à un
problème de réseau qu'il vaut mieux déléguer. Ce guide construit le contrôle sous
la forme de trois couches câblées dans le propre pipeline de validation du
framework et montre exactement où [EmailAddress] s'arrête. C'est le pendant
« framework web » du guide de validation d'e-mail en C#,
qui couvre les mêmes primitives en dehors du pipeline de requête.
La réponse courte#
Utilisez [EmailAddress] (ou le .EmailAddress() de FluentValidation) pour la
syntaxe, DnsClient.NET pour la recherche MX et une API de vérification pour le
contrôle SMTP de la boîte aux lettres — du moins cher au plus cher, en
court-circuitant dès qu'une couche est décisive. Les couches DNS et API relèvent
d'entrées/sorties asynchrones, elles ne conviennent donc pas à un
ValidationAttribute synchrone ; leur foyer idiomatique est un validateur
FluentValidation avec les services injectés via l'injection de dépendances (DI).
N'ouvrez pas vous-même des sockets SMTP bruts depuis votre application pour sonder
les boîtes aux lettres : le port sortant 25 est bloqué chez la plupart des
hébergeurs, et la réponse dépend de la réputation de l'IP d'envoi et du
greylisting que vous ne voulez pas réimplémenter. Chaque couche écarte des
adresses à moindre coût que la précédente ; seule la dernière peut valider une
adresse positivement. Si le pipeline vous est nouveau,
ce qu'est la vérification d'e-mail en présente
d'abord les termes.
Couche 1 : validation du format avec DataAnnotations#
Le contrôle de première couche intégré est l'attribut [EmailAddress] de
System.ComponentModel.DataAnnotations. Placez-le sur votre modèle de requête à
côté de [Required], et la liaison de modèle l'exécute à chaque liaison :
using System.ComponentModel.DataAnnotations;
public class SignupRequest
{
[Required]
[EmailAddress]
public string Email { get; init; } = string.Empty;
}
Dans un contrôleur MVC ou API marqué [ApiController], un attribut en échec
court-circuite en une réponse 400 ValidationProblemDetails avant que le
corps de votre action ne s'exécute — vous n'avez jamais à inspecter ModelState
à la main :
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();
}
}
Sachez exactement ce que cela vous apporte. EmailAddressAttribute vérifie la
forme — en fait il n'exige qu'un unique @ avec quelque chose de chaque côté —
et rien de plus. Il ne résout jamais le DNS, n'ouvre jamais de socket, et ignore
totalement si le domaine existe. definitely-fake@gmail.com passe.
info@company-that-folded.com passe. typo@gmial.com passe. Les trois sont
non délivrables, et aucun attribut qui se contente de lire la chaîne ne vous le
dira jamais — la même raison pour laquelle
la validation d'e-mail par regex échoue :
forme et délivrabilité sont deux questions distinctes, l'une un fait sur la
chaîne et l'autre un fait sur l'internet.
Pourquoi les deux couches suivantes ne peuvent pas être des attributs#
Voici le nœud qui façonne le reste de ce guide : la validation DataAnnotations est
synchrone. ValidationAttribute.IsValid renvoie un ValidationResult, pas une
Task : il n'existe donc aucun moyen propre d'await une requête DNS ou un appel
HTTP à l'intérieur. Bloquer sur .Result ou .GetAwaiter().GetResult() pour
forcer un appel asynchrone dans un attribut synchrone invite à la famine du pool
de threads et aux interblocages sous charge — exactement ce dont vous ne voulez
pas sur un parcours d'inscription.
C'est pourquoi la réponse idiomatique d'ASP.NET Core pour les couches asynchrones
est FluentValidation : ses règles MustAsync sont des validateurs de première
classe renvoyant des Task, et il résout les dépendances via le même conteneur
DI que le reste de votre application, si bien qu'un validateur peut recevoir un
HttpClient ou un service DNS dans son constructeur. Vous pouvez conserver
[EmailAddress] pour la couche un et ajouter FluentValidation pour les couches
deux et trois, ou — comme ci-dessous — laisser le propre .EmailAddress() de
FluentValidation couvrir aussi la syntaxe et regrouper les trois couches au même
endroit.
Couche 2 : le domaine peut-il recevoir du courrier ?#
Un domaine sans route de messagerie ne peut accepter de courrier pour personne :
une seule recherche élimine donc les domaines morts, les noms d'entreprise mal
orthographiés et les TLD inventés. Le hic : ASP.NET Core n'a pas de résolveur
MX intégré. System.Net.Dns résout les enregistrements A et AAAA mais pas le
MX, d'où l'approche standard reposant sur DnsClient.NET — un paquet NuGet
largement utilisé. Enveloppez-le derrière une petite interface pour que le
validateur dépende d'une abstraction, pas de la bibliothèque :
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;
}
}
}
Détachez le domaine au dernier @ avec une expression de plage (range) afin de
ne jamais mal analyser une adresse qui contient légalement plusieurs @.
Enregistrez un unique LookupClient en singleton — il est thread-safe et met les
réponses en cache, si bien qu'une instance par requête ne fait que jeter ce cache.
Certains domaines acceptent le courrier sur un enregistrement A sans MX ; si vous
voulez honorer ce cas limite, repliez-vous sur QueryType.A lorsque l'ensemble
MX est vide.
Couche 3 : la boîte aux lettres existe-t-elle réellement ?#
Les couches un et deux ne peuvent qu'écarter une adresse par exclusion. Un
domaine peut publier des enregistrements MX parfaits et n'avoir malgré tout aucune
boîte aux lettres à l'adresse que vous détenez — noreply-9f2x@gmail.com est une
syntaxe valide sur une route de messagerie active, et c'est pourtant une boîte
aux lettres qui n'a jamais été créée. Confirmer une boîte aux lettres précise
suppose la conversation de livraison SMTP : se connecter à l'hôte de messagerie,
émettre RCPT TO, lire la réponse et se déconnecter avant d'envoyer quoi que ce
soit. Comment fonctionne la vérification
d'e-mail parcourt tout ce pipeline, domaines
catch-all compris.
En principe, vous pouvez scripter cela en C# avec un TcpClient et des commandes
SMTP brutes. En pratique, vous ne devriez pas l'exécuter depuis votre serveur
applicatif : la plupart des fournisseurs cloud bloquent le port sortant 25, la
réponse dépend de la réputation de l'IP depuis laquelle vous vous connectez, et
les serveurs de réception appliquent greylisting et limitation de débit aux
expéditeurs inconnus. C'est la couche qui mérite d'être déléguée à une
infrastructure conçue pour cela.
L'endpoint de vérification de Qualisend exécute tout le pipeline depuis une
infrastructure à réputation gérée et renvoie un verdict en temps réel.
Modélisez-le comme un HttpClient typé pour que l'URL de base, le délai
d'expiration et la clé vivent au même endroit, et désérialisez l'enveloppe de
réponse avec 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 réponse revient à l'intérieur d'une enveloppe result :
{
"result": {
"status": "deliverable",
"score": 95,
"reason": null,
"sub_flags": { "role": false, "disposable": false, "free": false, "catch_all": false }
}
}
status est votre verdict principal — deliverable, risky, undeliverable
ou unknown. L'aide ci-dessus n'échoue durement que sur undeliverable et
laisse passer tout le reste, mais score, reason et sub_flags (role,
disposable, free, catch-all) sont là pour que vous puissiez appliquer votre propre
politique — mettre risky en attente de revue, signaler disposable à
l'inscription. POST https://api.qualisend.com/v1/verify est une forme
d'exemple ; consultez la référence de l'API sur /developers pour
l'endpoint exact, le format des clés à portée limitée et chaque champ. N'inventez
pas de champs à partir de l'exemple ci-dessus — lisez-les dans la documentation.
Assembler les couches pour valider une adresse e-mail dans ASP.NET Core#
Composez maintenant les trois dans un validateur FluentValidation. Enchaînez les
règles du moins cher au plus cher et fixez CascadeMode.Stop pour qu'il s'arrête
à la première couche en échec — les règles asynchrones DNS et API ne s'exécutent
que sur les adresses qui ont déjà passé la syntaxe :
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.");
}
}
Câblez les services dans Program.cs — le LookupClient en singleton, le
vérificateur en client typé avec la clé lue depuis la configuration (une variable
d'environnement ou user-secrets en production, jamais en dur), et le validateur
par balayage d'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();
Injecter IValidator<SignupRequest> et appeler ValidateAsync dans l'endpoint
est le schéma recommandé aujourd'hui — il exécute correctement les règles
asynchrones, là où l'ancienne intégration automatique de MVC ne le faisait jamais.
Le même validateur s'insère sans changement dans un contrôleur MVC ; résolvez-le
depuis le constructeur et appelez ValidateAsync avant de toucher à la base de
données.
Cet ordonnancement est toute l'astuce : la règle de syntaxe court-circuite les déchets gratuitement, le contrôle DNS local écarte les domaines morts pour rien, et l'API n'est sollicitée que pour les adresses qui ont franchi les deux. C'est la même structure à trois couches que les guides Node.js et C# — c'est le découpage en couches, pas le framework, qui rend la validation fiable. Exécutez ce pipeline rapide de façon synchrone à l'inscription et réservez une vérification plus poussée et par lots au nettoyage d'une liste existante, où la latence n'a pas d'importance et où la rigueur, si.
Foire aux questions#
L'attribut EmailAddress suffit-il à valider un e-mail dans ASP.NET Core ?#
Pour la syntaxe, oui — c'est le bon contrôle de première couche, et un choix plus
fiable qu'une expression régulière codée à la main. Mais EmailAddressAttribute
valide la forme (en réalité juste un unique @ avec du texte de chaque côté) ;
il ne résout jamais le DNS et ne contacte aucun serveur de messagerie, si bien
qu'un ModelState.IsValid réussi signifie « ça ressemble à un e-mail », pas
« ça arrivera à destination ». Associez-le à une recherche MX et à une vérification
SMTP de la boîte aux lettres avant de faire confiance à l'adresse.
Comment exécuter une validation d'e-mail asynchrone dans ASP.NET Core ?#
Les attributs DataAnnotations sont synchrones : une requête DNS ou un appel d'API
n'a donc pas sa place dans l'un d'eux — bloquer sur l'appel asynchrone risque de
provoquer des interblocages. Utilisez plutôt FluentValidation : écrivez des règles
MustAsync, injectez vos services DNS et de vérification dans le validateur, puis
appelez await validator.ValidateAsync(request) depuis votre endpoint ou votre
contrôleur. C'est l'approche recommandée aujourd'hui, car l'ancienne intégration
automatique de MVC n'a jamais exécuté les validateurs asynchrones.
ASP.NET Core dispose-t-il d'un moyen intégré de rechercher les enregistrements MX ?#
Non. System.Net.Dns résout les enregistrements d'hôte A et AAAA mais ne prend
pas en charge le MX, d'où l'approche standard reposant sur le paquet NuGet
DnsClient.NET. Interrogez QueryType.MX, enregistrez un unique LookupClient en
singleton, et traitez un ensemble de réponses vide — ou une DnsResponseException
— comme « pas de route de messagerie » plutôt que de le laisser lever une
exception.
Faut-il valider les e-mails à l'inscription ou lors du nettoyage d'une liste ?#
Les deux, à des profondeurs différentes. Exécutez la syntaxe et la recherche MX de façon synchrone à l'inscription — elles sont assez rapides pour bloquer la requête et donner un retour instantané — et agissez aussi sur le verdict immédiat de l'API à cet endroit ; le guide de l'inscription serverless montre le schéma de bout en bout. Réservez une vérification plus poussée et par lots au nettoyage d'une liste existante, où la latence n'a pas d'importance.
Prêt à ajouter la couche SMTP ? La référence de l'API fournit la
forme exacte de /verify, les clés à portée limitée et des exemples à copier-coller,
ou glissez une seule adresse dans le
vérificateur d'e-mail gratuit pour voir une chaîne
approuvée par [EmailAddress] revenir undeliverable.