diff --git a/aspnetcore/blazor/components/component-disposal.md b/aspnetcore/blazor/components/component-disposal.md
index a54ad0967ed3..4ca51cfce557 100644
--- a/aspnetcore/blazor/components/component-disposal.md
+++ b/aspnetcore/blazor/components/component-disposal.md
@@ -331,7 +331,7 @@ protected override void OnInitialized()
}
```
-The full example of the preceding code with anonymous lambda expressions appears in the article.
+The full example of the preceding code with anonymous lambda expressions appears in the article.
For more information, see [Cleaning up unmanaged resources](/dotnet/standard/garbage-collection/unmanaged) and the topics that follow it on implementing the `Dispose` and `DisposeAsync` methods.
diff --git a/aspnetcore/blazor/forms/binding.md b/aspnetcore/blazor/forms/binding.md
index 9e10329da8a7..3c0bab64f5c2 100644
--- a/aspnetcore/blazor/forms/binding.md
+++ b/aspnetcore/blazor/forms/binding.md
@@ -5,7 +5,7 @@ author: guardrex
description: Learn how to use binding in Blazor forms.
monikerRange: '>= aspnetcore-3.1'
ms.author: wpickett
-ms.date: 08/24/2026
+ms.date: 09/22/2026
uid: blazor/forms/binding
---
# ASP.NET Core Blazor forms binding
@@ -403,6 +403,71 @@ Developers aren't expected to interact with component to create a custom component that uses the `oninput` event ([`input`](https://developer.mozilla.org/docs/Web/API/HTMLElement/input_event)) instead of the `onchange` event ([`change`](https://developer.mozilla.org/docs/Web/API/HTMLElement/change_event)). Use of the `input` event triggers field validation on each keystroke.
+
+The following `CustomInputText` component inherits the framework's `InputText` component and sets event binding to the `oninput` event ([`input`](https://developer.mozilla.org/docs/Web/API/HTMLElement/input_event)).
+
+`CustomInputText.razor`:
+
+:::code language="razor" source="~/../blazor-samples/8.0/BlazorSample_BlazorWebApp/Components/CustomInputText.razor":::
+
+The `CustomInputText` component can be used anywhere is used. The following component uses the shared `CustomInputText` component.
+
+`Starship11.razor`:
+
+:::moniker range=">= aspnetcore-9.0"
+
+:::code language="razor" source="~/../blazor-samples/9.0/BlazorSample_BlazorWebApp/Components/Pages/Starship11.razor":::
+
+:::moniker-end
+
+:::moniker range=">= aspnetcore-8.0 < aspnetcore-9.0"
+
+:::code language="razor" source="~/../blazor-samples/8.0/BlazorSample_BlazorWebApp/Components/Pages/Starship11.razor":::
+
+:::moniker-end
+
+:::moniker range="< aspnetcore-8.0"
+
+```razor
+@page "/starship-11"
+@using System.ComponentModel.DataAnnotations
+@inject ILogger Logger
+
+
+
+
+
+
+
+
+
+ CurrentValue: @Model?.Id
+
+
+@code {
+ public Starship? Model { get; set; }
+
+ protected override void OnInitialized() => Model ??= new();
+
+ private void Submit()
+ {
+ Logger.LogInformation("Submit called: Processing the form");
+ }
+
+ public class Starship
+ {
+ [Required]
+ [StringLength(10, ErrorMessage = "Id is too long.")]
+ public string? Id { get; set; }
+ }
+}
+```
+
+:::moniker-end
+
## Custom input components
For custom input processing scenarios, the following subsections demonstrate custom input components:
diff --git a/aspnetcore/blazor/forms/index.md b/aspnetcore/blazor/forms/index.md
index 0859df0e93e9..ce9960105126 100644
--- a/aspnetcore/blazor/forms/index.md
+++ b/aspnetcore/blazor/forms/index.md
@@ -4,7 +4,7 @@ author: guardrex
description: Learn how to use forms in Blazor.
monikerRange: '>= aspnetcore-3.1'
ms.author: wpickett
-ms.date: 11/11/2025
+ms.date: 09/22/2026
uid: blazor/forms/index
---
# ASP.NET Core Blazor forms overview
@@ -35,7 +35,7 @@ The Blazor framework supports forms and provides built-in input components:
:::moniker range=">= aspnetcore-11.0"
-In Blazor Web Apps that use static server-side rendering (static SSR), input components automatically participate in client-side validation when the form contains a component. For details, see .
+In Blazor Web Apps that use static server-side rendering (static SSR), input components automatically participate in client-side validation when the form contains a component. For details, see .
:::moniker-end
@@ -187,7 +187,7 @@ In the next example, the preceding component is modified to create the form in t
* If the `` form field contains more than ten characters when the **`Submit`** button is selected, an error appears in the validation summary ("`Id is too long.`"). `Submit` is **not** called.
* If the `` form field contains a valid value when the **`Submit`** button is selected, `Submit` is called.
-†The component is covered in the [Validator component](xref:blazor/forms/validation#validator-components) section. ‡The component is covered in the [Validation Summary and Validation Message components](xref:blazor/forms/validation#validation-summary-and-validation-message-components) section.
+†The component is covered in the [Data Annotations Validator component and custom validation](xref:blazor/forms/validation#data-annotations-validator-component-and-custom-validation) section. ‡The component is covered in the [Validation Summary and Validation Message components](xref:blazor/forms/validation#validation-summary-and-validation-message-components) section.
`Starship2.razor`:
@@ -488,7 +488,7 @@ In Blazor Web Apps, client-side validation requires an active Blazor SignalR cir
## Client-side validation in static SSR forms
-In Blazor Web Apps, forms in components that adopt static server-side rendering (static SSR) gain client-side validation automatically when a component is present in the form. For details, see .
+In Blazor Web Apps, forms in components that adopt static server-side rendering (static SSR) gain client-side validation automatically when a component is present in the form. For details, see .
:::moniker-end
@@ -502,7 +502,7 @@ jQuery validation isn't supported in Razor components. We recommend any of the f
* Follow the guidance in for any of the following scenarios:
* Server-side validation in a Blazor Web App that adopts an interactive render mode.
- * Client-side validation in [static SSR forms](xref:blazor/forms/validation#client-side-validation-in-static-ssr-forms).
+ * Client-side validation in [static SSR forms](xref:blazor/forms/validation-client-side).
* Client-side validation in a standalone Blazor WebAssembly app.
* Use native HTML validation attributes (see [Client-side form validation](https://developer.mozilla.org/docs/Learn/Forms/Form_validation)).
* Adopt a third-party validation JavaScript library.
diff --git a/aspnetcore/blazor/forms/validation-advanced.md b/aspnetcore/blazor/forms/validation-advanced.md
new file mode 100644
index 000000000000..049a667e650b
--- /dev/null
+++ b/aspnetcore/blazor/forms/validation-advanced.md
@@ -0,0 +1,447 @@
+---
+title: ASP.NET Core Blazor advanced form validation
+ai-usage: ai-assisted
+author: guardrex
+description: Learn how to implement validator components and remote validation for Blazor forms.
+monikerRange: '>= aspnetcore-3.1'
+ms.author: wpickett
+ms.date: 09/22/2026
+uid: blazor/forms/validation-advanced
+---
+# ASP.NET Core Blazor advanced form validation
+
+[!INCLUDE[](~/includes/not-latest-version.md)]
+
+This article demonstrates reusable validator components and remote validation. For common form validation APIs, including data annotations, direct validation, message display, styling, state, and submit behavior, see .
+
+:::moniker range=">= aspnetcore-10.0"
+
+For model-based validation rules shared by Blazor and Minimal APIs, see .
+
+:::moniker-end
+
+:::moniker range=">= aspnetcore-11.0"
+
+For browser validation in static server-side rendering (static SSR), see .
+
+:::moniker-end
+
+
+
+
+
+
+## Build a validator component
+
+A validator component encapsulates validation that uses a form's `EditContext` and . 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 `EditContext` as 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`:
+
+```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 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:
+
+```razor
+
+
+
+
+
+ ...
+
+
+@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 .
+
+:::moniker range=">= aspnetcore-11.0"
+
+
+
+### Add asynchronous validation
+
+The same component pattern supports asynchronous work:
+
+* In an `OnValidationRequested` handler, call `e.AddAsyncValidator` to register form-level work. `EditForm` awaits it before invoking `OnValidSubmit` or `OnInvalidSubmit`.
+* In an `OnFieldChanged` handler, call to start validation for that field. Starting another validation for the same field supersedes and cancels the previous operation.
+
+For form-level asynchronous validation:
+
+```csharp
+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(
+ $"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:
+
+```csharp
+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 .
+
+For a complete component that combines form-level and per-field asynchronous validation, see the following sample:
+
+:::code language="razor" source="~/../blazor-samples/11.0/BlazorSample_BlazorWebApp/Components/UsernameUniquenessValidator.razor":::
+
+For asynchronous validation attributes on the model, see .
+
+:::moniker-end
+
+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.
+
+:::moniker range=">= aspnetcore-8.0"
+
+In static SSR, validator component code runs on the server during the form post and doesn't provide live .NET field validation between requests.
+
+:::moniker-end
+
+
+
+
+## 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:
+
+1. Runs data annotations validation locally.
+1. Sends locally valid input to the endpoint from `OnValidSubmit`.
+1. Receives field-keyed validation errors from the server.
+1. 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.
+
+:::moniker range=">= aspnetcore-11.0"
+
+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](#add-asynchronous-validation).
+
+:::moniker-end
+
+:::moniker range=">= aspnetcore-8.0"
+
+If the WebAssembly form is prerendered, its client-side services must also be available during prerendering. For the available approaches, see .
+
+:::moniker-end
+
+:::moniker range=">= aspnetcore-10.0"
+
+### Validate with a Minimal API
+
+Call in the server project to validate supported endpoint parameters before the handler runs.
+
+:::moniker-end
+
+:::moniker range="= aspnetcore-10.0"
+
+The APIs used for generated validation metadata are experimental in .NET 10. For details, see .
+
+:::moniker-end
+
+:::moniker range=">= aspnetcore-10.0"
+
+If the model is declared in the `.Client` project, register its generated validation metadata in both projects as described in .
+
+The endpoint adds a private business rule and returns errors keyed by model member name:
+
+:::moniker-end
+
+:::moniker range=">= aspnetcore-11.0"
+
+:::code language="csharp" source="~/../blazor-samples/11.0/BlazorWebAppRemoteValidation/BlazorWebAppRemoteValidation/Program.cs" id="snippet_ValidationEndpoint":::
+
+:::moniker-end
+
+:::moniker range="= aspnetcore-10.0"
+
+```csharp
+app.MapPost("/api/starships/validate", (StarshipModel model) =>
+{
+ Dictionary 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();
+});
+```
+
+:::moniker-end
+
+:::moniker range=">= aspnetcore-10.0"
+
+Automatic validation rejects invalid data annotations before the handler runs. returns `400 Bad Request` with an `errors` property containing field-keyed messages. Successful validation returns `204 No Content`.
+
+:::moniker-end
+
+:::moniker range="< aspnetcore-10.0"
+
+### 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.
+
+```csharp
+[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.
+
+:::moniker-end
+
+### Call the endpoint and display errors
+
+Register an `HttpClient` in the WebAssembly project with the app's base address:
+
+:::moniker range=">= aspnetcore-11.0"
+
+:::code language="csharp" source="~/../blazor-samples/11.0/BlazorWebAppRemoteValidation/BlazorWebAppRemoteValidation.Client/Program.cs" id="snippet_HttpClient":::
+
+:::moniker-end
+
+:::moniker range="< aspnetcore-11.0"
+
+```csharp
+builder.Services.AddScoped(sp =>
+ new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
+```
+
+:::moniker-end
+
+Place the `CustomValidation` component from [Build a validator component](#build-a-validator-component) in the form:
+
+:::moniker range=">= aspnetcore-11.0"
+
+:::code language="razor" source="~/../blazor-samples/11.0/BlazorWebAppRemoteValidation/BlazorWebAppRemoteValidation.Client/Pages/Home.razor":::
+
+:::moniker-end
+
+:::moniker range="< aspnetcore-11.0"
+
+```razor
+@using System.Net
+@using System.Net.Http.Json
+@inject HttpClient Http
+
+
+
+
+
+
+ ...
+
+
+@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();
+
+ if (problem is not null)
+ {
+ remoteErrors!.DisplayErrors(problem.Errors);
+ }
+
+ return;
+ }
+
+ response.EnsureSuccessStatusCode();
+ }
+
+ private sealed class ValidationProblemResponse
+ {
+ public Dictionary Errors { get; set; } =
+ new Dictionary();
+ }
+}
+```
+
+:::moniker-end
+
+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.
+
+:::moniker range=">= aspnetcore-11.0"
+
+The [complete remote-validation sample](https://github.com/dotnet/blazor-samples/tree/main/11.0/BlazorWebAppRemoteValidation) includes the host endpoint, shared model, cross-assembly validation registration, validator component, and Interactive WebAssembly form.
+
+:::moniker-end
+
+:::moniker range="= aspnetcore-10.0"
+
+The [.NET 10 remote-validation sample](https://github.com/dotnet/blazor-samples/tree/main/10.0/BlazorWebAppRemoteValidation) demonstrates the same validation flow in an Interactive Auto app with authentication and a server-side proxy.
+
+:::moniker-end
+
+## Additional resources
+
+:::moniker range=">= aspnetcore-11.0"
+
+*
+*
+*
+*
+
+:::moniker-end
+
+:::moniker range=">= aspnetcore-10.0 < aspnetcore-11.0"
+
+*
+*
+*
+
+:::moniker-end
+
+:::moniker range="< aspnetcore-10.0"
+
+
+
+:::moniker-end
diff --git a/aspnetcore/blazor/forms/validation-client-side.md b/aspnetcore/blazor/forms/validation-client-side.md
new file mode 100644
index 000000000000..3ae51b5726cc
--- /dev/null
+++ b/aspnetcore/blazor/forms/validation-client-side.md
@@ -0,0 +1,208 @@
+---
+title: ASP.NET Core Blazor client-side form validation in static SSR
+ai-usage: ai-assisted
+author: guardrex
+description: Learn how Blazor validates static server-side rendered forms in the browser before they're submitted.
+monikerRange: '>= aspnetcore-11.0'
+ms.author: wpickett
+ms.date: 09/22/2026
+uid: blazor/forms/validation-client-side
+---
+# ASP.NET Core Blazor client-side form validation in static SSR
+
+
+
+This article explains how Blazor adds live client-side validation to forms that use [static server-side rendering (static SSR)](xref:blazor/components/render-modes#static-server-side-rendering-static-ssr). The browser validates individual fields as the user edits them and validates the full form before it's submitted. If the client-side check passes, the form is submitted and validated again on the server.
+
+Forms that use an interactive render mode don't use the static SSR client-side validation feature described in this article. Both their per-field validation and full-form submit validation run in .NET through the form's .
+
+## How client-side validation works
+
+When a static SSR form contains a component, Blazor renders the form's validation rules into the page and enforces them using JavaScript before the form is submitted. The user sees validation errors without a round trip to the server.
+
+Client-side validation activates automatically when the following conditions are met:
+
+* The form's hosting component uses static SSR (no `@rendermode` directive applied to the component).
+* The form contains a component.
+* The form's model uses validation attributes.
+
+For the built-in set of validation attributes, no JavaScript configuration, additional package, or service registration is required. The section [Custom client-side validation rules](#custom-client-side-validation-rules) describes how to add support for custom validation attributes.
+
+> [!IMPORTANT]
+> Client-side validation is a user experience improvement, not an authoritative validation pass. It can be bypassed by disabling or modifying the browser's JavaScript execution. Server-side validation runs after the form is posted and remains authoritative. Never rely on client-side validation to protect data integrity.
+
+## Supported validation attributes
+
+The following attributes are enforced client-side, matching the server-side data annotations behavior:
+
+*
+*
+*
+*
+* (only when the operand type is numeric)
+*
+*
+*
+*
+*
+*
+*
+
+Validation attributes that don't appear in this list, including custom -derived attributes, aren't enforced client-side by default. They continue to run server-side after the form is submitted. To supply a client-side rule for a custom attribute, see the [Custom client-side validation rules](#custom-client-side-validation-rules) section.
+
+## Validation timing
+
+A field is validated when its value is committed. For text inputs (`` elements), this occurs when the field loses focus. Checkboxes and dropdown lists are validated immediately after selection.
+
+After a field has shown a validation error or after the form has been submitted at least once, the field is validated again on every keystroke so that corrections are reflected immediately.
+
+Submitting the form validates every tracked field. If any field is invalid, the submission is blocked and focus moves to the first invalid field.
+
+## Validation messages, localization, and accessibility
+
+Client-side validation uses to display messages for individual fields and to display messages for the whole form, which is the same way that interactive validation reports messages to users.
+
+When validation localization is configured, error messages are localized on the server as the page is rendered, so client-side validation displays the same localized strings as the server-side experience. Localization requires . For more information, see .
+
+ARIA attributes on input elements and validation message containers are managed by Blazor automatically, so assistive technologies announce validation errors without additional configuration.
+
+## Validation state CSS classes
+
+The client-side validation engine applies the same CSS classes as Blazor's interactive validation, which are shown in the following table.
+
+Element | Classes
+--- | ---
+Input | `valid` or `invalid`, plus `modified` once the user edits the field
+Validation message | `validation-message`
+Validation summary | `validation-summary-errors` or `validation-summary-valid`
+
+Client-side validation also calls the browser's [Constraint Validation API](https://developer.mozilla.org/docs/Web/API/Constraint_validation), so the standard CSS pseudo-classes `:valid` and `:invalid` reflect each input's current validation state.
+
+## Opt out of client-side validation
+
+The feature can be disabled at multiple levels. Server-side validation is unaffected by any of the options in this section.
+
+### Opt out for a single form
+
+Set the component's `DisableClientValidation` parameter to `true`:
+
+```razor
+
+```
+
+### Opt out for the entire app
+
+Set `DisableClientValidation` on when Razor components services are registered in the `Program` file:
+
+```csharp
+builder.Services.AddRazorComponents(options =>
+{
+ options.DisableClientValidation = true;
+});
+```
+
+The global option takes precedence. When it's set to `true`, forms don't emit client-side validation rules.
+
+### Opt out for a single submit button
+
+Use the standard HTML [`formnovalidate` attribute](https://developer.mozilla.org/docs/Web/API/HTMLInputElement/formNoValidate) on the button. The form is posted without a client-side check, and server-side validation still runs after the post:
+
+```razor
+
+```
+
+This can be used to implement a "save draft" or "back" button that don't require a completely valid form for the submit to succeed.
+
+## Custom client-side validation rules
+
+Custom validation attributes continue to run on the server but don't have a client-side implementation by default. Enforcing a custom rule in the browser involves two steps: emitting the rule from the .NET attribute and registering a matching JavaScript validator. The server-side implementation remains authoritative.
+
+### Emit the rule from .NET
+
+Implement `IClientValidationRuleProvider` on the validation attribute and return one or more `ClientValidationRule` instances. The rule's `Name` identifies the client-side validator, and `Parameters` supplies values the validator needs.
+
+The following `StartsWithAttribute` validates server-side in `IsValid` and contributes a `startswith` client-side rule with a `prefix` parameter:
+
+:::code language="csharp" source="~/../blazor-samples/11.0/BlazorSample_BlazorWebApp/Validation/StartsWithAttribute.cs":::
+
+Apply the attribute to the model in the usual way:
+
+:::code language="csharp" source="~/../blazor-samples/11.0/BlazorSample_BlazorWebApp/Validation/ShipModel.cs":::
+
+### Register the JavaScript validator
+
+In JavaScript, call `Blazor.formValidation.addValidator(name, validator)` to associate a rule name with a validator function. The `name` must exactly match `ClientValidationRule.Name`, including casing. Registrations are app-wide, and registering the same name again replaces the previous validator.
+
+> [!WARNING]
+> If no JavaScript validator is registered for an emitted rule name, the rule is skipped in the browser. Server-side validation still runs when the form is posted.
+
+Register custom validators once from app startup code. Choose the registration location based on how Blazor starts, as described in the following table.
+
+Blazor startup | Registration location
+--- | ---
+Automatic startup (default) | A script immediately after `blazor.web.js`
+Manual `Blazor.start()` | The continuation returned by `Blazor.start()`
+Either startup style | A JavaScript initializer's `afterWebStarted` callback
+
+A [JavaScript initializer](xref:blazor/fundamentals/startup#javascript-initializers) works with either startup style. In a file named `{ASSEMBLY NAME}.lib.module.js` in the app's `wwwroot` folder:
+
+:::code language="javascript" source="~/../blazor-samples/11.0/BlazorSample_BlazorWebApp/wwwroot/BlazorSample.lib.module.js":::
+
+With automatic startup, an app-specific validator script can instead be loaded immediately after `blazor.web.js`:
+
+```razor
+
+
+```
+
+The second script can call `Blazor.formValidation.addValidator` directly.
+
+With manual startup, define the registration in an app script:
+
+:::code language="javascript" source="~/../blazor-samples/11.0/BlazorSample_BlazorWebApp/wwwroot/js/custom-validation.js":::
+
+Load the scripts with automatic startup disabled, and register the validators after `Blazor.start()` completes:
+
+:::code language="razor" source="~/../blazor-samples/11.0/BlazorSample_BlazorWebApp/Components/AppManualStartup.razor":::
+
+> [!IMPORTANT]
+> Register validators from app startup code, not from a page or form component. A component script can run before Blazor starts, and scripts added by enhanced navigation aren't executed. Static SSR components also can't use `IJSRuntime` because they don't have an interactive .NET runtime.
+
+### Write JavaScript validator functions
+
+The validator receives a context object with the following members, as shown in the following table.
+
+Member | Description
+--- | ---
+`value` | The field's current value as a string, or `null`/`undefined` when there's no value.
+`element` | The validated `input`, `select`, or `textarea` element.
+`params` | The rule's `Parameters` as a string dictionary.
+
+The validator is expected to return `{ success: true }` when the value is valid. Return `{ success: false }` to use the rule's server-supplied message, or you can return `{ success: false, message: '...' }` to override the message for that call.
+
+Empty values should normally be treated as valid by rules other than `required`, allowing an optional field to remain empty while still validating values that are supplied.
+
+Implement the same rule semantics in .NET and JavaScript, including case sensitivity, normalization, and empty-value handling. If the implementations differ, the browser and the authoritative server-side validation can produce different results.
+
+## Validate form on demand
+
+The `Blazor.formValidation` API also exposes JavaScript methods for validating on demand, as the following table shows.
+
+Method | Description
+--- | ---
+`validateField(element)` | Validates a single field element and updates its error display. Returns `true` when valid.
+`validateForm(form)` | Validates every tracked field in a form. Returns `true` when all fields are valid.
+
+## Limitations
+
+* Client-side rules are emitted only for fields included in server-side validation as well. Without , only top-level model properties are validated. Validating nested objects and collections requires the app to call and the model to be discovered. For more information, see . Note that this limitation is an intentional feature to help prevent bugs where the authoritative server validation would be missing due to misconfiguration.
+* Inputs added to an existing form by a later [streaming rendering](xref:blazor/components/rendering#streaming-rendering) update don't receive client-side validation. A form delivered in a single streamed batch is covered normally.
+* Only the attributes listed in [Supported validation attributes](#supported-validation-attributes) have built-in client-side implementations. For example, a with a non-numeric operand type is only enforced on the server. Other attributes require a [custom client-side validation rule](#custom-client-side-validation-rules).
+* Custom JavaScript validators are synchronous. Rules that require a network call or other asynchronous work must run on the server or use asynchronous validation with an interactive render mode. For more information, see .
+
+## Additional resources
+
+*
+*
+*
+*
diff --git a/aspnetcore/blazor/forms/validation.md b/aspnetcore/blazor/forms/validation.md
index f1c8bc3fdc6c..9fcb3b289c72 100644
--- a/aspnetcore/blazor/forms/validation.md
+++ b/aspnetcore/blazor/forms/validation.md
@@ -5,2764 +5,628 @@ author: guardrex
description: Learn how to use validation in Blazor forms.
monikerRange: '>= aspnetcore-3.1'
ms.author: wpickett
-ms.date: 08/14/2026
+ms.date: 09/22/2026
uid: blazor/forms/validation
---
# ASP.NET Core Blazor forms validation
[!INCLUDE[](~/includes/not-latest-version.md)]
-This article explains how to use validation in Blazor forms.
+This article explains how to validate user input in Blazor forms.
-:::moniker range=">= aspnetcore-10.0"
-
-For an overview of validation, including how to register services for Minimal API projects, see .
+For most forms, the simplest and recommended approach is to add [data annotations validation attributes](xref:System.ComponentModel.DataAnnotations) to the model and place a component in the . Blazor also supports custom validation through the form's , either directly in the form component or in a reusable validator component.
-:::moniker-end
+Related articles provide more detail:
-## Form validation
+:::moniker range=">= aspnetcore-11.0"
-In basic form validation scenarios, an instance can use declared and instances to validate form fields. A handler for the event of the executes custom validation logic. The handler's result updates the instance.
+* For writing and configuring model-based validation rules, including custom and asynchronous rules, nested object validation, and localization, see .
+* For live browser validation in static server-side rendering (static SSR), see .
+* For complete validator-component and remote-validation implementations, see .
-Basic form validation is useful in cases where the form's model is defined within the component hosting the form, either as members directly on the component or in a subclass. Use of a [validator component](#validator-components) is recommended where an independent model class is used across several components.
+:::moniker-end
-:::moniker range=">= aspnetcore-8.0 < aspnetcore-11.0"
+:::moniker range=">= aspnetcore-10.0 < aspnetcore-11.0"
-In Blazor Web Apps, client-side validation requires an active Blazor SignalR circuit. Client-side validation isn't available to forms in components that have adopted static server-side rendering (static SSR). Forms that adopt static SSR are validated on the server after the form is submitted.
+* For writing and configuring model-based validation rules and nested object validation, see .
+* For complete validator-component and remote-validation implementations, see .
:::moniker-end
-:::moniker range=">= aspnetcore-11.0"
+:::moniker range="< aspnetcore-10.0"
-In Blazor Web Apps that use interactive render modes (Server, WebAssembly, or Auto), client-side validation runs through the live pipeline as in earlier releases. Forms that adopt static server-side rendering (static SSR) gain client-side validation automatically when a component is present in the form. For details, see .
+* For complete validator-component and remote-validation implementations, see .
:::moniker-end
-In the following component, the `HandleValidationRequested` handler method clears any existing validation messages by calling before validating the form.
+
-`Starship8.razor`:
+## Validate with data annotations
-:::moniker range=">= aspnetcore-9.0"
+The following model uses and :
-:::code language="razor" source="~/../blazor-samples/9.0/BlazorSample_BlazorWebApp/Components/Pages/Starship8.razor":::
-
-:::moniker-end
+`Starship.cs`:
-:::moniker range=">= aspnetcore-8.0 < aspnetcore-9.0"
+```csharp
+using System.ComponentModel.DataAnnotations;
-:::code language="razor" source="~/../blazor-samples/8.0/BlazorSample_BlazorWebApp/Components/Pages/Starship8.razor":::
+public class Starship
+{
+ [Required]
+ public string? Identifier { get; set; }
-:::moniker-end
+ [Range(1, 10, ErrorMessage = "Accommodation must be between 1 and 10.")]
+ public int MaximumAccommodation { get; set; }
+}
+```
-:::moniker range="< aspnetcore-8.0"
+Add the model to an `EditForm`, include `DataAnnotationsValidator`, and display errors with or . The callback is invoked only when validation succeeds:
```razor
-@page "/starship-8"
-@implements IDisposable
-@inject ILogger Logger
-
-
Holodeck Configuration
+
+
+
-
-
+
-
-
+
+
+
+
-
-
-
-
-
-
-
+
+
+
+
@code {
- private EditContext? editContext;
-
- public Holodeck? Model { get; set; }
-
- private ValidationMessageStore? messageStore;
-
- protected override void OnInitialized()
- {
- Model ??= new();
- editContext = new(Model);
- editContext.OnValidationRequested += HandleValidationRequested;
- messageStore = new(editContext);
- }
-
- private void HandleValidationRequested(object? sender,
- ValidationRequestedEventArgs args)
- {
- messageStore?.Clear();
-
- // Custom validation logic
- if (!Model!.Options)
- {
- messageStore?.Add(() => Model.Options, "Select at least one.");
- }
- }
+ private Starship Model { get; } = new Starship();
private void Submit()
{
- Logger.LogInformation("Submit called: Processing the form");
- }
-
- public class Holodeck
- {
- public bool Subsystem1 { get; set; }
- public bool Subsystem2 { get; set; }
- public bool Options => Subsystem1 || Subsystem2;
- }
-
- public void Dispose()
- {
- if (editContext is not null)
- {
- editContext.OnValidationRequested -= HandleValidationRequested;
- }
+ // Process the valid form.
}
}
```
-
-
-:::moniker-end
+:::moniker range=">= aspnetcore-8.0"
-## Data Annotations Validator component and custom validation
+For a static SSR form post, assign a unique `FormName` and receive the posted model with `[SupplyParameterFromForm]`:
-The component attaches data annotations validation to a cascaded . Enabling data annotations validation requires the component. To use a different validation system than data annotations, use a custom implementation instead of the component. The framework implementations for are available for inspection in the reference source:
+```razor
+
+ ...
+
-* [`DataAnnotationsValidator`](https://github.com/dotnet/AspNetCore/blob/main/src/Components/Forms/src/DataAnnotationsValidator.cs)
-* [`EnableDataAnnotationsValidation`](https://github.com/dotnet/AspNetCore/blob/main/src/Components/Forms/src/EditContextDataAnnotationsExtensions.cs)
+@code {
+ [SupplyParameterFromForm]
+ private Starship? Model { get; set; }
-:::moniker range=">= aspnetcore-10.0"
+ protected override void OnInitialized() => Model ??= new();
+}
+```
-For details on validation behavior, see the [`DataAnnotationsValidator` validation behavior](#dataannotationsvalidator-validation-behavior) section.
+For more information about form submission and model binding across render modes, see and .
:::moniker-end
-If you need to enable data annotations validation support for an in code, call with an injected (`@inject IServiceProvider ServiceProvider`) on the . For an advanced example, see the [`NotifyPropertyChangedValidationComponent` component in the ASP.NET Core Blazor framework's `BasicTestApp` (`dotnet/aspnetcore` GitHub repository)](https://github.com/dotnet/aspnetcore/blob/main/src/Components/test/testassets/BasicTestApp/FormsTest/NotifyPropertyChangedValidationComponent.razor). In a production version of the example, replace the `new TestServiceProvider()` argument for the service provider with an injected .
-
-[!INCLUDE[](~/includes/aspnetcore-repo-ref-source-links.md)]
-
-Blazor performs two types of validation:
-
-* *Field validation* is performed when the user tabs out of a field. During field validation, the component associates all reported validation results with the field.
-* *Model validation* is performed when the user submits the form. During model validation, the component attempts to determine the field based on the member name that the validation result reports. Validation results that aren't associated with an individual member are associated with the model rather than a field.
-
-In custom validation scenarios:
+Without a `DataAnnotationsValidator` component, validation attributes on the model don't participate in the form's validation.
-* Validation manages a for a form's .
-* The component is used to attach validation support to forms based on [validation attributes (data annotations)](xref:mvc/models/validation#validation-attributes).
+### When validation runs
-There are two general approaches for achieving custom validation, which are described in the next two sections of this article:
+Blazor performs field validation and full-form validation:
-* [Manual validation using the `OnValidationRequested` event](#manual-validation-using-the-onvalidationrequested-event): Manually validate a form's fields with data annotations validation and custom code for field checks when validation is requested via an event handler assigned to the event.
-* [Validator components](#validator-components): One or more custom validator components can be used to process validation for different forms on the same page or the same form at different steps of form processing (for example, client validation followed by server-side validation in a Blazor Web App).
+* Field validation runs after a field changes. In an interactive form, this occurs in .NET while the user edits the form.
+* Full-form validation normally runs when `EditForm` handles submission through `OnValidSubmit` or `OnInvalidSubmit`. An `OnSubmit` handler takes control of validation, as described in [Control form submission](#control-form-submission).
:::moniker range=">= aspnetcore-11.0"
-## Client-side validation in static SSR forms
+A static SSR form can provide live browser feedback with . The form is validated again authoritatively on the server when posted.
-When a Blazor form that uses [static server-side rendering (static SSR)](xref:blazor/components/render-modes#static-server-side-rendering-static-ssr) contains a component, Blazor automatically validates the form in the browser before the form is submitted. Server-side data annotations validation continues to run after the form is posted, so the client-side check supplements but never replaces the server-side check.
-
-Client-side validation activates automatically when both conditions are met:
+:::moniker-end
-* The form's hosting component uses static SSR (no `@rendermode` directive applied to the component).
-* The form contains a component.
+:::moniker range=">= aspnetcore-8.0 < aspnetcore-11.0"
-### Supported validation attributes
+A static SSR form is validated on the server when posted and doesn't provide live field validation between requests.
-The following attributes are enforced client-side, matching the server-side data annotations behavior:
+:::moniker-end
-*
-*
-*
-*
-*
-*
-*
-*
-*
-*
-*
-*
+Validation results that identify a member are associated with that field. Results without a member name are associated with the model and appear in a validation summary rather than a field's `ValidationMessage` component.
-Validation attributes that don't appear in this list, including custom -derived attributes, aren't enforced client-side. They continue to run server-side after the form is submitted.
+:::moniker range=">= aspnetcore-10.0"
-### Validation timing
+### Configure data annotations validation
-A field validates when it loses focus (blur) for the first time. After a field has shown a validation error or after the form has been submitted at least once, the field re-validates on every change so corrections appear immediately. Submitting the form validates every field.
+`DataAnnotationsValidator` always enables DataAnnotations validation for the form. To use the extended validation capabilities provided by the package, call the `AddValidation` extension method in the `Program` file:
-### Validation messages and accessibility
+```csharp
+builder.Services.AddValidation();
+```
-The existing and components display client-side validation errors without any changes. ARIA attributes on input elements and on the validation message containers are managed by Blazor automatically so that assistive technologies announce validation errors without additional configuration.
+The `AddValidation` call registers the package's validation services and activates a source generator that creates validation metadata for discovered model types. The available behavior depends on whether that metadata includes the form's model:
-### Localized validation messages
+:::moniker-end
-When validation localization is configured through `Microsoft.Extensions.Validation`, error messages are localized at server-render time before being included in the page, so the client-side validation shows the same localized strings as the server-side experience. For more information, see .
+:::moniker range=">= aspnetcore-11.0"
-### CSS framework integration
+| Configuration | Behavior |
+|---|---|
+| Generated metadata is available | Validates nested objects and collections and supports message localization. |
+| Generated metadata isn't available | Validates top-level properties, but doesn't validate nested objects or collections and doesn't use the `Microsoft.Extensions.Validation` message-localization pipeline. |
-Client-side validation integrates with the browser's [Constraint Validation API](https://developer.mozilla.org/docs/Web/API/Constraint_validation), so the standard CSS pseudo-classes `:valid` and `:invalid` reflect each input's current validation state.
+:::moniker-end
-### Enhanced navigation
+:::moniker range=">= aspnetcore-10.0 < aspnetcore-11.0"
-Client-side validation is preserved across [enhanced navigation](xref:blazor/fundamentals/navigation#enhanced-navigation-and-form-handling). When the user navigates to a page that contains an SSR form, the form is wired up automatically. Multiple forms on the same page validate independently of each other.
+| Configuration | Behavior |
+|---|---|
+| Generated metadata is available | Validates nested objects and collections. |
+| Generated metadata isn't available | Validates top-level properties only. |
-### Opting out
+The `ValidatableTypeAttribute` and `SkipValidationAttribute` APIs are experimental in .NET 10. For details and available workarounds, see .
-To keep server-side data annotations validation but disable client-side enforcement for a single form, set the component's `DisableClientValidation` parameter to `true`:
+:::moniker-end
-```razor
-
-```
+:::moniker range=">= aspnetcore-10.0"
-To bypass client-side validation for a single submit button, use the standard HTML `formnovalidate` attribute on the button. The form is then posted without a client-side check, and server-side validation still runs after the post:
+When using `Microsoft.Extensions.Validation`, declare model types in C# files (`.cs`) rather than Razor component files (`.razor`). The source generator creates validation metadata from C# source and can't include model types declared in Razor components.
-```razor
-
-```
+For configuration requirements, validation order, custom rules, nested object graphs, and generated metadata, see .
:::moniker-end
-## Manual validation using the `OnValidationRequested` event
-
-You can manually validate a form with a custom event handler assigned to the event to manage a .
-
-The Blazor framework provides the component to attach additional validation support to forms based on [validation attributes (data annotations)](xref:mvc/models/validation#validation-attributes).
+:::moniker range="< aspnetcore-10.0"
-Recalling the earlier `Starship8` component example, the `HandleValidationRequested` method is assigned to , where you can perform manual validation in C# code. A few changes demonstrate combining the existing manual validation with data annotations validation via a and a validation attribute applied to the `Holodeck` model.
+### Validate nested object graphs
-Reference the namespace in the component's Razor directives at the top of the component definition file:
+In .NET 9 or earlier, `DataAnnotationsValidator` validates top-level model properties but doesn't recursively validate collection or complex-type properties. For recursive validation, use `ObjectGraphDataAnnotationsValidator` and `[ValidateComplexType]` from the experimental [`Microsoft.AspNetCore.Components.DataAnnotations.Validation` package](https://www.nuget.org/packages/Microsoft.AspNetCore.Components.DataAnnotations.Validation):
```razor
-@using System.ComponentModel.DataAnnotations
+
+
+ ...
+
```
-Add an `Id` property to the `Holodeck` model with a validation attribute to limit the string's length to six characters:
-
```csharp
-[StringLength(6)]
-public string? Id { get; set; }
+public class Starship
+{
+ [ValidateComplexType]
+ public ShipDescription Description { get; set; } =
+ new ShipDescription();
+}
```
-Add a component (``) to the form. Typically, the component is placed immediately under the `` tag, but you can place it anywhere in the form:
+The package remains experimental in these framework versions.
-```razor
-
-```
+:::moniker-end
-Change the form's submit behavior in the `` tag from to , which ensures that the form is valid before executing the assigned event handler method:
+:::moniker range="< aspnetcore-6.0"
-```diff
-- OnSubmit="Submit"
-+ OnValidSubmit="Submit"
-```
+### `[CompareProperty]` attribute
-In the ``, add a field for the `Id` property:
+For .NET 5 or earlier, use the experimental package's `ComparePropertyAttribute` instead of . `ComparePropertyAttribute` associates the validation result with the field consistently during field and full-form validation.
-```razor
-
-
-
-
-```
+:::moniker-end
+
+
-After making the preceding changes, the form's behavior matches the following specification:
+:::moniker range=">= aspnetcore-10.0"
-* The data annotations validation on the `Id` property doesn't trigger a validation failure when the `Id` field merely loses focus. The validation executes when the user selects the **`Update`** button.
-* Any manual validation that you want to perform in the `HandleValidationRequested` method assigned to the form's event executes when the user selects the form's **`Update`** button. In the existing code of the `Starship8` component example, the user must select either or both of the checkboxes to validate the form.
-* The form doesn't process the `Submit` method until both the data annotations and manual validation pass.
+### Write model-based custom rules
-:::moniker range=">= aspnetcore-11.0"
+When built-in attributes can't express a rule, use a custom or . For detailed guidance, see .
-## Asynchronous validation
+:::moniker-end
+
+:::moniker range="< aspnetcore-10.0"
+
+### Write model-based custom rules
- exposes an asynchronous validation pipeline that custom validator components and custom submit handlers can use to run validation work that performs I/O, such as calling a server endpoint to check a value's uniqueness. The pipeline is built around the following API:
+When built-in attributes can't express a rule, use a [custom validation attribute](xref:mvc/models/validation#custom-attributes) or implement [`IValidatableObject`](xref:mvc/models/validation#ivalidatableobject). Both run through `DataAnnotationsValidator`.
-
+
-* `Microsoft.AspNetCore.Components.Forms.EditContext.ValidateAsync`: an asynchronous counterpart to that awaits any registered async work and accepts a .
-* `ValidationRequestedEventArgs.AddAsyncValidator`: registers asynchronous work to run as part of the current validation pass. It's called from an handler, typically to validate the form as a whole on submit.
-* `EditContext.RegisterAsyncFieldValidator`: registers asynchronous work for a single field. Registering a new validation for a field cancels and replaces the field's current pending validation.
+Custom attributes can resolve registered services through .
- awaits any registered async work before invoking . Sync-only forms continue to work without changes.
+:::moniker-end
-The built-in component runs the asynchronous `DataAnnotations` APIs (`AsyncValidationAttribute` and `IAsyncValidatableObject`), so asynchronous rules declared on the model work without adopting the patterns in this section.
+## Add validation through `EditContext`
-> [!IMPORTANT]
-> Asynchronous work can only be registered during an asynchronous validation pass. If a form is validated with the synchronous method, `AddAsyncValidator` throws an that directs the caller to `ValidateAsync`. This guarantees that an asynchronous validator is never silently skipped.
+`EditForm` creates an `EditContext` automatically when its `Model` parameter is assigned. To use validation APIs directly, create the `EditContext` yourself and assign it to . Don't assign both `Model` and `EditContext` to the same form.
-### Form-level async validation
+Custom validation commonly uses:
-Subscribe to and call `AddAsyncValidator` from the handler to run async work whenever the form is validated as a whole. The framework invokes the registered validator with the validation pass's cancellation token, which should be passed to any I/O that the validator performs, so the work is cancelled when the framework supersedes the current validation pass.
+* for full-form validation.
+* for field validation.
+* to add and clear messages.
+* to notify the UI after messages change.
-In the following example, a custom validator component checks a username against a remote endpoint when the form is submitted:
+The following interactive-form pattern adds a form-level business rule alongside data annotations validation and rechecks the rule when either relevant field changes:
```razor
@implements IDisposable
-@inject HttpClient Http
-@code {
- [CascadingParameter]
- private EditContext? CurrentEditContext { get; set; }
+
+
+
- [Parameter, EditorRequired]
- public RegistrationModel Model { get; set; } = default!;
+ ...
+
- private ValidationMessageStore? _messages;
+@code {
+ private Starship Model { get; } = new Starship();
+ private EditContext editContext = default!;
+ private ValidationMessageStore messages = default!;
protected override void OnInitialized()
{
- ArgumentNullException.ThrowIfNull(CurrentEditContext);
- _messages = new ValidationMessageStore(CurrentEditContext);
- CurrentEditContext.OnValidationRequested += OnValidationRequested;
+ editContext = new EditContext(Model);
+ messages = new ValidationMessageStore(editContext);
+ editContext.OnValidationRequested += ValidateBusinessRules;
+ editContext.OnFieldChanged += ValidateChangedField;
}
- private void OnValidationRequested(
- object? sender, ValidationRequestedEventArgs e) =>
- e.AddAsyncValidator(ValidateUsernameAsync);
+ private void ValidateBusinessRules(
+ object? sender, ValidationRequestedEventArgs e)
+ {
+ messages.Clear();
+ ValidateIdentifier();
+ editContext.NotifyValidationStateChanged();
+ }
- private async Task ValidateUsernameAsync(CancellationToken token)
+ private void ValidateChangedField(
+ object? sender, FieldChangedEventArgs e)
{
- var field = CurrentEditContext!.Field(nameof(Model.Username));
- _messages!.Clear(field);
+ if (e.FieldIdentifier.FieldName != nameof(Starship.Identifier) &&
+ e.FieldIdentifier.FieldName != nameof(Starship.MaximumAccommodation))
+ {
+ return;
+ }
- var available = await Http.GetFromJsonAsync(
- $"api/usernames/available?value={Uri.EscapeDataString(Model.Username)}",
- token);
+ messages.Clear(
+ editContext.Field(nameof(Starship.Identifier)));
+ ValidateIdentifier();
+ editContext.NotifyValidationStateChanged();
+ }
- if (!available)
+ private void ValidateIdentifier()
+ {
+ if (Model.MaximumAccommodation == 1 &&
+ string.IsNullOrWhiteSpace(Model.Identifier))
{
- _messages.Add(field, "The username is already taken.");
+ messages.Add(
+ editContext.Field(nameof(Starship.Identifier)),
+ "An identifier is required for a single-occupant ship.");
}
-
- CurrentEditContext!.NotifyValidationStateChanged();
+ }
+
+ private void Submit()
+ {
+ // Process the valid form.
}
public void Dispose()
{
- if (CurrentEditContext is not null)
- {
- CurrentEditContext.OnValidationRequested -= OnValidationRequested;
- }
+ editContext.OnValidationRequested -= ValidateBusinessRules;
+ editContext.OnFieldChanged -= ValidateChangedField;
}
}
```
-Place the component inside an alongside the form's inputs. Because awaits the async handlers before invoking , the submit handler runs only after the remote check completes successfully:
+An `OnFieldChanged` handler receives the changed field in `e.FieldIdentifier`. Clear or replace the affected messages and call `NotifyValidationStateChanged`, as the preceding example demonstrates.
-```razor
-
-
-
-
-
-
-```
+:::moniker range=">= aspnetcore-8.0"
-### Per-field async validation
+Static SSR doesn't provide live .NET field validation between requests.
-For async work that should run when the user edits a single field, call `RegisterAsyncFieldValidator` with the field's and a validator that starts the work. The framework tracks each validation so the field's pending and faulted state can be queried and visualized independently of other fields.
+:::moniker-end
-The owns the cancellation token source. If the user edits the same field again while a check is in flight, the prior validation is canceled and superseded automatically, so there's no token source for the component to create, cancel, or dispose.
+:::moniker range=">= aspnetcore-11.0"
-Add the following members to the validator component shown in the previous section to re-run the uniqueness check whenever the `Username` field changes:
+For asynchronous full-form validation, call `e.AddAsyncValidator` from an `OnValidationRequested` handler. For asynchronous field validation in an interactive form, call from an `OnFieldChanged` handler. A new asynchronous validation for the same field supersedes and cancels the previous one.
-```csharp
-protected override void OnInitialized()
-{
- ArgumentNullException.ThrowIfNull(CurrentEditContext);
- _messages = new ValidationMessageStore(CurrentEditContext);
- CurrentEditContext.OnValidationRequested += OnValidationRequested;
- CurrentEditContext.OnFieldChanged += OnFieldChanged;
-}
+For model-based asynchronous validation attributes, see . For a complete reusable validator component, see .
-private void OnFieldChanged(object? sender, FieldChangedEventArgs e)
-{
- if (e.FieldIdentifier.FieldName != nameof(RegistrationModel.Username))
- {
- return;
- }
+:::moniker-end
- CurrentEditContext!.RegisterAsyncFieldValidator(
- e.FieldIdentifier,
- token => CheckAsync(e.FieldIdentifier, token));
-}
+For a reusable implementation that encapsulates event subscriptions and its message store, see .
-private async Task CheckAsync(FieldIdentifier field, CancellationToken token)
-{
- _messages!.Clear(field);
+
- var available = await Http.GetFromJsonAsync(
- $"api/usernames/available?value={Uri.EscapeDataString(Model.Username)}",
- token);
+## Display validation messages
- if (!available)
- {
- _messages.Add(field, "The username is already taken.");
- }
-
- CurrentEditContext!.NotifyValidationStateChanged();
-}
+Use to display messages associated with one field:
-public void Dispose()
-{
- if (CurrentEditContext is not null)
- {
- CurrentEditContext.OnValidationRequested -= OnValidationRequested;
- CurrentEditContext.OnFieldChanged -= OnFieldChanged;
- }
-}
+```razor
+
```
-A canceled task is discarded silently and does not change the field's faulted state. A task that throws an exception other than places the field in the faulted state described in the next section.
-
-### Pending and faulted state
+Use to display messages for the form:
-While an async task is in flight, the field is *pending*. If an async task throws an exception other than , the field is *faulted*. Each state has both a per-field and a form-level query:
-
-| State | Per-field | Form-level (any field) |
-|----------|----------------------------------------------------|----------------------------------|
-| Pending | `EditContext.IsValidationPending(fieldIdentifier)` | `EditContext.IsValidationPending()` |
-| Faulted | `EditContext.IsValidationFaulted(fieldIdentifier)` | `EditContext.IsValidationFaulted()` |
+```razor
+
+```
-The per-field overloads accept either a or a `() => model.Property` lambda for convenient use in Razor markup:
+Assign the summary's `Model` parameter to restrict it to messages associated with a particular model:
```razor
-
-
-
-@if (EditContext.IsValidationPending(() => Model.Username))
-{
- Checking…
-}
-else if (EditContext.IsValidationFaulted(() => Model.Username))
-{
-
- Validation could not be completed.
-
-}
+
```
-The form-level parameterless overloads return `true` when any field is currently pending or faulted. A common use is disabling the submit button while validation is in flight:
+To inspect current messages in code, call :
-```razor
-
+```csharp
+var allMessages = editContext.GetValidationMessages();
+var fieldMessages = editContext.GetValidationMessages(
+ editContext.Field(nameof(Starship.Identifier)));
```
- automatically adds the `pending` and `faulted` CSS classes to its rendered element while the bound field is in the corresponding state, in addition to the existing `modified` / `valid` / `invalid` classes. The classes compose, so unmodified pending styling and modified pending styling can be targeted independently:
+These methods read the current validation state. They don't initiate validation.
-```css
-.pending {
- background-image: url('spinner.gif');
- background-repeat: no-repeat;
- background-position: right center;
-}
+## Customize validation appearance
-.modified.pending {
- border-color: lightblue;
-}
+Blazor applies CSS classes that represent field and message state:
-.modified.faulted {
- border-color: orange;
-}
-```
+| Element | Classes |
+|---|---|
+| Input | `valid` or `invalid`, plus `modified` after the user edits the field |
+| Validation message | `validation-message` |
+| Validation summary | `validation-summary-errors` or `validation-summary-valid` |
-### Calling `ValidateAsync` from a custom submit handler
+:::moniker range=">= aspnetcore-11.0"
-
+The Blazor project templates include styles for the common valid and invalid classes. Add styles for other classes as needed. `ValidationMessage` and `ValidationSummary` also accept arbitrary HTML attributes. Supplying a `class` attribute replaces the component's default class.
-When a form uses instead of , call `Microsoft.AspNetCore.Components.Forms.EditContext.ValidateAsync` from the handler to await any registered async work before deciding whether to proceed:
+:::moniker range=">= aspnetcore-5.0"
-```razor
-
-
-
-
-
+To change the classes applied to input components, derive from .
-@code {
- private EditContext _editContext = default!;
+:::moniker-end
+
+:::moniker range=">= aspnetcore-8.0"
- protected override void OnInitialized() =>
- _editContext = new EditContext(Model);
+```csharp
+using Microsoft.AspNetCore.Components.Forms;
- private async Task HandleSubmitAsync()
+public sealed class BootstrapFieldCssClassProvider : FieldCssClassProvider
+{
+ public override string GetFieldCssClass(
+ EditContext editContext,
+ in FieldIdentifier fieldIdentifier)
{
- if (await _editContext.ValidateAsync(CancellationToken.None))
+ if (!editContext.IsModified(fieldIdentifier))
{
- await RegisterAsync();
+ return string.Empty;
}
+
+ return editContext.IsValid(fieldIdentifier)
+ ? "is-valid"
+ : "is-invalid";
}
}
```
-
+:::moniker range=">= aspnetcore-5.0 < aspnetcore-8.0"
-The synchronous method continues to work for forms that only have synchronous validators, but it's obsolete as of .NET 11. Call `Microsoft.AspNetCore.Components.Forms.EditContext.ValidateAsync` instead. If a handler attempts to register asynchronous work during a synchronous pass, `AddAsyncValidator` throws an directing the caller to use `ValidateAsync`.
+```csharp
+using System.Linq;
+using Microsoft.AspNetCore.Components.Forms;
-### Async validation across rendering modes
+public sealed class BootstrapFieldCssClassProvider : FieldCssClassProvider
+{
+ public override string GetFieldCssClass(
+ EditContext editContext,
+ in FieldIdentifier fieldIdentifier)
+ {
+ if (!editContext.IsModified(fieldIdentifier))
+ {
+ return string.Empty;
+ }
-The async validation API is the same in every Blazor rendering mode. Validator code runs wherever the component runs: in the browser for Interactive WebAssembly, on the server for Interactive Server, and on the server during the form POST for static SSR. Static SSR renders the full response after async validation completes.
+ return editContext.GetValidationMessages(fieldIdentifier).Any()
+ ? "is-invalid"
+ : "is-valid";
+ }
+}
+```
:::moniker-end
-## Validator components
+:::moniker range=">= aspnetcore-11.0"
-Validator components support form validation by managing a for a form's .
+A custom `FieldCssClassProvider` determines the complete class value for each field. If the form uses asynchronous field validation, handle `IsValidationPending(fieldIdentifier)` and `IsValidationFaulted(fieldIdentifier)` in the provider when pending or faulted classes are required.
-The Blazor framework provides the component to attach validation support to forms based on [validation attributes (data annotations)](xref:mvc/models/validation#validation-attributes). You can create custom validator components to process validation messages for different forms on the same page or the same form at different steps of form processing (for example, client validation followed by server-side validation in a Blazor Web App). The validator component example shown in this section, `CustomValidation`, is used in the following sections of this article:
+:::moniker-end
-* [Business logic validation with a validator component](#business-logic-validation-with-a-validator-component)
-* [Remote validation with a validator component](#remote-validation-with-a-validator-component)
+:::moniker range=">= aspnetcore-5.0"
-Of the [data annotation built-in validators](xref:mvc/models/validation#built-in-attributes), only the [`[Remote]` validation attribute](xref:mvc/models/validation#remote-attribute) isn't supported in Blazor.
+Assign the provider to the form's `EditContext`:
-> [!NOTE]
-> Custom data annotation validation attributes can be used instead of custom validator components in many cases. Custom attributes applied to the form's model activate with the use of the component. When used with server-side validation in a Blazor Web App, any custom attributes applied to the model must be executable on the server. For more information, see the [Custom validation attributes](#custom-validation-attributes) section.
+```csharp
+editContext.SetFieldCssClassProvider(
+ new BootstrapFieldCssClassProvider());
+```
-Create a validator component from :
+For custom input markup, call to obtain the class selected by the current provider.
-* The form's is a [cascading parameter](xref:blazor/components/cascading-values-and-parameters) of the component.
-* When the validator component is initialized, a new is created to maintain a current list of form errors.
-* The message store receives errors when developer code in the form's component calls the `DisplayErrors` method. The errors are passed to the `DisplayErrors` method in a [`Dictionary>`](xref:System.Collections.Generic.Dictionary%602). In the dictionary, the key is the name of the form field that has one or more errors. The value is the error list.
-* Messages are cleared when any of the following have occurred:
- * Validation is requested on the when the event is raised. All of the errors are cleared.
- * A field changes in the form when the event is raised. Only the errors for the field are cleared.
- * The `ClearErrors` method is called by developer code. All of the errors are cleared.
+:::moniker-end
-Update the namespace in the following class to match your app's namespace.
+
-`CustomValidation.cs`:
+## Respond to validation state
-:::code language="csharp" source="~/../blazor-samples/8.0/BlazorSample_BlazorWebApp/CustomValidation.cs":::
+`EditContext` exposes the current validation state without initiating validation.
-> [!IMPORTANT]
-> Specifying a namespace is **required** when deriving from . Failing to specify a namespace results in a build error:
->
-> > :::no-loc text="Tag helpers cannot target tag name '\.{CLASS NAME}' because it contains a ' ' character.":::
->
-> The `{CLASS NAME}` placeholder is the name of the component class. The custom validator example in this section specifies the example namespace `BlazorSample`.
+* Use `IsModified(field)` or `IsModified()` to determine whether a field or any field in the form has changed.
+* Use `GetValidationMessages(field)` or `GetValidationMessages()` to inspect current field or form messages.
-> [!NOTE]
-> Anonymous lambda expressions are registered event handlers for and in the preceding example. It isn't necessary to implement and unsubscribe the event delegates in this scenario. For more information, see .
+:::moniker range=">= aspnetcore-8.0"
-## Business logic validation with a validator component
+Use `IsValid(field)` to determine whether a field currently has validation messages.
-For general business logic validation, use a [validator component](#validator-components) that receives form errors in a dictionary.
+:::moniker-end
-Basic validation is useful in cases where the form's model is defined within the component hosting the form, either as members directly on the component or in a subclass. Use of a validator component is recommended where an independent model class is used across several components.
+:::moniker range="< aspnetcore-8.0"
-In the following example:
+For a field, the absence of messages can be checked with `!editContext.GetValidationMessages(field).Any()`.
-* A shortened version of the `Starfleet Starship Database` form (`Starship3` component) of the [Example form](xref:blazor/forms/input-components#example-form) section of the *Input components* article is used that only accepts the starship's classification and description. Data annotation validation isn't triggered on form submission because the component isn't included in the form.
-* The `CustomValidation` component from the [Validator components](#validator-components) section of this article is used.
-* The validation requires a value for the ship's description (`Description`) if the user selects the "`Defense`" ship classification (`Classification`).
+:::moniker-end
-When validation messages are set in the component, they're added to the validator's and shown in the 's validation summary.
+The following example displays custom UI only after a field is modified and invalid:
-`Starship9.razor`:
+:::moniker range=">= aspnetcore-8.0"
-:::moniker range=">= aspnetcore-9.0"
+```razor
+@{
+ var identifier = editContext.Field(nameof(Starship.Identifier));
+}
-:::code language="razor" source="~/../blazor-samples/9.0/BlazorSample_BlazorWebApp/Components/Pages/Starship9.razor":::
+@if (editContext.IsModified(identifier) &&
+ !editContext.IsValid(identifier))
+{
+
+}
+```
:::moniker-end
-:::moniker range="< aspnetcore-8.0"
+Input components, `ValidationMessage`, and `ValidationSummary` update themselves when validation state changes. A component that renders other conditional validation UI should subscribe to and call `StateHasChanged`:
-```razor
-@page "/starship-9"
-@inject ILogger Logger
+```csharp
+private void HandleValidationStateChanged(
+ object? sender, ValidationStateChangedEventArgs e) =>
+ _ = InvokeAsync(StateHasChanged);
+```
-
Starfleet Starship Database
+Unsubscribe from `OnValidationStateChanged` when the component is disposed.
-
New Ship Entry Form
+:::moniker range=">= aspnetcore-11.0"
-
-
-
-
-
-
-
-
-
-
-
-
-
+Use `IsValidationPending(field)` and `IsValidationFaulted(field)` for asynchronous field validation. The parameterless methods describe form-level `ValidateAsync` passes and don't aggregate the state of every field.
-@code {
- private CustomValidation? customValidation;
+These states also include asynchronous work performed by . An `AsyncValidationAttribute` applied to a property uses field state during field validation, including the default `pending` and `faulted` CSS classes. During `ValidateAsync`, asynchronous attributes and contribute to the form-level state reported by the parameterless methods.
+
+Live pending indicators require an interactive render mode. During a static SSR form post, server-side validation completes before the response is rendered.
- public Starship? Model { get; set; }
+:::moniker-end
- protected override void OnInitialized() =>
- Model ??= new() { ProductionDate = DateTime.UtcNow };
+## Control form submission
- private void Submit()
- {
- customValidation?.ClearErrors();
+`EditForm` provides three submission callbacks:
- var errors = new Dictionary>();
+| Callback | Behavior |
+|---|---|
+| | Runs after automatic validation succeeds. |
+| | Runs after automatic validation fails. |
+| | Gives the handler control of validation and submission. |
- if (Model!.Classification == "Defense" &&
- string.IsNullOrEmpty(Model.Description))
- {
- errors.Add(nameof(Model.Description),
- new() { "For a 'Defense' ship classification, " +
- "'Description' is required." });
- }
+`OnValidSubmit` and `OnInvalidSubmit` can be used together. Don't combine `OnSubmit` with either of them.
- if (errors.Any())
- {
- customValidation?.DisplayErrors(errors);
- }
- else
+:::moniker range=">= aspnetcore-11.0"
+
+`EditForm` uses before invoking `OnValidSubmit` or `OnInvalidSubmit`, so it awaits synchronous and asynchronous validators. When handling `OnSubmit`, call `ValidateAsync` before processing the form:
+
+```razor
+
+ ...
+
+
+@code {
+ private async Task HandleSubmit(EditContext editContext)
+ {
+ if (await editContext.ValidateAsync())
{
- Logger.LogInformation("Submit called: Processing the form");
+ await SaveAsync();
}
}
}
```
-
+The synchronous method is obsolete in .NET 11. It doesn't await asynchronous validation and throws if a handler attempts to register asynchronous work.
-:::moniker-end
-
-> [!NOTE]
-> As an alternative to using [validation components](#validator-components), data annotation validation attributes can be used. Custom attributes applied to the form's model activate with the use of the component. When used with server-side validation in a Blazor Web App, the attributes must be executable on the server. For more information, see the [Custom validation attributes](#custom-validation-attributes) section.
-
-:::moniker range=">= aspnetcore-10.0"
-
-## Remote validation in a Minimal API
-
-In a [Minimal API](xref:fundamentals/minimal-apis), call the extension method for [data annotation validation of model types](xref:mvc/models/validation#validation-attributes) for all web API endpoints:
-
-```csharp
-builder.Services.AddValidation();
-```
-
-The implementation automatically discovers types that are defined in Minimal API handlers or as base types of types defined in Minimal API handlers. An endpoint filter performs validation on these types and is added for each endpoint.
-
-Built-in validation also supports [custom validation attributes](xref:mvc/models/validation#custom-attributes).
-
-For more information, see .
-
-:::moniker-end
-
-## Remote validation with a validator component
-
-:::moniker range=">= aspnetcore-10.0"
-
-*This section demonstrates remote validation using a Blazor Web App (global Interactive Auto render mode) and a Minimal API.*
-
-Remote validation is supported in addition to Blazor Web App client/server-side validation:
-
-* Process client validation in the form with the component.
-* When the form passes client validation ( is called), send the to a backend Minimal API for remote validation.
-* Process remote model validation:
- * Data annotations validation with built-in support for Minimal APIs.
- * Custom validation logic.
-* Send validation errors, if any, back to the client.
-* Either disable the form on success or display the errors so that the user can correct any problems with the form's field values.
-
-Basic validation is useful in cases where the form's model is defined within the component hosting the form, either as members directly on the component or in a subclass. Use of a *validator component* is recommended where an independent model class is used across several components. The approach demonstrated by the following guidance uses a validator component.
-
-The following example is based on:
-
-* A Blazor Web App with global Interactive Auto components created from the [Blazor Web App project template](xref:blazor/project-structure).
-* A `CustomValidation` component to handle adding model errors to the form's validation message store for display in the UI.
-* A [Minimal API](xref:fundamentals/minimal-apis) project that validates:
- * Data annotations validation attributes on the model class (), including for [custom validation attributes](xref:mvc/models/validation#custom-attributes).
- * Custom validation logic that determines if a description form field (`Description`) has a value if the user selects a particular classification in another form field (`Defense` classification).
-
-The validation for the `Defense` ship classification only occurs on the server because the upcoming form doesn't perform the same validation client-side when the form is submitted to the server. Remote validation without client validation is common in apps that require private business logic validation of user input on the server. For example, private information from data stored for a user might be required to validate user input. Private data is never sent to the client for client validation.
-
-> [!NOTE]
-> For more information on security pertaining to the following example, see the following resources:
->
-> *
-> * (and the other articles in the Blazor *Security and Identity* node)
-> * [Microsoft identity platform documentation](/entra/identity-platform/)
-
-Create a `Starship` folder in the `.Client` project of the Blazor Web App.
-
-Place the following `StarshipModel` model (`StarshipModel.cs`) into the `Starship` folder ***and*** into the Minimal API project of the solution.
-
-> [!NOTE]
-> If you choose to place one copy of the `StarshipModel` into a shared class library project for use by both the Blazor Web App and the Minimal API project, confirm that the shared class library uses the shared framework or add the [`System.ComponentModel.Annotations` package](https://www.nuget.org/packages/System.ComponentModel.Annotations) to the shared project. This ensures that the model has access to data annotations.
->
-> [!INCLUDE[](~/includes/package-reference.md)]
-
-In the two `StarshipModel` classes, set the namespace (`{NAMESPACE}`) appropriately for each project (for example, `BlazorSample.Client.Starship` in the Blazor Web App and `MinimalApiJwt.Models` in the Minimal API project). Some developers prefer to use a different folder scheme. If you position the classes in the projects in different locations, set the namespaces appropriately.
-
-`Starship/StarshipModel.cs` (Blazor Web App) or `Models/StarshipModel.cs` (Minimal API project):
-
-```csharp
-using System.ComponentModel.DataAnnotations;
-
-namespace {NAMESPACE};
-
-public class StarshipModel
-{
- [Required]
- [StringLength(16, ErrorMessage = "Identifier too long (16 character limit).")]
- public string? Id { get; set; }
-
- public string? Description { get; set; }
-
- [Required]
- public string? Classification { get; set; }
-
- [Range(1, 100000, ErrorMessage = "Accommodation invalid (1-100000).")]
- public int MaximumAccommodation { get; set; }
-
- [Required]
- [Range(typeof(bool), "true", "true", ErrorMessage = "Approval required.")]
- public bool IsValidatedDesign { get; set; }
-
- [Required]
- public DateTime ProductionDate { get; set; }
-}
-```
-
-Add an interface for a form validation service to the `.Client` project in the `Starship` folder. The interface is used to register validation services in the Blazor Web App.
-
-`Starship/IFormValidation.cs`:
-
-```csharp
-namespace BlazorSample.Client.Starship;
-
-public interface IFormValidation
-{
- Task> ValidateStarshipFormAsync(
- StarshipModel starship);
-}
-```
-
-Add a client form validator class to the `.Client` project's `Starship` folder. The client form validator is used when the app is running on the client. The validator class posts to the Blazor Web App endpoint, which then proxies to the Minimal API.
-
-`Starship/ClientFormValidation.cs`:
-
-```csharp
-using System.Net.Http.Json;
-
-namespace BlazorSample.Client.Starship;
-
-internal sealed class ClientFormValidation(HttpClient httpClient) : IFormValidation
-{
- public async Task> ValidateStarshipFormAsync(
- StarshipModel starship)
- {
- Dictionary genericError = new()
- {
- {
- "Validation Error",
- ["An unexpected client error occurred during validation."]
- }
- };
-
- try
- {
- using var response = await httpClient.PostAsJsonAsync(
- "/starship-validation", starship);
-
- if (response.IsSuccessStatusCode)
- {
- var deserializedResponseContent =
- await response.Content.ReadFromJsonAsync
- >();
-
- return deserializedResponseContent ?? genericError;
- }
- }
- catch (Exception ex)
- {
- // Log exception
- }
-
- return genericError;
- }
-}
-```
-
-Confirm or update the namespace of the preceding class.
-
-Create a `Starship` folder in the server project of the Blazor Web App.
-
-In the Blazor Web App, create a server form validator that implements the `IFormValidation` interface. Place the server form validator class in the server-side `Starship` folder. The server form validator is used when the Blazor Web App is running on the server. The validator class posts the form's model to the backend Minimal API for processing.
-
-`Starship/ServerFormValidation.cs`:
-
-```csharp
-using System.Net;
-using System.Net.Http.Headers;
-using System.Text.Json;
-using Microsoft.AspNetCore.Authentication;
-using Microsoft.AspNetCore.Mvc;
-using BlazorSample.Client.Starship;
-
-namespace BlazorSample.Starship;
-
-internal sealed class ServerFormValidation(
- IHttpContextAccessor httpContextAccessor, IHttpClientFactory httpClientFactory)
- : IFormValidation
-{
- public async Task> ValidateStarshipFormAsync(
- StarshipModel starship)
- {
- Dictionary genericError = new()
- {
- {
- "Validation Error",
- ["An unexpected server error occurred during validation."]
- }
- };
-
- try
- {
- if (httpContextAccessor.HttpContext is null)
- {
- throw new Exception("HttpContext not available");
- }
-
- var request = new HttpRequestMessage(HttpMethod.Post,
- "https://localhost:7277/api-starship-validation")
- {
- Content = new StringContent(JsonSerializer.Serialize(starship),
- System.Text.Encoding.UTF8, "application/json")
- };
-
- var accessToken =
- await httpContextAccessor.HttpContext.GetTokenAsync("access_token");
-
- request.Headers.Authorization =
- new AuthenticationHeaderValue("Bearer", accessToken);
-
- using var httpClient = httpClientFactory.CreateClient();
-
- var response = await httpClient.SendAsync(request);
-
- if (response?.StatusCode == HttpStatusCode.NoContent)
- {
- return new Dictionary();
- }
-
- if (response?.StatusCode == HttpStatusCode.BadRequest)
- {
- var content = await response.Content.ReadAsStringAsync();
-
- var deserialized =
- JsonSerializer.Deserialize(
- content,
- new JsonSerializerOptions(JsonSerializerDefaults.Web));
-
- return deserialized?.Errors ?? genericError;
- }
-
- return genericError;
- }
- catch (Exception ex)
- {
- // Log exception
- }
-
- return genericError;
- }
-}
-```
-
-In the `Program` file of the Blazor Web App:
-
-* Register the server form validator (`ServerFormValidation`) for the `IFormValidation` interface in the DI container.
-* The server form validator is used on the server to call `ValidateStarshipFormAsync` for form validation.
-
-```csharp
-builder.Services.AddScoped();
-
-...
-
-app.MapPost("/starship-validation", (IFormValidation formValidator,
- StarshipModel model) =>
-{
- return formValidator.ValidateStarshipFormAsync(model);
-}).RequireAuthorization();
-```
-
-The `.Client` project of a Blazor Web App must register an for HTTP POST requests to the Minimal API. Add the following to the `.Client` project's `Program` file:
-
-```csharp
-builder.Services.AddHttpClient(httpClient =>
-{
- httpClient.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress);
-});
-```
-
-The preceding example sets the base address with `builder.HostEnvironment.BaseAddress` (), which gets the base address for the app and is typically derived from the `` tag's `href` value in the host page.
-
-In the `Program` file of the `MinimalApiJwt` project, add the following starship form validation endpoint. The endpoint validates that the model's `Description` property has a value when the model's `Classification` property is `Defense`. If validation fails, a `ValidationProblem` returns a dictionary with the failed field and a description of the error. If validation passes, a *204 - No Content* response is issued. In a typical production app, any number of custom form model checks are made, and the validation errors dictionary can include multiple failures (`string[]` value) for each model property.
-
-In the `Program` file of the Minimal API project:
-
-```csharp
-app.MapPost("/api-starship-validation", (
- StarshipModel model, ILogger logger) =>
-{
- Dictionary errors = [];
-
- if (model.Classification == "Defense" && string.IsNullOrEmpty(model.Description))
- {
- errors.Add(nameof(model.Description),
- ["For a 'Defense' ship, 'Description' is required."]);
- }
-
- if (errors.Count > 0)
- {
- return Results.ValidationProblem(
- errors: errors,
- detail: "One or more validation errors occurred.",
- instance: typeof(Program).Assembly.GetName().Name,
- title: "Validation Errors",
- type: "https://tools.ietf.org/html/rfc9110#section-15.5.1");
- }
-
- return Results.NoContent();
-
-}).RequireAuthorization();
-```
-
-Also in the `Program` file of the Minimal API, register [built-in validation services](xref:fundamentals/minimal-apis#validation-support-in-minimal-apis):
-
-```csharp
-builder.Services.AddValidation();
-```
-
-Built-in validation automatically intercepts the endpoint request and validates the types that the endpoint receives. If the model fails validation, the framework returns a *400 - Bad Request* response with error details without executing the endpoint's code. If you don't want to implement built-in model validation, don't use the preceding line of code in the Minimal API's `Program` file.
-
-In the `.Client` project, add the following `CustomValidation` component. When the component's `DisplayErrors` method is called with a set of validation errors, the errors are added to the parent component's edit context validation message store. Errors are cleared from the edit context by calling the `ClearErrors` method.
-
-`CustomValidation.cs`:
-
-```csharp
-using Microsoft.AspNetCore.Components;
-using Microsoft.AspNetCore.Components.Forms;
-using Microsoft.AspNetCore.Mvc;
-
-namespace BlazorSample.Client;
-
-public class CustomValidation : ComponentBase
-{
- private ValidationMessageStore? messageStore;
-
- [CascadingParameter]
- private EditContext? CurrentEditContext { get; set; }
-
- protected override void OnInitialized()
- {
- if (CurrentEditContext is null)
- {
- throw new InvalidOperationException(
- $"{nameof(CustomValidation)} requires a cascading " +
- $"parameter of type {nameof(EditContext)}. " +
- $"For example, you can use {nameof(CustomValidation)} " +
- $"inside an {nameof(EditForm)}.");
- }
-
- messageStore = new(CurrentEditContext);
-
- CurrentEditContext.OnValidationRequested += (s, e) =>
- messageStore?.Clear();
- CurrentEditContext.OnFieldChanged += (s, e) =>
- messageStore?.Clear(e.FieldIdentifier);
- }
-
- public void DisplayErrors(IDictionary errors)
- {
- if (CurrentEditContext is not null)
- {
- foreach (var err in errors)
- {
- messageStore?.Add(CurrentEditContext.Field(err.Key), err.Value);
- }
-
- CurrentEditContext.NotifyValidationStateChanged();
- }
- }
-
- public void ClearErrors()
- {
- messageStore?.Clear();
- CurrentEditContext?.NotifyValidationStateChanged();
- }
-}
-```
-
-In the `.Client` project, the `Starfleet Starship Database` form is updated to show validation errors with help of the `CustomValidation` component. When validation messages are returned, they're added to the `CustomValidation` component's . The errors are available in the form's for display by the form's validation summary. Confirm or update the namespace for `BlazorSample.Client.Starship`.
-
-Note that the form requires authorization, so the user must be signed into the app to navigate to the form.
-
-> [!NOTE]
-> Forms based on automatically enable [antiforgery support](xref:blazor/forms/index#antiforgery-support).
-
-`Pages/Starship10.razor` in the `.Client` project:
-
-```razor
-@page "/starship-10"
-@using Microsoft.AspNetCore.Authorization
-@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
-@using BlazorSample.Client.Starship
-@attribute [Authorize]
-@inject IFormValidation FormValidation
-@inject ILogger Logger
-
-
Starfleet Starship Database
-
-
New Ship Entry Form
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- @message
-
-
-
-@code {
- private CustomValidation? customValidation;
- private bool disabled;
- private string? message;
- private string messageStyles = "visibility:hidden";
-
- [SupplyParameterFromForm]
- private StarshipModel? Model { get; set; }
-
- protected override void OnInitialized() =>
- Model ??= new() { ProductionDate = DateTime.UtcNow };
-
- private async Task Submit(EditContext editContext)
- {
- customValidation?.ClearErrors();
-
- try
- {
- var validationProblemDetails =
- await FormValidation.ValidateStarshipFormAsync(
- (StarshipModel)editContext.Model);
-
- if (validationProblemDetails?.Count > 0)
- {
- customValidation?.DisplayErrors(validationProblemDetails);
- }
- else
- {
- disabled = true;
- messageStyles = "color:green";
- message = "The form has been processed.";
- }
- }
- catch (AccessTokenNotAvailableException ex)
- {
- ex.Redirect();
- }
- catch (Exception ex)
- {
- Logger.LogError(ex, "Form processing error.");
- disabled = true;
- messageStyles = "color:red";
- message = "There was an error processing the form.";
- }
- }
-}
-```
-
-> [!NOTE]
-> As an alternative to the use of a [validation component](#validator-components), custom data annotation validation attributes can be used. Custom attributes applied to the form's model activate with the use of the component. For more information, see the [Custom validation attributes](#custom-validation-attributes) section.
-
-To reach the form easily, add the following entry to the `NavMenu` component (`Layout/NavMenu.razor`) in the `.Client` project:
-
-```razor
-
-
- Starship
-
-
-```
-
-When automatic model binding validation fails on the server, the framework returns a [default bad request response](xref:web-api/index#default-badrequest-response) with a . The response contains more data than just the validation errors, as shown in the following example when all of the fields of the `Starfleet Starship Database` form aren't submitted and the form fails validation:
-
-```json
-{
- "type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
- "title": "One or more validation errors occurred.",
- "status": 400,
- "errors": {
- "Id": ["The Id field is required."],
- "Classification": ["The Classification field is required."],
- "IsValidatedDesign": ["This form disallows unapproved ships."],
- "MaximumAccommodation": ["Accommodation invalid (1-100000)."]
- }
-}
-```
-
-> [!NOTE]
-> To demonstrate the preceding JSON responses, you must either disable the form's client validation to permit empty field form submission or use a tool to send a request directly to the Minimal API, such as [Firefox Browser Developer](https://www.mozilla.org/firefox/developer/).
-
-If automatic type validation passes but the custom validation fails, the following JSON response is received from the Minimal API:
-
-```json
-{
- "type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
- "title": "One or more validation errors occurred.",
- "instance": "MinimalApiJwt",
- "status": 400,
- "errors": {
- "Description": ["For a 'Defense' ship, 'Description' is required."]
- }
-}
-```
-
-:::moniker-end
-
-:::moniker range=">= aspnetcore-8.0 < aspnetcore-10.0"
-
-*This section is focused on Blazor Web App scenarios, but the approach for any type of app that uses server-side validation with web API adopts the same general approach.*
-
-Remote validation is supported in addition to Blazor Web App client-side and server-side validation:
-
-* Process client validation in the form with the component.
-* When the form passes client validation ( is called), send the to a backend server API for form processing.
-* Process model validation on the server.
-* The server API includes both the built-in framework data annotations validation and custom validation logic supplied by the developer. If validation passes on the server, process the form and send back a success status code ([`200 - OK`](https://developer.mozilla.org/docs/Web/HTTP/Status/200)). If validation fails, return a failure status code ([`400 - Bad Request`](https://developer.mozilla.org/docs/Web/HTTP/Status/400)) and the field validation errors.
-* Either disable the form on success or display the errors.
-
-Basic validation is useful in cases where the form's model is defined within the component hosting the form, either as members directly on the component or in a subclass. Use of a validator component is recommended where an independent model class is used across several components.
-
-The following example is based on:
-
-* A Blazor Web App with Interactive WebAssembly components created from the [Blazor Web App project template](xref:blazor/project-structure).
-* The `Starship` model (`Starship.cs`) of the [Example form](xref:blazor/forms/input-components#example-form) section of the *Input components* article.
-* The `CustomValidation` component shown in the [Validator components](#validator-components) section.
-
-Place the `Starship` model (`Starship.cs`) into a shared class library project so that both the client and server projects can use the model. Add or update the namespace to match the namespace of the shared app (for example, `namespace BlazorSample.Shared`). Since the model requires data annotations, confirm that the shared class library uses the shared framework or add the [`System.ComponentModel.Annotations` package](https://www.nuget.org/packages/System.ComponentModel.Annotations) to the shared project.
-
-[!INCLUDE[](~/includes/package-reference.md)]
-
-In the main project of the Blazor Web App, add a controller to process starship validation requests and return failed validation messages. Update the namespaces in the last `using` statement for the shared class library project and the `namespace` for the controller class. In addition to client and server data annotations validation, the controller validates that a value is provided for the ship's description (`Description`) if the user selects the `Defense` ship classification (`Classification`).
-
-The validation for the `Defense` ship classification only occurs on the server in the controller because the upcoming form doesn't perform the same validation client-side when the form is submitted to the server. Remote validation is common in apps that require private business logic validation of user input. For example, private information from data stored for a user might be required to validate user input. Private data obviously can't be sent to the client for client validation.
-
-> [!NOTE]
-> The `StarshipValidation` controller in this section uses Microsoft Identity 2.0. The Web API only accepts tokens for users that have the "`API.Access`" scope for this API. Additional customization is required if the API's scope name is different from `API.Access`.
->
-> For more information on security, see:
->
-> * (and the other articles in the Blazor *Security and Identity* node)
-> * [Microsoft identity platform documentation](/entra/identity-platform/)
-
-`Controllers/StarshipValidation.cs`:
-
-```csharp
-using Microsoft.AspNetCore.Authorization;
-using Microsoft.AspNetCore.Mvc;
-using BlazorSample.Shared;
-
-namespace BlazorSample.Server.Controllers;
-
-[Authorize]
-[ApiController]
-[Route("[controller]")]
-public class StarshipValidationController(
- ILogger logger)
- : ControllerBase
-{
- static readonly string[] scopeRequiredByApi = [ "API.Access" ];
-
- [HttpPost]
- public async Task Post(Starship model)
- {
- HttpContext.VerifyUserHasAnyAcceptedScope(scopeRequiredByApi);
-
- try
- {
- if (model.Classification == "Defense" &&
- string.IsNullOrEmpty(model.Description))
- {
- ModelState.AddModelError(nameof(model.Description),
- "For a 'Defense' ship " +
- "classification, 'Description' is required.");
- }
- else
- {
- logger.LogInformation("Processing the form asynchronously");
-
- // async ...
-
- return Ok(ModelState);
- }
- }
- catch (Exception ex)
- {
- logger.LogError("Validation Error: {Message}", ex.Message);
- }
-
- return BadRequest(ModelState);
- }
-}
-```
-
-Confirm or update the namespace of the preceding controller (`BlazorSample.Server.Controllers`) to match the app's controllers' namespace.
-
-When a model binding validation error occurs on the server, an [`ApiController`](xref:web-api/index) () normally returns a [default bad request response](xref:web-api/index#default-badrequest-response) with a . The response contains more data than just the validation errors, as shown in the following example when all of the fields of the `Starfleet Starship Database` form aren't submitted and the form fails validation:
-
-```json
-{
- "title": "One or more validation errors occurred.",
- "status": 400,
- "errors": {
- "Id": [ "The Id field is required." ],
- "Classification": [ "The Classification field is required." ],
- "IsValidatedDesign": [ "This form disallows unapproved ships." ],
- "MaximumAccommodation": [ "Accommodation invalid (1-100000)." ]
- }
-}
-```
-
-> [!NOTE]
-> To demonstrate the preceding JSON response, you must either disable the form's client validation to permit empty field form submission or use a tool to send a request directly to the server API, such as [Firefox Browser Developer](https://www.mozilla.org/firefox/developer/).
-
-If the server API returns the preceding default JSON response, it's possible for the client to parse the response in developer code to obtain the children of the `errors` node for forms validation error processing. It's inconvenient to write developer code to parse the file. Parsing the JSON manually requires producing a [`Dictionary>`](xref:System.Collections.Generic.Dictionary%602) of errors after calling . Ideally, the server API should only return the validation errors, as the following example shows:
-
-```json
-{
- "Id": [ "The Id field is required." ],
- "Classification": [ "The Classification field is required." ],
- "IsValidatedDesign": [ "This form disallows unapproved ships." ],
- "MaximumAccommodation": [ "Accommodation invalid (1-100000)." ]
-}
-```
-
-To modify the server API's response to make it only return the validation errors, change the delegate that's invoked on actions that are annotated with in the `Program` file. For the API endpoint (`/StarshipValidation`), return a with the . For any other API endpoints, preserve the default behavior by returning the object result with a new .
-
-Add the namespace to the top of the `Program` file in the main project of the Blazor Web App:
-
-```csharp
-using Microsoft.AspNetCore.Mvc;
-```
-
-In the `Program` file, add or update the following extension method and add the following call to :
-
-```csharp
-builder.Services.AddControllersWithViews()
- .ConfigureApiBehaviorOptions(options =>
- {
- options.InvalidModelStateResponseFactory = context =>
- {
- if (context.HttpContext.Request.Path == "/StarshipValidation")
- {
- return new BadRequestObjectResult(context.ModelState);
- }
- else
- {
- return new BadRequestObjectResult(
- new ValidationProblemDetails(context.ModelState));
- }
- };
- });
-```
-
-If you're adding controllers to the main project of the Blazor Web App for the first time, map controller endpoints when you place the preceding code that registers services for controllers. The following example uses default controller routes:
-
-```csharp
-app.MapDefaultControllerRoute();
-```
-
-> [!NOTE]
-> The preceding example explicitly registers controller services by calling to automatically [mitigate Cross-Site Request Forgery (XSRF/CSRF) attacks](xref:security/anti-request-forgery). If you merely use , antiforgery isn't enabled automatically.
-
-For more information on controller routing and validation failure error responses, see the following resources:
-
-*
-*
-
-In the `.Client` project, add the `CustomValidation` component shown in the [Validator components](#validator-components) section. Update the namespace to match the app (for example, `namespace BlazorSample.Client`).
-
-In the `.Client` project, the `Starfleet Starship Database` form is updated to show validation errors with help of the `CustomValidation` component. When validation messages are returned, they're added to the `CustomValidation` component's . The errors are available in the form's for display by the form's validation summary.
-
-In the following component, update the namespace of the shared project (`@using BlazorSample.Shared`) to the shared project's namespace. Note that the form requires authorization, so the user must be signed into the app to navigate to the form.
-
-`Starship10.razor`:
-
-> [!NOTE]
-> Forms based on automatically enable [antiforgery support](xref:blazor/forms/index#antiforgery-support). The controller should use to register controller services and automatically enable antiforgery support for the web API.
-
-```razor
-@page "/starship-10"
-@rendermode InteractiveWebAssembly
-@using System.Net
-@using System.Net.Http.Json
-@using Microsoft.AspNetCore.Authorization
-@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
-@using BlazorSample.Shared
-@attribute [Authorize]
-@inject HttpClient Http
-@inject ILogger Logger
-
-
Starfleet Starship Database
-
-
New Ship Entry Form
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- @message
-
-
-
-@code {
- private CustomValidation? customValidation;
- private bool disabled;
- private string? message;
- private string messageStyles = "visibility:hidden";
-
- [SupplyParameterFromForm]
- private Starship? Model { get; set; }
-
- protected override void OnInitialized() =>
- Model ??= new() { ProductionDate = DateTime.UtcNow };
-
- private async Task Submit(EditContext editContext)
- {
- customValidation?.ClearErrors();
-
- try
- {
- using var response = await Http.PostAsJsonAsync(
- "StarshipValidation", (Starship)editContext.Model);
-
- var errors = await response.Content
- .ReadFromJsonAsync>>() ??
- new Dictionary>();
-
- if (response.StatusCode == HttpStatusCode.BadRequest &&
- errors.Any())
- {
- customValidation?.DisplayErrors(errors);
- }
- else if (!response.IsSuccessStatusCode)
- {
- throw new HttpRequestException(
- $"Validation failed. Status Code: {response.StatusCode}");
- }
- else
- {
- disabled = true;
- messageStyles = "color:green";
- message = "The form has been processed.";
- }
- }
- catch (AccessTokenNotAvailableException ex)
- {
- ex.Redirect();
- }
- catch (Exception ex)
- {
- Logger.LogError("Form processing error: {Message}", ex.Message);
- disabled = true;
- messageStyles = "color:red";
- message = "There was an error processing the form.";
- }
- }
-}
-```
-
-The `.Client` project of a Blazor Web App must also register an for HTTP POST requests to a backend web API controller. Confirm or add the following to the `.Client` project's `Program` file:
-
-```csharp
-builder.Services.AddScoped(sp =>
- new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
-```
-
-The preceding example sets the base address with `builder.HostEnvironment.BaseAddress` (), which gets the base address for the app and is typically derived from the `` tag's `href` value in the host page.
-
-> [!NOTE]
-> As an alternative to the use of a [validation component](#validator-components), custom data annotation validation attributes can be used. Custom attributes applied to the form's model activate with the use of the component. For more information, see the [Custom validation attributes](#custom-validation-attributes) section.
-
-:::moniker-end
-
-:::moniker range="< aspnetcore-8.0"
-
-*This section is focused on hosted Blazor WebAssembly scenarios, but the approach for any type of app that uses server-side validation with web API adopts the same general approach.*
-
-Remote validation is supported in addition to server-side validation in a hosted Blazor WebAssembly app:
-
-* Process client validation in the form with the component.
-* When the form passes client validation ( is called), send the to a backend server API for form processing.
-* Process model validation on the server.
-* The server API includes both the built-in framework data annotations validation and custom validation logic supplied by the developer. If validation passes on the server, process the form and send back a success status code ([`200 - OK`](https://developer.mozilla.org/docs/Web/HTTP/Status/200)). If validation fails, return a failure status code ([`400 - Bad Request`](https://developer.mozilla.org/docs/Web/HTTP/Status/400)) and the field validation errors.
-* Either disable the form on success or display the errors.
-
-Basic validation is useful in cases where the form's model is defined within the component hosting the form, either as members directly on the component or in a subclass. Use of a validator component is recommended where an independent model class is used across several components.
-
-The following example is based on:
-
-* A hosted Blazor WebAssembly [solution](xref:blazor/tooling#visual-studio-solution-file-sln) created from the [Blazor WebAssembly project template](xref:blazor/project-structure). The approach is supported for any of the secure hosted Blazor solutions described in the [hosted Blazor WebAssembly security documentation](xref:blazor/security/webassembly/index#implementation-guidance).
-* The `Starship` model (`Starship.cs`) of the [Example form](xref:blazor/forms/input-components#example-form) section of the *Input components* article.
-* The `CustomValidation` component shown in the [Validator components](#validator-components) section.
-
-Place the `Starship` model (`Starship.cs`) into the solution's **`Shared`** project so that both the client and server apps can use the model. Add or update the namespace to match the namespace of the shared app (for example, `namespace BlazorSample.Shared`). Since the model requires data annotations, add the [`System.ComponentModel.Annotations` package](https://www.nuget.org/packages/System.ComponentModel.Annotations) to the **`Shared`** project.
-
-[!INCLUDE[](~/includes/package-reference.md)]
-
-In the **:::no-loc text="Server":::** project, add a controller to process starship validation requests and return failed validation messages. Update the namespaces in the last `using` statement for the **`Shared`** project and the `namespace` for the controller class. In addition to client and server data annotations validation, the controller validates that a value is provided for the ship's description (`Description`) if the user selects the `Defense` ship classification (`Classification`).
-
-The validation for the `Defense` ship classification only occurs on the server in the controller because the upcoming form doesn't perform the same validation client-side when the form is submitted to the server. Remote validation is common in apps that require private business logic validation of user input on the server. For example, private information from data stored for a user might be required to validate user input. Private data obviously can't be sent to the client for client validation.
-
-> [!NOTE]
-> The `StarshipValidation` controller in this section uses Microsoft Identity 2.0. The Web API only accepts tokens for users that have the "`API.Access`" scope for this API. Additional customization is required if the API's scope name is different from `API.Access`.
->
-> For more information on security, see:
->
-> * (and the other articles in the Blazor *Security and Identity* node)
-> * [Microsoft identity platform documentation](/entra/identity-platform/)
-
-`Controllers/StarshipValidation.cs`:
-
-```csharp
-using Microsoft.AspNetCore.Authorization;
-using Microsoft.AspNetCore.Mvc;
-using BlazorSample.Shared;
-
-namespace BlazorSample.Server.Controllers;
-
-[Authorize]
-[ApiController]
-[Route("[controller]")]
-public class StarshipValidationController(
- ILogger logger)
- : ControllerBase
-{
- static readonly string[] scopeRequiredByApi = new[] { "API.Access" };
-
- [HttpPost]
- public async Task Post(Starship model)
- {
- HttpContext.VerifyUserHasAnyAcceptedScope(scopeRequiredByApi);
-
- try
- {
- if (model.Classification == "Defense" &&
- string.IsNullOrEmpty(model.Description))
- {
- ModelState.AddModelError(nameof(model.Description),
- "For a 'Defense' ship " +
- "classification, 'Description' is required.");
- }
- else
- {
- logger.LogInformation("Processing the form asynchronously");
-
- // async ...
-
- return Ok(ModelState);
- }
- }
- catch (Exception ex)
- {
- logger.LogError("Validation Error: {Message}", ex.Message);
- }
-
- return BadRequest(ModelState);
- }
-}
-```
-
-Confirm or update the namespace of the preceding controller (`BlazorSample.Server.Controllers`) to match the app's controllers' namespace.
-
-When a model binding validation error occurs on the server, an [`ApiController`](xref:web-api/index) () normally returns a [default bad request response](xref:web-api/index#default-badrequest-response) with a . The response contains more data than just the validation errors, as shown in the following example when all of the fields of the `Starfleet Starship Database` form aren't submitted and the form fails validation:
-
-```json
-{
- "title": "One or more validation errors occurred.",
- "status": 400,
- "errors": {
- "Id": [ "The Id field is required." ],
- "Classification": [ "The Classification field is required." ],
- "IsValidatedDesign": [ "This form disallows unapproved ships." ],
- "MaximumAccommodation": [ "Accommodation invalid (1-100000)." ]
- }
-}
-```
-
-> [!NOTE]
-> To demonstrate the preceding JSON response, you must either disable the form's client validation to permit empty field form submission or use a tool to send a request directly to the server API, such as [Firefox Browser Developer](https://www.mozilla.org/firefox/developer/).
-
-If the server API returns the preceding default JSON response, it's possible for the client to parse the response in developer code to obtain the children of the `errors` node for forms validation error processing. It's inconvenient to write developer code to parse the file. Parsing the JSON manually requires producing a [`Dictionary>`](xref:System.Collections.Generic.Dictionary%602) of errors after calling . Ideally, the server API should only return the validation errors, as the following example shows:
-
-```json
-{
- "Id": [ "The Id field is required." ],
- "Classification": [ "The Classification field is required." ],
- "IsValidatedDesign": [ "This form disallows unapproved ships." ],
- "MaximumAccommodation": [ "Accommodation invalid (1-100000)." ]
-}
-```
-
-To modify the server API's response to make it only return the validation errors, change the delegate that's invoked on actions that are annotated with in the `Program` file. For the API endpoint (`/StarshipValidation`), return a with the . For any other API endpoints, preserve the default behavior by returning the object result with a new .
-
-Add the namespace to the top of the `Program` file in the **:::no-loc text="Server":::** app:
-
-```csharp
-using Microsoft.AspNetCore.Mvc;
-```
-
-In the `Program` file, locate the extension method and add the following call to :
-
-```csharp
-builder.Services.AddControllersWithViews()
- .ConfigureApiBehaviorOptions(options =>
- {
- options.InvalidModelStateResponseFactory = context =>
- {
- if (context.HttpContext.Request.Path == "/StarshipValidation")
- {
- return new BadRequestObjectResult(context.ModelState);
- }
- else
- {
- return new BadRequestObjectResult(
- new ValidationProblemDetails(context.ModelState));
- }
- };
- });
-```
-
-> [!NOTE]
-> The preceding example explicitly registers controller services by calling to automatically [mitigate Cross-Site Request Forgery (XSRF/CSRF) attacks](xref:security/anti-request-forgery). If you merely use , antiforgery isn't enabled automatically.
-
-In the **:::no-loc text="Client":::** project, add the `CustomValidation` component shown in the [Validator components](#validator-components) section. Update the namespace to match the app (for example, `namespace BlazorSample.Client`).
-
-In the **:::no-loc text="Client":::** project, the `Starfleet Starship Database` form is updated to show validation errors with help of the `CustomValidation` component. When validation messages are returned, they're added to the `CustomValidation` component's . The errors are available in the form's for display by the form's validation summary.
-
-In the following component, update the namespace of the **`Shared`** project (`@using BlazorSample.Shared`) to the shared project's namespace. Note that the form requires authorization, so the user must be signed into the app to navigate to the form.
-
-`Starship10.razor`:
-
-```razor
-@page "/starship-10"
-@using System.Net
-@using System.Net.Http.Json
-@using Microsoft.AspNetCore.Authorization
-@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
-@using BlazorSample.Shared
-@attribute [Authorize]
-@inject HttpClient Http
-@inject ILogger Logger
-
-
Starfleet Starship Database
-
-
New Ship Entry Form
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- @message
-
-
-
-@code {
- private CustomValidation? customValidation;
- private bool disabled;
- private string? message;
- private string messageStyles = "visibility:hidden";
-
- public Starship? Model { get; set; }
-
- protected override void OnInitialized() =>
- Model ??= new() { ProductionDate = DateTime.UtcNow };
-
- private async Task Submit(EditContext editContext)
- {
- customValidation?.ClearErrors();
-
- try
- {
- using var response = await Http.PostAsJsonAsync(
- "StarshipValidation", (Starship)editContext.Model);
-
- var errors = await response.Content
- .ReadFromJsonAsync>>() ??
- new Dictionary>();
-
- if (response.StatusCode == HttpStatusCode.BadRequest &&
- errors.Any())
- {
- customValidation?.DisplayErrors(errors);
- }
- else if (!response.IsSuccessStatusCode)
- {
- throw new HttpRequestException(
- $"Validation failed. Status Code: {response.StatusCode}");
- }
- else
- {
- disabled = true;
- messageStyles = "color:green";
- message = "The form has been processed.";
- }
- }
- catch (AccessTokenNotAvailableException ex)
- {
- ex.Redirect();
- }
- catch (Exception ex)
- {
- Logger.LogError("Form processing error: {Message}", ex.Message);
- disabled = true;
- messageStyles = "color:red";
- message = "There was an error processing the form.";
- }
- }
-}
-```
-
-> [!NOTE]
-> As an alternative to the use of a [validation component](#validator-components), custom data annotation validation attributes can be used. Custom attributes applied to the form's model activate with the use of the component. For more information, see the [Custom validation attributes](#custom-validation-attributes) section.
-
-> [!NOTE]
-> The remote validation approach in this section is suitable for any of the hosted Blazor WebAssembly solution examples in this documentation set:
->
-> * [Microsoft Entra ID (ME-ID)](xref:blazor/security/webassembly/hosted-with-microsoft-entra-id)
-> * [Azure Active Directory (AAD) B2C](xref:blazor/security/webassembly/hosted-with-azure-active-directory-b2c)
-> * [Identity Server](xref:blazor/security/webassembly/hosted-with-identity-server)
-
-:::moniker-end
-
-## `InputText` based on the input event
-
-Use the component to create a custom component that uses the `oninput` event ([`input`](https://developer.mozilla.org/docs/Web/API/HTMLElement/input_event)) instead of the `onchange` event ([`change`](https://developer.mozilla.org/docs/Web/API/HTMLElement/change_event)). Use of the `input` event triggers field validation on each keystroke.
-
-The following `CustomInputText` component inherits the framework's `InputText` component and sets event binding to the `oninput` event ([`input`](https://developer.mozilla.org/docs/Web/API/HTMLElement/input_event)).
-
-`CustomInputText.razor`:
-
-:::code language="razor" source="~/../blazor-samples/8.0/BlazorSample_BlazorWebApp/Components/CustomInputText.razor":::
-
-The `CustomInputText` component can be used anywhere is used. The following component uses the shared `CustomInputText` component.
-
-`Starship11.razor`:
-
-:::moniker range=">= aspnetcore-9.0"
-
-:::code language="razor" source="~/../blazor-samples/9.0/BlazorSample_BlazorWebApp/Components/Pages/Starship11.razor":::
-
-:::moniker-end
-
-:::moniker range=">= aspnetcore-8.0 < aspnetcore-9.0"
-
-:::code language="razor" source="~/../blazor-samples/8.0/BlazorSample_BlazorWebApp/Components/Pages/Starship11.razor":::
-
-:::moniker-end
-
-:::moniker range="< aspnetcore-8.0"
-
-```razor
-@page "/starship-11"
-@using System.ComponentModel.DataAnnotations
-@inject ILogger Logger
-
-
-
-
-
-
-
-
-
- CurrentValue: @Model?.Id
-
-
-@code {
- public Starship? Model { get; set; }
-
- protected override void OnInitialized() => Model ??= new();
-
- private void Submit()
- {
- Logger.LogInformation("Submit called: Processing the form");
- }
-
- public class Starship
- {
- [Required]
- [StringLength(10, ErrorMessage = "Id is too long.")]
- public string? Id { get; set; }
- }
-}
-```
-
-
-
-:::moniker-end
-
-## Validation Summary and Validation Message components
-
-The component summarizes all validation messages, which is similar to the [Validation Summary Tag Helper](xref:mvc/views/working-with-forms#the-validation-summary-tag-helper):
-
-```razor
-
-```
-
-Output validation messages for a specific model with the `Model` parameter:
-
-```razor
-
-```
-
-The component displays validation messages for a specific field, which is similar to the [Validation Message Tag Helper](xref:mvc/views/working-with-forms#the-validation-message-tag-helper). Specify the field for validation with the attribute and a lambda expression naming the model property:
-
-```razor
-
-```
-
-The and components support arbitrary attributes. Any attribute that doesn't match a component parameter is added to the generated `
` or `
` element. If a class attribute is supplied, its value replaces the component's default CSS class.
-
-Control the style of validation messages in the app's stylesheet (`wwwroot/css/app.css` or `wwwroot/css/site.css`). The default `validation-message` class sets the text color of validation messages to red:
-
-```css
-.validation-message {
- color: red;
-}
-```
-
-:::moniker range=">= aspnetcore-8.0"
-
-## Determine if a form field is valid
-
-Use to determine if a field is valid without obtaining validation messages.
-
-❌ Supported, but not recommended:
-
-```csharp
-var isValid = !editContext.GetValidationMessages(fieldIdentifier).Any();
-```
-
-✔️ Recommended:
-
-```csharp
-var isValid = editContext.IsValid(fieldIdentifier);
-```
-
-:::moniker-end
-
-## Custom validation attributes
-
-To ensure that a validation result is correctly associated with a field when using a [custom validation attribute](xref:mvc/models/validation#custom-attributes), pass the validation context's when creating the .
-
-`CustomValidator.cs`:
-
-:::moniker range=">= aspnetcore-8.0"
-
-```csharp
-using System;
-using System.ComponentModel.DataAnnotations;
-
-public class CustomValidator : ValidationAttribute
-{
- protected override ValidationResult IsValid(object? value,
- ValidationContext validationContext)
- {
- ...
-
- return new ValidationResult("Validation message to user.",
- [ validationContext.MemberName! ]);
- }
-}
-```
-
-:::moniker-end
-
-:::moniker range=">= aspnetcore-6.0 < aspnetcore-8.0"
-
-```csharp
-using System;
-using System.ComponentModel.DataAnnotations;
-
-public class CustomValidator : ValidationAttribute
-{
- protected override ValidationResult IsValid(object? value,
- ValidationContext validationContext)
- {
- ...
-
- return new ValidationResult("Validation message to user.",
- new[] { validationContext.MemberName! });
- }
-}
-```
-
-:::moniker-end
-
-:::moniker range="< aspnetcore-6.0"
-
-```csharp
-using System;
-using System.ComponentModel.DataAnnotations;
-
-public class CustomValidator : ValidationAttribute
-{
- protected override ValidationResult IsValid(object value,
- ValidationContext validationContext)
- {
- ...
-
- return new ValidationResult("Validation message to user.",
- new[] { validationContext.MemberName });
- }
-}
-```
-
-:::moniker-end
-
-Inject services into custom validation attributes through the . The following example demonstrates a salad chef form that validates user input with dependency injection (DI).
-
-The `SaladChef` class indicates the approved starship ingredient list for a Ten Forward salad.
-
-`SaladChef.cs`:
-
-:::code language="csharp" source="~/../blazor-samples/8.0/BlazorSample_BlazorWebApp/SaladChef.cs":::
-
-Register `SaladChef` in the app's DI container in the `Program` file:
-
-```csharp
-builder.Services.AddTransient();
-```
-
-The `IsValid` method of the following `SaladChefValidatorAttribute` class obtains the `SaladChef` service from DI to check the user's input.
-
-`SaladChefValidatorAttribute.cs`:
-
-:::code language="csharp" source="~/../blazor-samples/8.0/BlazorSample_BlazorWebApp/SaladChefValidatorAttribute.cs":::
-
-The following component validates user input by applying the `SaladChefValidatorAttribute` (`[SaladChefValidator]`) to the salad ingredient string (`SaladIngredient`).
-
-`Starship12.razor`:
-
-:::moniker range=">= aspnetcore-9.0"
-
-:::code language="razor" source="~/../blazor-samples/9.0/BlazorSample_BlazorWebApp/Components/Pages/Starship12.razor":::
-
-:::moniker-end
-
-:::moniker range=">= aspnetcore-8.0 < aspnetcore-9.0"
-
-:::code language="razor" source="~/../blazor-samples/8.0/BlazorSample_BlazorWebApp/Components/Pages/Starship12.razor":::
-
-:::moniker-end
-
-:::moniker range="< aspnetcore-8.0"
-
-```razor
-@page "/starship-12"
-@inject SaladChef SaladChef
-
-
-
-
-
-
-
-
- @foreach (var message in context.GetValidationMessages())
- {
-
@message
- }
-
-
-
-@code {
- private string? saladToppers;
-
- [SaladChefValidator]
- public string? SaladIngredient { get; set; }
-
- protected override void OnInitialized() =>
- saladToppers ??= string.Join(", ", SaladChef.SaladToppers);
-}
-```
-
-:::moniker-end
-
-:::moniker range=">= aspnetcore-7.0"
-
-## Custom validation CSS class attributes
-
-Custom validation CSS class attributes are useful when integrating with CSS frameworks, such as [Bootstrap](https://getbootstrap.com/).
-
-To specify custom validation CSS class attributes, start by providing CSS styles for custom validation. In the following example, valid (`validField`) and invalid (`invalidField`) styles are specified.
-
-Add the following CSS classes to the app's stylesheet:
-
-```css
-.validField {
- border-color: lawngreen;
-}
-
-.invalidField {
- background-color: tomato;
-}
-```
-
-Create a class derived from that checks for field validation messages and applies the appropriate valid or invalid style.
-
-`CustomFieldClassProvider.cs`:
-
-:::moniker-end
-
-:::moniker range=">= aspnetcore-8.0"
-
-```csharp
-using Microsoft.AspNetCore.Components.Forms;
-
-public class CustomFieldClassProvider : FieldCssClassProvider
-{
- public override string GetFieldCssClass(EditContext editContext,
- in FieldIdentifier fieldIdentifier)
- {
- var isValid = editContext.IsValid(fieldIdentifier);
-
- return isValid ? "validField" : "invalidField";
- }
-}
-```
-
-:::moniker-end
-
-:::moniker range=">= aspnetcore-7.0 < aspnetcore-8.0"
-
-```csharp
-using Microsoft.AspNetCore.Components.Forms;
-
-public class CustomFieldClassProvider : FieldCssClassProvider
-{
- public override string GetFieldCssClass(EditContext editContext,
- in FieldIdentifier fieldIdentifier)
- {
- var isValid = !editContext.GetValidationMessages(fieldIdentifier).Any();
-
- return isValid ? "validField" : "invalidField";
- }
-}
-```
-
-:::moniker-end
-
-:::moniker range=">= aspnetcore-7.0"
-
-
-
-Set the `CustomFieldClassProvider` class as the Field CSS Class Provider on the form's instance with .
-
-`Starship13.razor`:
-
-:::moniker-end
-
-:::moniker range=">= aspnetcore-9.0"
-
-:::code language="razor" source="~/../blazor-samples/9.0/BlazorSample_BlazorWebApp/Components/Pages/Starship13.razor":::
-
-:::moniker-end
-
-:::moniker range=">= aspnetcore-8.0 < aspnetcore-9.0"
-
-:::code language="razor" source="~/../blazor-samples/8.0/BlazorSample_BlazorWebApp/Components/Pages/Starship13.razor":::
-
-:::moniker-end
-
-:::moniker range=">= aspnetcore-7.0 < aspnetcore-8.0"
-
-```razor
-@page "/starship-13"
-@using System.ComponentModel.DataAnnotations
-@inject ILogger Logger
-
-
-
-
-
-
-
-
-@code {
- private EditContext? editContext;
-
- public Starship? Model { get; set; }
-
- protected override void OnInitialized()
- {
- Model ??= new();
- editContext = new(Model);
- editContext.SetFieldCssClassProvider(new CustomFieldClassProvider());
- }
-
- private void Submit()
- {
- Logger.LogInformation("Submit called: Processing the form");
- }
-
- public class Starship
- {
- [Required]
- [StringLength(10, ErrorMessage = "Id is too long.")]
- public string? Id { get; set; }
- }
-}
-```
-
-
-
-:::moniker-end
-
-:::moniker range=">= aspnetcore-7.0"
-
-The preceding example checks the validity of all form fields and applies a style to each field. If the form should only apply custom styles to a subset of the fields, make `CustomFieldClassProvider` apply styles conditionally. The following `CustomFieldClassProvider2` example only applies a style to the `Name` field. For any fields with names not matching `Name`, `string.Empty` is returned, and no style is applied. Using [reflection](/dotnet/csharp/advanced-topics/reflection-and-attributes/), the field is matched to the model member's property or field name, not an `id` assigned to the HTML entity.
-
-`CustomFieldClassProvider2.cs`:
-
-:::moniker-end
-
-:::moniker range=">= aspnetcore-8.0"
-
-```csharp
-using Microsoft.AspNetCore.Components.Forms;
-
-public class CustomFieldClassProvider2 : FieldCssClassProvider
-{
- public override string GetFieldCssClass(EditContext editContext,
- in FieldIdentifier fieldIdentifier)
- {
- if (fieldIdentifier.FieldName == "Name")
- {
- var isValid = editContext.IsValid(fieldIdentifier);
-
- return isValid ? "validField" : "invalidField";
- }
-
- return string.Empty;
- }
-}
-```
-
-:::moniker-end
-
-:::moniker range=">= aspnetcore-7.0 < aspnetcore-8.0"
-
-```csharp
-using Microsoft.AspNetCore.Components.Forms;
-
-public class CustomFieldClassProvider2 : FieldCssClassProvider
-{
- public override string GetFieldCssClass(EditContext editContext,
- in FieldIdentifier fieldIdentifier)
- {
- if (fieldIdentifier.FieldName == "Name")
- {
- var isValid = !editContext.GetValidationMessages(fieldIdentifier).Any();
-
- return isValid ? "validField" : "invalidField";
- }
-
- return string.Empty;
- }
-}
-```
-
-:::moniker-end
-
-:::moniker range=">= aspnetcore-7.0"
-
-
-
-> [!NOTE]
-> Matching the field name in the preceding example is case sensitive, so a model property member designated "`Name`" must match a conditional check on "`Name`":
->
-> * ✔️Correctly matches: `fieldId.FieldName == "Name"`
-> * ❌Fails to match: `fieldId.FieldName == "name"`
-> * ❌Fails to match: `fieldId.FieldName == "NAME"`
-> * ❌Fails to match: `fieldId.FieldName == "nAmE"`
-
-Add an additional property to `Model`, for example:
-
-```csharp
-[StringLength(10, ErrorMessage = "Description is too long.")]
-public string? Description { get; set; }
-```
-
-Add the `Description` to the `CustomValidationForm` component's form:
-
-```razor
-
-```
-
-Update the instance in the component's `OnInitialized` method to use the new Field CSS Class Provider:
-
-```csharp
-editContext?.SetFieldCssClassProvider(new CustomFieldClassProvider2());
-```
-
-Because a CSS validation class isn't applied to the `Description` field, it isn't styled. However, field validation runs normally. If more than 10 characters are provided, the validation summary indicates the error:
-
-> Description is too long.
-
-In the following example:
-
-* The custom CSS style is applied to the `Name` field.
-* Any other fields apply logic similar to Blazor's default logic and using Blazor's default field CSS validation styles, `modified` with `valid` or `invalid`. Note that for the default styles, you don't need to add them to the app's stylesheet if the app is based on a Blazor project template. For apps not based on a Blazor project template, the default styles can be added to the app's stylesheet:
-
- ```css
- .valid.modified:not([type=checkbox]) {
- outline: 1px solid #26b050;
- }
-
- .invalid {
- outline: 1px solid red;
- }
- ```
-
-`CustomFieldClassProvider3.cs`:
-
-:::moniker-end
-
-:::moniker range=">= aspnetcore-8.0"
-
-```csharp
-using Microsoft.AspNetCore.Components.Forms;
-
-public class CustomFieldClassProvider3 : FieldCssClassProvider
-{
- public override string GetFieldCssClass(EditContext editContext,
- in FieldIdentifier fieldIdentifier)
- {
- var isValid = editContext.IsValid(fieldIdentifier);
-
- if (fieldIdentifier.FieldName == "Name")
- {
- return isValid ? "validField" : "invalidField";
- }
- else
- {
- if (editContext.IsModified(fieldIdentifier))
- {
- return isValid ? "modified valid" : "modified invalid";
- }
- else
- {
- return isValid ? "valid" : "invalid";
- }
- }
- }
-}
-```
-
-:::moniker-end
-
-:::moniker range=">= aspnetcore-7.0 < aspnetcore-8.0"
-
-```csharp
-using Microsoft.AspNetCore.Components.Forms;
-
-public class CustomFieldClassProvider3 : FieldCssClassProvider
-{
- public override string GetFieldCssClass(EditContext editContext,
- in FieldIdentifier fieldIdentifier)
- {
- var isValid = !editContext.GetValidationMessages(fieldIdentifier).Any();
-
- if (fieldIdentifier.FieldName == "Name")
- {
- return isValid ? "validField" : "invalidField";
- }
- else
- {
- if (editContext.IsModified(fieldIdentifier))
- {
- return isValid ? "modified valid" : "modified invalid";
- }
- else
- {
- return isValid ? "valid" : "invalid";
- }
- }
- }
-}
-```
-
-:::moniker-end
-
-:::moniker range=">= aspnetcore-7.0"
-
-
-
-Update the instance in the component's `OnInitialized` method to use the preceding Field CSS Class Provider:
-
-```csharp
-editContext.SetFieldCssClassProvider(new CustomFieldClassProvider3());
-```
-
-Using `CustomFieldClassProvider3`:
-
-* The `Name` field uses the app's custom validation CSS styles.
-* The `Description` field uses logic similar to Blazor's logic and Blazor's default field CSS validation styles.
-
-:::moniker-end
-
-## Class-level validation with `IValidatableObject`
-
-[Class-level validation with `IValidatableObject`](xref:mvc/models/validation#ivalidatableobject) ([API documentation](xref:System.ComponentModel.DataAnnotations.IValidatableObject)) is supported for Blazor form models. validation only executes when the form is submitted and only if all other validation succeeds.
-
-:::moniker range="< aspnetcore-10.0"
-
-## Blazor data annotations validation package
-
-> [!NOTE]
-> The [`Microsoft.AspNetCore.Components.DataAnnotations.Validation` package](https://www.nuget.org/packages/Microsoft.AspNetCore.Components.DataAnnotations.Validation) is no longer recommended for apps that target .NET 10 or later. For more information, see the [Nested objects, collection types, and complex types](#nested-objects-collection-types-and-complex-types) section.
-
-The [`Microsoft.AspNetCore.Components.DataAnnotations.Validation` package](https://www.nuget.org/packages/Microsoft.AspNetCore.Components.DataAnnotations.Validation) fills validation experience gaps using the component. The package is currently *experimental*.
-
-> [!WARNING]
-> The [`Microsoft.AspNetCore.Components.DataAnnotations.Validation` package](https://www.nuget.org/packages/Microsoft.AspNetCore.Components.DataAnnotations.Validation) has a latest version of *release candidate* at [NuGet.org](https://www.nuget.org/packages/Microsoft.AspNetCore.Components.DataAnnotations.Validation). Continue to use the *experimental* release candidate package at this time. Experimental features are provided for the purpose of exploring feature viability and may not ship in a stable version. Watch the [Announcements GitHub repository](https://github.com/aspnet/Announcements), the [`dotnet/aspnetcore` GitHub repository](https://github.com/dotnet/aspnetcore), or this topic section for further updates.
-
-:::moniker-end
-
-:::moniker range="< aspnetcore-6.0"
-
-## `[CompareProperty]` attribute
-
-The doesn't work well with the component because the doesn't associate the validation result with a specific member. This can result in inconsistent behavior between field-level validation and when the entire model is validated on a submit. The [`Microsoft.AspNetCore.Components.DataAnnotations.Validation` *experimental* package](https://www.nuget.org/packages/Microsoft.AspNetCore.Components.DataAnnotations.Validation) introduces an additional validation attribute, `ComparePropertyAttribute`, that works around these limitations. In a Blazor app, `[CompareProperty]` is a direct replacement for the [`[Compare]` attribute](xref:System.ComponentModel.DataAnnotations.CompareAttribute).
-
-:::moniker-end
-
-:::moniker range=">= aspnetcore-10.0"
-
-## Nested objects and collection types
-
-Blazor form validation includes support for validating properties of nested objects and collection items with the built-in .
-
-To create a validated form, use a component inside an component.
-
-To opt into the nested objects and collection types validation feature:
-
-1. Call the extension method in the `Program` file where services are registered.
-2. Declare the form model types in a C# class file, not in a Razor component (`.razor`).
-3. Annotate the root form model type with the [`[ValidatableType]` attribute](xref:Microsoft.Extensions.Validation.ValidatableTypeAttribute), which indicates that a type is validatable to support discovery by the validation source generator.
-
-The following example demonstrates customer orders with nested collection form validation.
-
-In `Program.cs`, call on the service collection:
-
-```csharp
-builder.Services.AddValidation();
-```
-
-In the following `Order` class, the `[ValidatableType]` attribute is required on the top-level model type. The other types are discovered automatically.
-
-`Order.cs`:
-
-```csharp
-using System.ComponentModel.DataAnnotations;
-
-[ValidatableType]
-public class Order
-{
- public Customer Customer { get; set; } = new();
- public List OrderItems { get; set; } = [];
-}
-
-public class Customer
-{
- [Required(ErrorMessage = "Name is required.")]
- public string? FullName { get; set; }
-
- [Required(ErrorMessage = "Email is required.")]
- public string? Email { get; set; }
-
- public ShippingAddress ShippingAddress { get; set; } = new();
-}
-```
-
-`OrderItem.cs`:
-
-```csharp
-public class OrderItem
-{
- [Required(ErrorMessage = "Id is required.")]
- public int Id { get; set; }
-
- [Required(ErrorMessage = "Description is required.")]
- public string? Description { get; set; }
-
- [Required(ErrorMessage = "Price is required.")]
- public decimal Price { get; set; }
-}
-```
-
-`ShippingAddress.cs`:
-
-```csharp
-public class ShippingAddress
-{
- [Required(ErrorMessage = "Street is required.")]
- public string? Street { get; set; }
-
- [Required(ErrorMessage = "City is required.")]
- public string? City { get; set; }
-
- [Required(ErrorMessage = "State/Province is required.")]
- public string? StateProvince { get; set; }
-
- [Required(ErrorMessage = "PostalCode is required.")]
- public string? PostalCode { get; set; }
-}
-```
-
-In the following `OrderPage` component, the component is present in the component.
-
-`OrderPage.razor`:
+For interactive forms, the form-level pending state can be used to disable submission while `ValidateAsync` is running:
```razor
-
-
-
-
Customer Details
-
-
-
-
-
- // ... form continues ...
-
-
-@code {
- public Order? Model { get; set; }
-
- protected override void OnInitialized() => Model ??= new();
-}
+
```
-The requirement to declare the model types outside of Razor components (`.razor` files) is due to the fact that both the nested collection validation feature and the Razor compiler itself are using a source generator. Currently, output of one source generator can't be used as an input for another source generator.
-
-For guidance on using validation models from a different assembly, such as a library or the `.Client` project of a Blazor Web App, see .
-
:::moniker-end
-:::moniker range="< aspnetcore-10.0"
-
-## Nested objects, collection types, and complex types
+:::moniker range="< aspnetcore-11.0"
-> [!NOTE]
-> For apps targeting .NET 10 or later, we no longer recommend using the [`Microsoft.AspNetCore.Components.DataAnnotations.Validation` *experimental* package](https://www.nuget.org/packages/Microsoft.AspNetCore.Components.DataAnnotations.Validation) and approach described in this section. We recommend using the built-in validation features of the component.
-
-Blazor provides support for validating form input using data annotations with the built-in . However, the in .NET 9 or earlier only validates top-level properties of the model bound to the form that aren't collection- or complex-type properties.
-
-To validate the bound model's entire object graph, including collection- and complex-type properties, use the `ObjectGraphDataAnnotationsValidator` provided by the *experimental* [`Microsoft.AspNetCore.Components.DataAnnotations.Validation` package](https://www.nuget.org/packages/Microsoft.AspNetCore.Components.DataAnnotations.Validation) in .NET 9 or earlier:
+When handling `OnSubmit`, call