หมายเหตุ
การเข้าถึงหน้านี้ต้องได้รับการอนุญาต คุณสามารถลอง ลงชื่อเข้าใช้หรือเปลี่ยนไดเรกทอรีได้
การเข้าถึงหน้านี้ต้องได้รับการอนุญาต คุณสามารถลองเปลี่ยนไดเรกทอรีได้
Note
This isn't the latest version of this article. For the current release, see the .NET 10 version of this article.
Warning
This version of ASP.NET Core is no longer supported. For more information, see the .NET and .NET Core Support Policy. For the current release, see the .NET 10 version of this article.
This article demonstrates reusable validator components and remote validation. For common form validation APIs, including data annotations, direct EditContext validation, message display, styling, state, and submit behavior, see ASP.NET Core Blazor forms validation.
For model-based validation rules shared by Blazor and Minimal APIs, see Validation in ASP.NET Core.
For browser validation in static server-side rendering (static SSR), see ASP.NET Core Blazor client-side form validation in static SSR.
Build a validator component
A validator component encapsulates validation that uses a form's EditContext and ValidationMessageStore. This is useful when the same validation behavior is used by several forms or when errors arrive from a service rather than from validation attributes on the model.
The component:
- Receives the form's
EditContextas a cascading parameter. - Creates a message store for its errors.
- Clears stale form errors when validation is requested.
- Clears a field's stale errors when the field changes.
- Exposes methods for displaying and clearing errors.
- Unsubscribes its event handlers when disposed.
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;
}
}
}
Place the component inside an EditForm and capture a component reference when the form or a service should display errors:
<EditForm Model="Model" OnValidSubmit="Submit">
<DataAnnotationsValidator />
<CustomValidation @ref="customValidation" />
<ValidationSummary />
...
</EditForm>
@code {
private CustomValidation? customValidation;
}
The component can be used alongside DataAnnotationsValidator. Each validator has its own message store associated with the same EditContext, and ValidationMessage and ValidationSummary display messages from both validators.
To implement a business rule inside the validator component instead of accepting external errors, run the rule from HandleValidationRequested or HandleFieldChanged and add its messages to messages. For a smaller example that performs this directly in a form component, see ASP.NET Core Blazor forms validation.
Add asynchronous validation
The same component pattern supports asynchronous work:
- In an
OnValidationRequestedhandler, calle.AddAsyncValidatorto register form-level work.EditFormawaits it before invokingOnValidSubmitorOnInvalidSubmit. - In an
OnFieldChangedhandler, callEditContext.RegisterAsyncFieldValidatorto start validation for that field. Starting another validation for the same field supersedes and cancels the previous operation.
For form-level asynchronous validation:
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();
}
For field-level asynchronous validation:
private void HandleFieldChanged(
object? sender, FieldChangedEventArgs e)
{
CurrentEditContext!.RegisterAsyncFieldValidator(
e.FieldIdentifier,
token => ValidateFieldAsync(e.FieldIdentifier, token));
}
Pass the supplied cancellation token to I/O. Clear prior messages before starting the operation, avoid publishing partial results after an exception, and call NotifyValidationStateChanged after updating messages.
An operation canceled because it was superseded or because the validation pass was canceled is discarded. Other exceptions place the field or form in the faulted state. For displaying pending and faulted state, see ASP.NET Core Blazor forms validation.
For a complete component that combines form-level and per-field asynchronous validation, see the following sample:
@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;
}
}
}
For asynchronous validation attributes on the model, see Validation in ASP.NET Core.
Validator component code runs where the component runs. In Interactive WebAssembly, it runs in the browser, while it runs on the server over the circuit in Interactive Server.
In static SSR, validator component code runs on the server during the form post and doesn't provide live .NET field validation between requests.
Remote validation from Interactive WebAssembly
Remote validation sends form data from an Interactive WebAssembly component to a server endpoint and adds returned field errors to the form's EditContext. It's useful when a rule requires private server data, an external service, or other logic that shouldn't run in the browser.
The form:
- Runs data annotations validation locally.
- Sends locally valid input to the endpoint from
OnValidSubmit. - Receives field-keyed validation errors from the server.
- Adds remote errors to the form through the validator component.
OnValidSubmit only means that local validation succeeded. Process or save the model only after remote validation also succeeds.
Important
Don't send private validation data or business rules to the browser. The server must validate every request independently because client-side validation can be bypassed.
This example validates remotely when the form is submitted. For live per-field remote checks, use the asynchronous field-validation pattern from Add asynchronous validation.
If the WebAssembly form is prerendered, its client-side services must also be available during prerendering. For the available approaches, see Prerender ASP.NET Core Razor components.
Validate with a Minimal API
Call AddValidation in the server project to validate supported endpoint parameters before the handler runs.
The Microsoft.Extensions.Validation APIs used for generated validation metadata are experimental in .NET 10. For details, see Validation in ASP.NET Core.
If the model is declared in the .Client project, register its generated validation metadata in both projects as described in Validation in ASP.NET Core.
The endpoint adds a private business rule and returns errors keyed by model member name:
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();
});
Automatic validation rejects invalid data annotations before the handler runs. ValidationProblem returns 400 Bad Request with an errors property containing field-keyed messages. Successful validation returns 204 No Content.
Validate with an API controller
In a hosted Blazor WebAssembly solution, place the shared model in the Shared project and validate it with an API controller in the Server project. The [ApiController] attribute automatically rejects invalid data annotations before the action runs.
[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();
}
}
Register and map controllers in the server project. The controller returns 400 Bad Request with a ValidationProblemDetails response when validation fails and 204 No Content when it succeeds.
Call the endpoint and display errors
Register an HttpClient in the WebAssembly project with the app's base address:
builder.Services.AddScoped(sp =>
new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
builder.Services.AddScoped(sp =>
new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
Place the CustomValidation component from Build a validator component in the form:
@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[]>();
}
}
The validator component clears a remote field error when that field changes, so the user can correct the value and submit again. Protect the endpoint according to the application's security requirements; authentication and authorization are outside the scope of this validation example.
The complete remote-validation sample includes the host endpoint, shared model, cross-assembly validation registration, validator component, and Interactive WebAssembly form.
The .NET 10 remote-validation sample demonstrates the same validation flow in an Interactive Auto app with authentication and a server-side proxy.
Additional resources
ASP.NET Core