ASP.NET Core Blazor の高度なフォーム検証

Note

これは、この記事の最新バージョンではありません。 現在のリリースについては、 この記事の .NET 10 バージョンを参照してください。

Warning

このバージョンの ASP.NET Core はサポート対象から除外されました。 詳細については、 .NET および .NET Core サポート ポリシーを参照してください。 現在のリリースについては、 この記事の .NET 10 バージョンを参照してください。

この記事では、再利用可能な検証コンポーネントとリモート検証について説明します。 データ注釈、直接EditContext検証、メッセージ表示、スタイル設定、状態、送信動作など、一般的なフォーム検証 API については、フォームの検証 ASP.NET Core Blazor参照してください。

Blazor API と最小 API によって共有されるモデルベースの検証規則については、ASP.NET Coreの検証を参照してください。

静的サーバー側レンダリング (静的 SSR) でのブラウザー検証については、静的 SSR のクライアント側フォーム検証 ASP.NET Core Blazor参照してください。

バリデーター コンポーネントをビルドする

検証コンポーネントは、フォームの EditContext と ValidationMessageStoreを使用する検証をカプセル化します。 これは、同じ検証動作が複数のフォームで使用される場合や、モデルの検証属性ではなくサービスからエラーが発生した場合に便利です。

コンポーネント:

  • フォームの EditContext をカスケード パラメーターとして受け取ります。
  • エラー用のメッセージ ストアを作成します。
  • 検証が要求されたときに古いフォーム エラーをクリアします。
  • フィールドが変更されたときに、フィールドの古いエラーをクリアします。
  • エラーを表示およびクリアするためのメソッドを公開します。
  • 破棄されると、イベント ハンドラーの登録を解除します。

CustomValidation.razor:

@implements IDisposable

@code {
    [CascadingParameter]
    private EditContext? CurrentEditContext { get; set; }

    private ValidationMessageStore messages = default!;

    protected override void OnInitialized()
    {
        if (CurrentEditContext is null)
        {
            throw new InvalidOperationException(
                "CustomValidation requires a cascading EditContext.");
        }

        messages = new ValidationMessageStore(CurrentEditContext);
        CurrentEditContext.OnValidationRequested +=
            HandleValidationRequested;
        CurrentEditContext.OnFieldChanged += HandleFieldChanged;
    }

    public void DisplayErrors(IDictionary<string, string[]> errors)
    {
        foreach (var error in errors)
        {
            messages.Add(
                CurrentEditContext!.Field(error.Key),
                error.Value);
        }

        CurrentEditContext!.NotifyValidationStateChanged();
    }

    public void ClearErrors()
    {
        messages.Clear();
        CurrentEditContext!.NotifyValidationStateChanged();
    }

    private void HandleValidationRequested(
        object? sender, ValidationRequestedEventArgs e) =>
        ClearErrors();

    private void HandleFieldChanged(
        object? sender, FieldChangedEventArgs e)
    {
        messages.Clear(e.FieldIdentifier);
        CurrentEditContext!.NotifyValidationStateChanged();
    }

    public void Dispose()
    {
        if (CurrentEditContext is not null)
        {
            CurrentEditContext.OnValidationRequested -=
                HandleValidationRequested;
            CurrentEditContext.OnFieldChanged -= HandleFieldChanged;
        }
    }
}

フォームまたはサービスにエラーが表示される場合は、コンポーネントを EditForm 内に配置し、コンポーネント参照をキャプチャします。

<EditForm Model="Model" OnValidSubmit="Submit">
    <DataAnnotationsValidator />
    <CustomValidation @ref="customValidation" />
    <ValidationSummary />

    ...
</EditForm>

@code {
    private CustomValidation? customValidation;
}

コンポーネントは、 DataAnnotationsValidatorと共に使用できます。 各検証コントロールには、同じ EditContextに関連付けられた独自のメッセージ ストアがあり、両方の検証コントロールからのメッセージを ValidationMessage および ValidationSummary 表示します。

外部エラーを受け入れる代わりに検証コントロール コンポーネント内にビジネス ルールを実装するには、 HandleValidationRequested または HandleFieldChanged からルールを実行し、そのメッセージを messagesに追加します。 フォーム コンポーネントでこれを直接実行する小さな例については、フォームの検証 ASP.NET Core Blazor参照してください。

非同期検証を追加する

