Асинхронная валидация приходит в Minimal APIs в .NET 11 Preview 6
Preview 6 добавляет AsyncValidationAttribute и IAsyncValidatableObject, чтобы правила DataAnnotations могли обращаться к базе данных до запуска вашего endpoint, не блокируя поток.
.NET 11 Preview 6 закрывает пробел, который DataAnnotations несёт с момента выхода: валидация была синхронной. Если правилу требовалось обратиться к базе данных (занят ли этот email, свободен ли ещё этот слот?), вы либо блокировали поток внутри IsValid, либо отказывались от DataAnnotations и переходили на FluentValidation. Preview 6 добавляет полноценные асинхронные валидаторы и связывает их со встроенной валидацией Minimal APIs, представленной в .NET 10.
Две новые точки подключения
В System.ComponentModel.DataAnnotations появляются два типа. Первый — это AsyncValidationAttribute для правила на отдельном члене:
public sealed class UniqueEmailAttribute : AsyncValidationAttribute
{
// Still required, but throws if the attribute is async-only.
protected override ValidationResult? IsValid(object? value, ValidationContext context)
=> throw new InvalidOperationException("Use IsValidAsync.");
protected override async Task<ValidationResult?> IsValidAsync(
object? value, ValidationContext context, CancellationToken cancellationToken)
{
var db = context.GetRequiredService<AppDbContext>();
var exists = await db.Users.AnyAsync(u => u.Email == (string?)value, cancellationToken);
return exists ? new ValidationResult("Email is already registered.") : ValidationResult.Success;
}
}
Второй — это IAsyncValidatableObject для правил, которые охватывают несколько свойств или требуют объект целиком. Он возвращает IAsyncEnumerable<ValidationResult>, и поскольку он расширяет IValidatableObject, вы по-прежнему реализуете синхронный Validate (который выбрасывает исключение, когда вы валидируете только асинхронно):
public class ReservationRequest : IAsyncValidatableObject
{
public DateOnly Date { get; set; }
public int PartySize { get; set; }
public IEnumerable<ValidationResult> Validate(ValidationContext context)
=> throw new InvalidOperationException("Use ValidateAsync.");
public async IAsyncEnumerable<ValidationResult> ValidateAsync(
ValidationContext context,
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
var db = context.GetRequiredService<AppDbContext>();
var taken = await db.Reservations.CountAsync(r => r.Date == Date, cancellationToken);
if (taken + PartySize > 40)
yield return new ValidationResult("No capacity left for that date.", [nameof(PartySize)]);
}
}
Как это подключить
Никакого нового включения помимо вызова AddValidation(), который уже появился в .NET 10, не требуется. Зарегистрируйте его, и фреймворк запустит ваши асинхронные валидаторы до того, как выполнится тело endpoint:
builder.Services.AddValidation();
app.MapPost("/reservations", (ReservationRequest request) => Results.Ok(request));
Недопустимый запрос обрывается на 400 с полезной нагрузкой ValidationProblemDetails, ровно как в синхронном пути. Под капотом это опирается на новый API Validator.ValidateObjectAsync в базовых библиотеках, поэтому работает и за пределами ASP.NET Core.
Деталь, которую стоит знать
Асинхронная валидация случайно провоцирует последовательные обращения, поэтому фреймворк выполняет работу параллельно там, где может: асинхронные атрибуты на одном члене стартуют вместе, а элементы коллекции валидируются параллельно. При этом сохраняется существующий порядок между членом, типом и IValidatableObject, так что дешёвая проверка [Required] падает быстро, прежде чем вы заплатите за запрос к базе данных. Сочетайте это с автоматической защитой от CSRF из Preview 6, и встроенный конвейер покрывает заметно больше, чем версией ранее.
Возьмите SDK .NET 11 Preview 6, нацельтесь на net11.0 и прочитайте заметки о выпуске ASP.NET Core, чтобы узнать всю поверхность валидаторов.
Comments
Sign in with GitHub to comment. Reactions and replies thread back to the comments repo.