Start Debugging

.NET 11 Preview 6 で非同期バリデーションが Minimal API に対応

Preview 6 は AsyncValidationAttribute と IAsyncValidatableObject を追加し、DataAnnotations のルールがスレッドをブロックせずに、エンドポイントの実行前にデータベースへ問い合わせられるようにします。

.NET 11 Preview 6 は、DataAnnotations が登場以来抱えてきたギャップを埋めます。バリデーションは同期的でした。ルールがデータベースを確認する必要がある場合(このメールはすでに使われているか、この枠はまだ空いているか)、IsValid の中でスレッドをブロックするか、DataAnnotations をあきらめて FluentValidation に頼るしかありませんでした。Preview 6 は第一級の非同期バリデーターを追加し、.NET 10 で導入された Minimal API の組み込みバリデーションに組み込みます。

2 つの新しいフック

System.ComponentModel.DataAnnotations に 2 つの型が登場します。1 つ目は単一メンバー向けのルールである 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;
    }
}

2 つ目は、複数のプロパティにまたがる、あるいはオブジェクト全体を必要とするルール向けの 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)]);
    }
}

組み込み方

.NET 10 ですでに導入された AddValidation() の呼び出しを超える新しいオプトインはありません。これを登録すると、フレームワークはエンドポイントの本体が実行される前に非同期バリデーターを実行します。

builder.Services.AddValidation();

app.MapPost("/reservations", (ReservationRequest request) => Results.Ok(request));

不正なリクエストは、同期パスとまったく同じように、ValidationProblemDetails のペイロードを伴う 400 で短絡します。内部的にはベースライブラリの新しい Validator.ValidateObjectAsync API に乗っているため、ASP.NET Core の外でも動作します。

知っておく価値のある点

非同期バリデーションは、うっかり直列のラウンドトリップを招きがちです。そのためフレームワークは可能な限り処理を並行して実行します。同じメンバー上の非同期属性は同時に開始し、コレクションの項目は並列にバリデーションされます。それでもメンバー、型、IValidatableObject の間の既存の順序は保たれるため、安価な [Required] チェックはデータベースへのクエリのコストを払う前に素早く失敗します。これを Preview 6 の自動 CSRF 保護と組み合わせれば、組み込みパイプラインは 1 バージョン前よりもはるかに広い範囲をカバーします。

.NET 11 Preview 6 SDK を入手し、net11.0 をターゲットにして、バリデーターの全体像については ASP.NET Core のリリースノートを参照してください。

Comments

Sign in with GitHub to comment. Reactions and replies thread back to the comments repo.

< 戻る