同じコンポーネント パターンで非同期処理がサポートされます。

  • OnValidationRequested ハンドラーで、e.AddAsyncValidatorを呼び出してフォーム レベルの作業を登録します。 EditForm は、 OnValidSubmit または OnInvalidSubmitを呼び出す前に待機します。
  • OnFieldChanged ハンドラーで、EditContext.RegisterAsyncFieldValidatorを呼び出して、そのフィールドの検証を開始します。 同じフィールドに対して別の検証を開始すると、前の操作が優先され、取り消されます。

フォーム レベルの非同期検証の場合:

private void HandleValidationRequested(
    object? sender, ValidationRequestedEventArgs e) =>
    e.AddAsyncValidator(ValidateAsync);

private async Task ValidateAsync(CancellationToken cancellationToken)
{
    var field = CurrentEditContext!.Field(nameof(Model.Username));
    messages.Clear(field);

    var available = await Http.GetFromJsonAsync<bool>(
        $"api/usernames/available?value={Uri.EscapeDataString(Model.Username)}",
        cancellationToken);

    if (!available)
    {
        messages.Add(field, "The username is already taken.");
    }

    CurrentEditContext.NotifyValidationStateChanged();
}

フィールド レベルの非同期検証の場合:

private void HandleFieldChanged(
    object? sender, FieldChangedEventArgs e)
{
    CurrentEditContext!.RegisterAsyncFieldValidator(
        e.FieldIdentifier,
        token => ValidateFieldAsync(e.FieldIdentifier, token));
}

指定されたキャンセル トークンを I/O に渡します。 操作を開始する前に前のメッセージをクリアし、例外の後に部分的な結果を発行しないようにし、メッセージの更新後に NotifyValidationStateChanged を呼び出します。

操作が置き換えられたか、検証パスが取り消されたために取り消された操作は破棄されます。 その他の例外により、フィールドまたはフォームがエラー状態になります。 保留中の状態とエラー状態を表示する方法については、フォームの検証 ASP.NET Core Blazor参照してください。

フォーム レベルとフィールドごとの非同期検証を組み合わせた完全なコンポーネントについては、次のサンプルを参照してください。

@implements IDisposable
@inject HttpClient Http

@* A validator component that runs asynchronous validation, both when the whole form is
   validated on submit and per field as the user edits the Username field. *@

@code {
    [CascadingParameter]
    private EditContext? CurrentEditContext { get; set; }

    [Parameter, EditorRequired]
    public RegistrationModel Model { get; set; } = default!;

    private ValidationMessageStore? messages;

    protected override void OnInitialized()
    {
        ArgumentNullException.ThrowIfNull(CurrentEditContext);
        messages = new ValidationMessageStore(CurrentEditContext);
        CurrentEditContext.OnValidationRequested += OnValidationRequested;
        CurrentEditContext.OnFieldChanged += OnFieldChanged;
    }

    // Registers asynchronous work for the whole form. EditForm awaits it before invoking
    // OnValidSubmit.
    private void OnValidationRequested(
        object? sender, ValidationRequestedEventArgs e) =>
        e.AddAsyncValidator(ValidateUsernameAsync);

    // Registers asynchronous work for a single field. A new registration supersedes and
    // cancels any validation already in flight for the field.
    private void OnFieldChanged(object? sender, FieldChangedEventArgs e)
    {
        if (e.FieldIdentifier.FieldName != nameof(RegistrationModel.Username))
        {
            return;
        }

        CurrentEditContext!.RegisterAsyncFieldValidator(
            e.FieldIdentifier,
            token => CheckAsync(e.FieldIdentifier, token));
    }

    private Task ValidateUsernameAsync(CancellationToken token) =>
        CheckAsync(CurrentEditContext!.Field(nameof(Model.Username)), token);

    private async Task CheckAsync(FieldIdentifier field, CancellationToken token)
    {
        messages!.Clear(field);

        var available = await Http.GetFromJsonAsync<bool>(
            $"api/usernames/available?value={Uri.EscapeDataString(Model.Username)}",
            token);

        if (!available)
        {
            messages.Add(field, "The username is already taken.");
        }

        CurrentEditContext!.NotifyValidationStateChanged();
    }

    public void Dispose()
    {
        if (CurrentEditContext is not null)
        {
            CurrentEditContext.OnValidationRequested -= OnValidationRequested;
            CurrentEditContext.OnFieldChanged -= OnFieldChanged;
        }
    }
}

モデルの非同期検証属性については、「ASP.NET Coreでの検証」を参照してください。

検証コントロール コンポーネント コードは、コンポーネントが実行される場所で実行されます。 Interactive WebAssembly ではブラウザーで実行され、対話型サーバーの回線を介してサーバー上で実行されます。

静的 SSR では、検証コントロール コンポーネント コードはフォームポスト中にサーバー上で実行され、要求間のライブ.NETフィールド検証は提供されません。

対話型 WebAssembly からのリモート検証

リモート検証では、対話型 WebAssembly コンポーネントからサーバー エンドポイントにフォーム データが送信され、返されたフィールド エラーがフォームの EditContextに追加されます。 これは、ルールでプライベート サーバー データ、外部サービス、またはブラウザーで実行すべきではないその他のロジックが必要な場合に便利です。

フォームは次のとおりです。

  1. データ注釈の検証をローカルで実行します。
  2. OnValidSubmitからエンドポイントにローカルで有効な入力を送信します。
  3. フィールドキー検証エラーをサーバーから受信します。
  4. 検証コンポーネントを使用して、フォームにリモート エラーを追加します。

OnValidSubmit は、ローカル検証が成功したことを意味します。 リモート検証も成功した後にのみ、モデルを処理または保存します。

Important

プライベート検証データやビジネス ルールをブラウザーに送信しないでください。 クライアント側の検証はバイパスできるため、サーバーはすべての要求を個別に検証する必要があります。

次の使用例は、フォームが送信されたときにリモートで検証します。 フィールドごとのライブ リモート チェックの場合は、[非同期検証の 追加] の非同期フィールド検証パターンを使用します。

WebAssembly フォームがプリレンダリングされている場合は、そのクライアント側サービスもプリレンダリング中に使用できる必要があります。 使用可能な方法については、「Prerender ASP.NET Core Razor コンポーネント」を参照してください。

最小限の API を使用して検証する

ハンドラーを実行する前に、サーバー プロジェクトの AddValidation を呼び出して、サポートされているエンドポイント パラメーターを検証します。

生成された検証メタデータに使用されるMicrosoft.Extensions.Validation API は、.NET 10 では試験段階です。 詳細については、「ASP.NET Coreでの検証」を参照してください。

モデルが.Client プロジェクトで宣言されている場合は、「ASP.NET Coreの検証」の説明に従って、生成された検証メタデータを両方のプロジェクトに登録します。

エンドポイントはプライベート ビジネス ルールを追加し、モデル メンバー名でキー指定されたエラーを返します。

app.MapPost("/api/starships/validate", (StarshipModel model) =>
{
    Dictionary<string, string[]> errors = [];

    if (model.Classification == "Defense" &&
        string.IsNullOrWhiteSpace(model.Description))
    {
        errors[nameof(model.Description)] =
            ["A defense ship requires a description."];
    }

    if (errors.Count > 0)
    {
        return Results.ValidationProblem(errors);
    }

    return Results.NoContent();
});
app.MapPost("/api/starships/validate", (StarshipModel model) =>
{
    Dictionary<string, string[]> errors = [];

    if (model.Classification == "Defense" &&
        string.IsNullOrWhiteSpace(model.Description))
    {
        errors[nameof(model.Description)] =
            ["A defense ship requires a description."];
    }

    if (errors.Count > 0)
    {
        return Results.ValidationProblem(errors);
    }

    return Results.NoContent();
});

自動検証では、ハンドラーを実行する前に無効なデータ注釈が拒否されます。 ValidationProblemは、フィールドキー付きメッセージを含むerrorsプロパティを持つ400 Bad Requestを返します。 検証が成功すると、 204 No Contentが返されます。

API コントローラーを使用して検証する

ホスト Blazor WebAssembly ソリューションで、共有モデルを Shared プロジェクトに配置し、 Server プロジェクトの API コントローラーを使用して検証します。 [ApiController]属性は、アクションを実行する前に無効なデータ注釈を自動的に拒否します。

[ApiController]
[Route("api/starships/validate")]
public class StarshipValidationController : ControllerBase
{
    [HttpPost]
    public IActionResult Validate(StarshipModel model)
    {
        if (model.Classification == "Defense" &&
            string.IsNullOrWhiteSpace(model.Description))
        {
            ModelState.AddModelError(
                nameof(model.Description),
                "A defense ship requires a description.");
        }

        if (!ModelState.IsValid)
        {
            return ValidationProblem(ModelState);
        }

        return NoContent();
    }
}

サーバー プロジェクトにコントローラーを登録してマップします。 コントローラーは、検証が失敗したときにValidationProblemDetails応答と共に400 Bad Requestを返し、成功すると204 No Contentします。

エンドポイントを呼び出してエラーを表示する

WebAssembly プロジェクトの HttpClient をアプリのベース アドレスに登録します。

builder.Services.AddScoped(sp =>
    new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
builder.Services.AddScoped(sp =>
    new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });

[検証コンポーネントのビルド] のCustomValidation コンポーネントを次の形式で配置します。

@page "/"
@using System.Net
@using System.Net.Http.Json
@using BlazorWebAppRemoteValidation.Client.Models
@inject HttpClient Http

<PageTitle>Remote validation</PageTitle>

<h1>Remote validation</h1>

<p>Local validation runs before the form calls the remote endpoint. The endpoint validates the model again and applies a private business rule that requires a description for defense ships.</p>

<EditForm Model="Model" OnValidSubmit="Submit">
    <DataAnnotationsValidator />
    <CustomValidation @ref="remoteErrors" />
    <ValidationSummary />

    <p>
        <label>
            Identifier:
            <InputText id="identifier" @bind-Value="Model.Identifier" />
        </label>
        <ValidationMessage For="() => Model.Identifier" />
    </p>

    <p>
        <label>
            Classification:
            <InputSelect id="classification" @bind-Value="Model.Classification">
                <option value="">Select...</option>
                <option value="Exploration">Exploration</option>
                <option value="Defense">Defense</option>
            </InputSelect>
        </label>
        <ValidationMessage For="() => Model.Classification" />
    </p>

    <p>
        <label>
            Description:
            <InputText id="description" @bind-Value="Model.Description" />
        </label>
        <ValidationMessage For="() => Model.Description" />
    </p>

    <button id="submit" type="submit">Submit</button>
</EditForm>

@if (accepted)
{
    <p id="accepted" role="status">The server accepted the form.</p>
}

@code {
    private StarshipModel Model { get; } = new();
    private CustomValidation? remoteErrors;
    private bool accepted;

    private async Task Submit()
    {
        accepted = false;

        using var response = await Http.PostAsJsonAsync(
            "api/starships/validate", Model);

        if (response.IsSuccessStatusCode)
        {
            accepted = true;
            return;
        }

        if (response.StatusCode == HttpStatusCode.BadRequest)
        {
            var problem = await response.Content
                .ReadFromJsonAsync<ValidationProblemResponse>()
                ?? throw new InvalidOperationException(
                    "The validation response didn't contain a response body.");

            remoteErrors!.DisplayErrors(problem.Errors);
            return;
        }

        response.EnsureSuccessStatusCode();
    }

    private sealed record ValidationProblemResponse(
        Dictionary<string, string[]> Errors);
}
@using System.Net
@using System.Net.Http.Json
@inject HttpClient Http

<EditForm Model="Model" OnValidSubmit="Submit">
    <DataAnnotationsValidator />
    <CustomValidation @ref="remoteErrors" />
    <ValidationSummary />

    ...
</EditForm>

@code {
    private StarshipModel Model { get; } = new StarshipModel();
    private CustomValidation? remoteErrors;

    private async Task Submit()
    {
        using var response = await Http.PostAsJsonAsync(
            "api/starships/validate", Model);

        if (response.IsSuccessStatusCode)
        {
            // Process or save the model.
            return;
        }

        if (response.StatusCode == HttpStatusCode.BadRequest)
        {
            var problem = await response.Content
                .ReadFromJsonAsync<ValidationProblemResponse>();

            if (problem is not null)
            {
                remoteErrors!.DisplayErrors(problem.Errors);
            }

            return;
        }

        response.EnsureSuccessStatusCode();
    }

    private sealed class ValidationProblemResponse
    {
        public Dictionary<string, string[]> Errors { get; set; } =
            new Dictionary<string, string[]>();
    }
}

検証コントロール コンポーネントは、そのフィールドが変更されたときにリモート フィールド エラーをクリアするため、ユーザーは値を修正して再度送信できます。 アプリケーションのセキュリティ要件に従ってエンドポイントを保護する。認証と承認は、この検証例の範囲外です。

完全なリモート検証サンプルには、ホスト エンドポイント、共有モデル、アセンブリ間検証登録、検証コンポーネント、対話型 WebAssembly フォームが含まれます。

.NET 10 のリモート検証サンプルでは、認証とサーバー側プロキシを使用する Interactive Auto アプリの同じ検証フローを示します。

ASP.NET Core のフォーム検証Blazor