From e8ce7bd1d412d8ab3cc993bd66852a7a754028cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Rozto=C4=8Dil?= Date: Mon, 17 Aug 2026 15:12:23 +0200 Subject: [PATCH 01/11] Refactor existing validation docs, add .NET 11 validation features coverage --- .../blazor/components/component-disposal.md | 2 +- aspnetcore/blazor/forms/binding.md | 69 + aspnetcore/blazor/forms/index.md | 8 +- .../blazor/forms/validation-advanced.md | 1703 ++++++++++++ .../blazor/forms/validation-client-side.md | 283 ++ aspnetcore/blazor/forms/validation.md | 2378 +++-------------- .../blazor/globalization-localization.md | 2 +- .../localization/make-content-localizable.md | 88 +- aspnetcore/fundamentals/minimal-apis.md | 14 +- aspnetcore/fundamentals/validation.md | 558 +++- .../aspnetcore-11/includes/blazor.md | 71 +- .../includes/validation-localization.md | 18 +- aspnetcore/toc.yml | 4 + 13 files changed, 2868 insertions(+), 2330 deletions(-) create mode 100644 aspnetcore/blazor/forms/validation-advanced.md create mode 100644 aspnetcore/blazor/forms/validation-client-side.md 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..950c160b67b6 100644 --- a/aspnetcore/blazor/forms/binding.md +++ b/aspnetcore/blazor/forms/binding.md @@ -403,6 +403,75 @@ 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..b466854625d7 100644 --- a/aspnetcore/blazor/forms/index.md +++ b/aspnetcore/blazor/forms/index.md @@ -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..0bb2879c88bb --- /dev/null +++ b/aspnetcore/blazor/forms/validation-advanced.md @@ -0,0 +1,1703 @@ +--- +title: ASP.NET Core Blazor advanced form validation +ai-usage: ai-assisted +author: guardrex +description: Learn how to control Blazor form validation directly with EditContext, validator components, and remote validation. +monikerRange: '>= aspnetcore-3.1' +ms.author: wpickett +ms.date: 08/17/2026 +uid: blazor/forms/validation-advanced +--- +# ASP.NET Core Blazor advanced form validation + +[!INCLUDE[](~/includes/not-latest-version.md)] + +This article explains how to take direct control of Blazor form validation using , validator components, and remote validation. + +The techniques in this article are for scenarios that validation attributes on the model can't express, most commonly when validation messages come from outside the model, such as a web API response or a business rule that requires server-side data. + +For validation with data annotations attributes and the component, see . Writing custom rules as attributes on the model is simpler than the approaches in this article. For more information, see [Custom attributes](xref:mvc/models/validation#custom-attributes). + +## Validate with `EditContext` and `ValidationMessageStore` + +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. + +This approach 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 range=">= aspnetcore-8.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. + +:::moniker-end + +:::moniker range=">= aspnetcore-11.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 . + +:::moniker-end + +In the following component, the `HandleValidationRequested` handler method clears any existing validation messages by calling before validating the form. + +`Starship8.razor`: + +:::moniker range=">= aspnetcore-9.0" + +:::code language="razor" source="~/../blazor-samples/9.0/BlazorSample_BlazorWebApp/Components/Pages/Starship8.razor"::: + +:::moniker-end + +:::moniker range=">= aspnetcore-8.0 < aspnetcore-9.0" + +:::code language="razor" source="~/../blazor-samples/8.0/BlazorSample_BlazorWebApp/Components/Pages/Starship8.razor"::: + +:::moniker-end + +:::moniker range="< aspnetcore-8.0" + +```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 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; + } + } +} +``` + + + +:::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). + +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. + +Reference the namespace in the component's Razor directives at the top of the component definition file: + +```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; } +``` + +Add a component (``) to the form. Typically, the component is placed immediately under the `` tag, but you can place it anywhere in the form: + +```razor + +``` + +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: + +```diff +- OnSubmit="Submit" ++ OnValidSubmit="Submit" +``` + +In the ``, add a field for the `Id` property: + +```razor +
+ + +
+``` + +After making the preceding changes, the form's behavior matches the following specification: + +* 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. + +## Validator components + +Validator components support form validation by managing a for a form's . + +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: + +* [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) + +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. + +> [!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 . + +Create a validator component from : + +* 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. + +Update the namespace in the following class to match your app's namespace. + +`CustomValidation.cs`: + +:::code language="csharp" source="~/../blazor-samples/8.0/BlazorSample_BlazorWebApp/CustomValidation.cs"::: + +> [!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`. + +> [!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 . + +## Business logic validation with a validator component + +For general business logic validation, use a [validator component](#validator-components) that receives form errors in a dictionary. + +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. + +In the following example: + +* 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`). + +When validation messages are set in the component, they're added to the validator's and shown in the 's validation summary. + +`Starship9.razor`: + +:::moniker range=">= aspnetcore-9.0" + +:::code language="razor" source="~/../blazor-samples/9.0/BlazorSample_BlazorWebApp/Components/Pages/Starship9.razor"::: + +:::moniker-end + +:::moniker range=">= aspnetcore-8.0 < aspnetcore-9.0" + +:::code language="razor" source="~/../blazor-samples/8.0/BlazorSample_BlazorWebApp/Components/Pages/Starship9.razor"::: + +:::moniker-end + +:::moniker range="< aspnetcore-8.0" + +```razor +@page "/starship-9" +@inject ILogger Logger + +

Starfleet Starship Database

+ +

New Ship Entry Form

+ + + + +
+ +
+
+ +
+
+ +
+
+ +@code { + private CustomValidation? customValidation; + + public Starship? Model { get; set; } + + protected override void OnInitialized() => + Model ??= new() { ProductionDate = DateTime.UtcNow }; + + private void Submit() + { + customValidation?.ClearErrors(); + + var errors = new Dictionary>(); + + if (Model!.Classification == "Defense" && + string.IsNullOrEmpty(Model.Description)) + { + errors.Add(nameof(Model.Description), + new() { "For a 'Defense' ship classification, " + + "'Description' is required." }); + } + + if (errors.Any()) + { + customValidation?.DisplayErrors(errors); + } + else + { + Logger.LogInformation("Submit called: Processing the form"); + } + } +} +``` + + + +:::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 . + +:::moniker range=">= aspnetcore-11.0" + +## Asynchronous validation + + exposes an asynchronous validation pipeline that custom validator components and custom submit handlers 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: + +* `ValidationRequestedEventArgs.AddAsyncValidator`: registers asynchronous work to run as part of the current validation pass. 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. +* `EditContext.ValidateAsync`: an asynchronous counterpart to that invokes the registered validators and awaits them. It accepts a . + + awaits any registered asynchronous work before invoking . Forms with only synchronous validators continue to work without changes. + +To author asynchronous rules as data annotations attributes on the model instead of writing a validator component, see . The built-in component runs asynchronous attributes without any additional configuration. + +> [!IMPORTANT] +> Asynchronous work can only be registered during an asynchronous validation pass. If a form is validated with the obsolete synchronous method, `AddAsyncValidator` throws an that directs the caller to `ValidateAsync`. This guarantees that an asynchronous validator is never silently skipped. + +### Form-level async validation + +Subscribe to and call `AddAsyncValidator` from the handler to run asynchronous 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. + +In the following example, a custom validator component checks a username against a remote endpoint when the form is submitted: + +```razor +@implements IDisposable +@inject HttpClient Http + +@code { + [CascadingParameter] + private EditContext? CurrentEditContext { get; set; } + + [Parameter, EditorRequired] + public RegistrationModel Model { get; set; } = default!; + + private ValidationMessageStore? _messages; + + protected override void OnInitialized() + { + ArgumentNullException.ThrowIfNull(CurrentEditContext); + _messages = new ValidationMessageStore(CurrentEditContext); + CurrentEditContext.OnValidationRequested += OnValidationRequested; + } + + private void OnValidationRequested( + object? sender, ValidationRequestedEventArgs e) => + e.AddAsyncValidator(ValidateUsernameAsync); + + private async Task ValidateUsernameAsync(CancellationToken token) + { + var field = CurrentEditContext!.Field(nameof(Model.Username)); + _messages!.Clear(field); + + var available = await Http.GetFromJsonAsync( + $"api/usernames/available?value={Uri.EscapeDataString(Model.Username)}", + token); + + if (!available) + { + _messages.Add(field, "The username is already taken."); + } + + CurrentEditContext!.NotifyValidationStateChanged(); + } + + public void Dispose() + { + if (CurrentEditContext is not null) + { + CurrentEditContext.OnValidationRequested -= OnValidationRequested; + } + } +} +``` + +Place the component inside an alongside the form's inputs. Because awaits the asynchronous validators before invoking , the submit handler runs only after the remote check completes successfully: + +```razor + + + + + + +``` + +### Per-field async validation + +For asynchronous 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 that the field's pending and faulted state can be queried and displayed independently of other fields. + +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. + +Add the following members to the validator component shown in the previous section to re-run the uniqueness check whenever the `Username` field changes: + +```csharp +protected override void OnInitialized() +{ + ArgumentNullException.ThrowIfNull(CurrentEditContext); + _messages = new ValidationMessageStore(CurrentEditContext); + CurrentEditContext.OnValidationRequested += OnValidationRequested; + CurrentEditContext.OnFieldChanged += OnFieldChanged; +} + +private void OnFieldChanged(object? sender, FieldChangedEventArgs e) +{ + if (e.FieldIdentifier.FieldName != nameof(RegistrationModel.Username)) + { + return; + } + + CurrentEditContext!.RegisterAsyncFieldValidator( + e.FieldIdentifier, + token => CheckAsync(e.FieldIdentifier, token)); +} + +private async Task CheckAsync(FieldIdentifier field, CancellationToken token) +{ + _messages!.Clear(field); + + var available = await Http.GetFromJsonAsync( + $"api/usernames/available?value={Uri.EscapeDataString(Model.Username)}", + token); + + if (!available) + { + _messages.Add(field, "The username is already taken."); + } + + CurrentEditContext!.NotifyValidationStateChanged(); +} + +public void Dispose() +{ + if (CurrentEditContext is not null) + { + CurrentEditContext.OnValidationRequested -= OnValidationRequested; + CurrentEditContext.OnFieldChanged -= OnFieldChanged; + } +} +``` + +Write the validator as an `async` method so that an exception thrown before the first `await` is captured into the returned task rather than thrown from `RegisterAsyncFieldValidator`. To cancel from an additional source, create a linked token source inside the validator with . + +Validators should clear prior messages for the field up front, as the preceding example does, and avoid writing partial results on a path that might throw. + +### Cancellation and faults + +A validation that's canceled because it was superseded, or because the caller's token was canceled, is discarded silently and doesn't change the field's faulted state. + +A validation that fails for any other reason places the field in the *faulted* state. This includes a validation that completes as canceled due to an unrelated source, such as an or database timeout. Such a cancellation is treated as an infrastructure fault rather than as success, so a field is never reported as valid because its validation didn't finish. + +For how to display pending and faulted state in the UI, see . + +### Calling `ValidateAsync` from a custom submit handler + +When a form uses instead of , call `ValidateAsync` from the handler to await any registered asynchronous work before deciding whether to proceed: + +```razor + + + + + + +@code { + private EditContext _editContext = default!; + + protected override void OnInitialized() => + _editContext = new EditContext(Model); + + private async Task HandleSubmitAsync() + { + if (await _editContext.ValidateAsync(CancellationToken.None)) + { + await RegisterAsync(); + } + } +} +``` + +The synchronous method is obsolete as of .NET 11. Call `ValidateAsync` instead. `Validate` continues to work for forms that only have synchronous validators, but it throws an if a handler attempts to register asynchronous work during the pass. + +### Async validation across rendering modes + +The asynchronous 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 asynchronous validation completes. + +:::moniker-end + +:::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 . + +To reach the form easily, add the following entry to the `NavMenu` component (`Layout/NavMenu.razor`) in the `.Client` project: + +```razor + +``` + +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 . + +:::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 . + +> [!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 + +## Additional resources + +* +* +* + +:::moniker range=">= aspnetcore-10.0" + +* + +:::moniker-end + +:::moniker range=">= aspnetcore-11.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..f8af1e2af082 --- /dev/null +++ b/aspnetcore/blazor/forms/validation-client-side.md @@ -0,0 +1,283 @@ +--- +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: 08/17/2026 +uid: blazor/forms/validation-client-side +--- +# ASP.NET Core Blazor client-side form validation in static SSR + +[!INCLUDE[](~/includes/not-latest-version.md)] + +This article explains how Blazor validates forms in the browser when the form uses [static server-side rendering (static SSR)](xref:blazor/components/render-modes#static-server-side-rendering-static-ssr). + +Forms that use an interactive render mode validate through the live pipeline and don't use the feature described in this article. For validation that applies to every render mode, see . + +## 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 in the browser before the form is submitted. The user sees validation errors without a round trip to the server. + +Client-side validation activates automatically when both of 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. + +No JavaScript configuration, additional package, or service registration is required. + +> [!IMPORTANT] +> Client-side validation is a user experience improvement, not a security boundary. It can be bypassed by disabling or modifying the browser's JavaScript. Server-side validation runs after the form is posted and remains authoritative. Never rely on client-side validation to protect data integrity. + +### The .NET model remains the source of truth + +Validation rules aren't authored separately for the client. The server derives them from the data annotations attributes on the form's model and renders them into the page, so the client-side rules can't drift from the server-side rules. + +The rules are carried in a single inert custom element that Blazor appends to the form: + +```html + +``` + +Because the payload is held in an attribute rather than as element content, the element renders nothing and needs no CSS to remain hidden. + +> [!NOTE] +> Although the carrier element is invisible, it's a real element in the DOM and is a child of the form. CSS selectors that depend on element position, such as `:last-child`, `:nth-child()`, and adjacent sibling combinators (`+`), can match differently in a form that has client-side validation enabled. + +## Fields that receive client-side rules + +Client-side rules are only emitted for fields that the server also validates when the form is submitted. A field that the server ignores never receives a client-side rule. + +This matters for models with nested objects and collections. Validating nested members requires , so: + +* When the app calls and the model is discovered, nested members are validated on the server and receive client-side rules. +* Otherwise, only top-level properties are validated on the server, so only top-level properties receive client-side rules. + +Adopting therefore changes which fields are validated in the browser. For more information, see . + +The rule prevents client-side validation from suggesting coverage that the authoritative server-side pass doesn't provide, which would give a false sense of security. + +## 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. 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. + +> [!NOTE] +> A with a non-numeric operand type, such as a date range, doesn't produce a client-side rule. The range is still enforced server-side. + +The and client-side validators intentionally accept the same input as their .NET counterparts rather than applying stricter rules. Apps that require stricter checks can register a custom validator. + +## Validation timing + +A field is validated when its value is committed, which for text inputs occurs when the field loses focus and for checkboxes and dropdown lists occurs immediately on 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 and accessibility + +The and components display client-side validation errors without any changes. + +ARIA attributes on input elements and on 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, so one stylesheet covers both: + +| 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` | + +Because the class names match the interactive render modes, the stylesheet included in the Blazor project templates styles static SSR validation and interactive validation identically with no additional configuration. + +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. + +## Enhanced navigation + +Client-side validation is preserved across [enhanced navigation](xref:blazor/fundamentals/navigation#enhanced-navigation-and-form-handling). When a user navigates to a page that contains a static SSR form, the form is wired up automatically, including when the page update replaces one form with another. Multiple forms on the same page validate independently of each other. + +## Streaming rendering + +Inputs added to a form by a later [streaming rendering](xref:blazor/components/rendering#streaming-rendering) update aren't covered by client-side validation. They're still validated on the server when the form is submitted. + +A form that's delivered in a single streamed batch is covered normally. This limitation only applies when inputs are added to a form that has already rendered. + +## Opt out of client-side validation + +Server-side validation is unaffected by every option in this section. Only the in-browser check is disabled. + +### 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`, no form emits client-side validation rules, and a form can't opt back in with `DisableClientValidation="false"` on its component. + +### Opt out for a single submit button + +Use the standard HTML `formnovalidate` attribute on the button. The form is posted without a client-side check, and server-side validation still runs after the post: + +```razor + +``` + +This is useful for a "save draft" or "back" button that shouldn't require a completely valid form. + +## Localized validation messages + +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 . + +## Custom client-side validation rules + +A custom validation attribute isn't enforced in the browser by default because the framework has no way to execute arbitrary .NET validation logic on the client. To enforce a custom rule client-side, supply the rule on the server and register a matching validator function on the client. Both halves are required: a rule with no matching validator has no effect, and a validator with no matching rule is never called. + +### Emit a rule from a validation attribute + +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 framework attaches each rule's resolved error message, including the localized message when localization is configured, so the attribute supplies only the rule's shape. + +The following `StartsWithAttribute` validates server-side in `IsValid` and contributes a `startsWith` client-side rule with a `prefix` parameter: + +```csharp +using System.ComponentModel.DataAnnotations; +using Microsoft.AspNetCore.Components.Forms; + +public sealed class StartsWithAttribute : ValidationAttribute, IClientValidationRuleProvider +{ + private readonly string prefix; + + public StartsWithAttribute(string prefix) + { + this.prefix = prefix; + ErrorMessage = $"The value must start with '{prefix}'."; + } + + protected override ValidationResult? IsValid(object? value, + ValidationContext validationContext) + { + if (value is string text && !text.StartsWith(prefix, StringComparison.Ordinal)) + { + return new ValidationResult(ErrorMessage, [ validationContext.MemberName! ]); + } + + return ValidationResult.Success; + } + + public IEnumerable GetClientValidationRules() + { + yield return new ClientValidationRule( + "startswith", + new Dictionary { ["prefix"] = prefix }); + } +} +``` + +Apply the attribute to the model in the usual way: + +```csharp +public class ShipModel +{ + [StartsWith("NCC-")] + public string? Registry { get; set; } +} +``` + +### Register the matching client-side validator + +Register a validator function with the same rule name using `addValidator`. + +The `Blazor.formValidation` service is created while Blazor starts, so it isn't available to script that runs before start-up completes. Register the validator from a [JavaScript initializer](xref:blazor/fundamentals/startup#javascript-initializers), which receives the `Blazor` instance after start-up. + +In a JavaScript initializer file named `{APP NAMESPACE}.lib.module.js` placed in the app's `wwwroot` folder, where the `{APP NAMESPACE}` placeholder is the app's namespace: + +```javascript +export function afterWebStarted(blazor) { + blazor.formValidation.addValidator('startswith', (context) => { + const value = context.value; + + // An empty value is valid. Use [Required] to require a value. + if (!value) { + return { success: true }; + } + + return { success: value.startsWith(context.params.prefix) }; + }); +} +``` + +Rule names are matched exactly, so the name passed to `addValidator` must match the `ClientValidationRule` `Name` value, including casing. + +Registering the validator after start-up is sufficient even for a form that's already on the page. The rule is already present in the rendered metadata, and the engine resolves the validator function by name when validation runs. + +The validator receives a context object with the following members: + +| Member | Description | +|---|---| +| `value` | The field's current value as a string, or `null`/`undefined` when there's no value. | +| `element` | The `input`, `select`, or `textarea` element being validated. | +| `params` | The rule's `Parameters` as a string dictionary. | + +The validator returns `{ success: true }` when the value is valid. Return `{ success: false }` to use the rule's server-supplied message, or `{ success: false, message: '...' }` to override the message for that call. + +> [!NOTE] +> A validator function is synchronous. Client-side validation is intended for immediate feedback, so rules that require a network call or other asynchronous work should be validated on the server. For asynchronous validation in interactive render modes, see . + +Empty values are conventionally treated as valid by rules other than `required`, which allows an optional field to remain empty while still being validated when a value is present. + +### Validate programmatically + +The `Blazor.formValidation` API also exposes methods for validating on demand: + +| Method | Description | +|---|---| +| `addValidator(name, validator)` | Registers a custom validator for a rule name. | +| `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. | + +## Replace rule generation + +To take complete control of the validation metadata rendered for a form, implement `ClientValidationProvider` and register it in the service container. The provider returns a that renders the metadata for the fields that were rendered in the form, or `null` when there's nothing to emit. + +This is an advanced extensibility point for scenarios such as sourcing rules from a system other than data annotations. Most apps use the built-in provider and, when a custom rule is needed, implement `IClientValidationRuleProvider` instead. + +## Additional resources + +* +* +* +* diff --git a/aspnetcore/blazor/forms/validation.md b/aspnetcore/blazor/forms/validation.md index f1c8bc3fdc6c..e29014a57d98 100644 --- a/aspnetcore/blazor/forms/validation.md +++ b/aspnetcore/blazor/forms/validation.md @@ -5,137 +5,117 @@ 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: 08/17/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 . +Blazor validates a form's model using [data annotations attributes](xref:System.ComponentModel.DataAnnotations), the same attributes used elsewhere in ASP.NET Core. Most forms only require adding a component to an and annotating the model. -:::moniker-end +More advanced scenarios are covered in separate articles: -## 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. +* : How forms that use static server-side rendering (static SSR) are validated in the browser before submission. +* : Driving validation directly with , writing validator components, and remote validation. +* : Behavior shared with Minimal APIs, including writing custom rules, validating nested objects and collections, and localizing messages. -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" -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. +* : Driving validation directly with , writing validator components, and remote validation. +* : Behavior shared with Minimal APIs, including validating nested objects and collections. :::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 . +* : Driving validation directly with , writing validator components, and remote validation. :::moniker-end -In the following component, the `HandleValidationRequested` handler method clears any existing validation messages by calling before validating the form. - -`Starship8.razor`: +## Validate a form with data annotations -:::moniker range=">= aspnetcore-9.0" +To validate a form: -:::code language="razor" source="~/../blazor-samples/9.0/BlazorSample_BlazorWebApp/Components/Pages/Starship8.razor"::: +1. Annotate the model's properties with [validation attributes](xref:mvc/models/validation#built-in-attributes). +1. Add a component inside the component. +1. Display errors with or components. -:::moniker-end +The following model uses the and attributes: -:::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" +The following form validates the model. The callback is only invoked when validation passes: ```razor -@page "/starship-8" -@implements IDisposable -@inject ILogger Logger - -

Holodeck Configuration

+ + + - -
+

-

-
+ +

+

-

-
- -
-
- -
+ +

+ +
@code { - private EditContext? editContext; + private Starship? Model { get; set; } - public Holodeck? Model { get; set; } + protected override void OnInitialized() => Model ??= new(); - private ValidationMessageStore? messageStore; + private void Submit() { /* Process the valid form. */ } +} +``` - protected override void OnInitialized() - { - Model ??= new(); - editContext = new(Model); - editContext.OnValidationRequested += HandleValidationRequested; - messageStore = new(editContext); - } +Without a component, the model's validation attributes have no effect on the form. - private void HandleValidationRequested(object? sender, - ValidationRequestedEventArgs args) - { - messageStore?.Clear(); +### When validation runs - // Custom validation logic - if (!Model!.Options) - { - messageStore?.Add(() => Model.Options, "Select at least one."); - } - } +Blazor performs two types of validation: - private void Submit() - { - Logger.LogInformation("Submit called: Processing the form"); - } +* *Field validation* runs when the user changes a field and moves out of it. The component associates all reported validation results with that field. +* *Model validation* runs when the form is submitted. The component determines the field for each result from the member name that the result reports. Results that aren't associated with an individual member are associated with the model rather than a field. - public class Holodeck - { - public bool Subsystem1 { get; set; } - public bool Subsystem2 { get; set; } - public bool Options => Subsystem1 || Subsystem2; - } +:::moniker range=">= aspnetcore-10.0" - public void Dispose() - { - if (editContext is not null) - { - editContext.OnValidationRequested -= HandleValidationRequested; - } - } -} -``` +## `DataAnnotationsValidator` validation behavior - +The component has the same validation order and short-circuiting behavior as . The following rules are applied when validating an instance of type `T`: + +1. Member properties of `T` are validated, including recursively validating nested objects. +1. Type-level attributes of `T` are validated. +1. The method is executed, if `T` implements it. + +If one of the preceding steps produces a validation error, the remaining steps are skipped. :::moniker-end @@ -146,1951 +126,426 @@ The compon * [`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) -:::moniker range=">= aspnetcore-10.0" - -For details on validation behavior, see the [`DataAnnotationsValidator` validation behavior](#dataannotationsvalidator-validation-behavior) section. - -:::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: * 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). -There are two general approaches for achieving custom validation, which are described in the next two sections of this article: - -* [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). - -:::moniker range=">= aspnetcore-11.0" - -## Client-side validation in static SSR forms - -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: +Two general approaches are available for validation logic that isn't declared on the model, both described in : -* The form's hosting component uses static SSR (no `@rendermode` directive applied to the component). -* The form contains a component. +* Manual validation using the 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: 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). -### Supported validation attributes - -The following attributes are enforced client-side, matching the server-side data annotations behavior: - -* -* -* -* -* -* -* -* -* -* -* -* - -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. +## Validation Summary and Validation Message components -### Validation timing +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): -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. +```razor + +``` -### Validation messages and accessibility +Output validation messages for a specific model with the `Model` parameter: + +```razor + +``` -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 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: -### Localized validation messages +```razor + +``` -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 . +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. -### CSS framework integration +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: -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. +```css +.validation-message { + color: red; +} +``` -### Enhanced navigation +:::moniker range=">= aspnetcore-8.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. +## Determine if a form field is valid -### Opting out +Use to determine if a field is valid without obtaining validation messages. -To keep server-side data annotations validation but disable client-side enforcement for a single form, set the component's `DisableClientValidation` parameter to `true`: + Supported, but not recommended: -```razor - +```csharp +var isValid = !editContext.GetValidationMessages(fieldIdentifier).Any(); ``` -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: + Recommended: -```razor - +```csharp +var isValid = editContext.IsValid(fieldIdentifier); ``` :::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 . +## Choose the validation your form needs -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). +The default configuration validates the top-level properties of the form's model. Some scenarios require additional setup. Use the following table to find the guidance for a goal: -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. - -Reference the namespace in the component's Razor directives at the top of the component definition file: +:::moniker range=">= aspnetcore-11.0" -```razor -@using System.ComponentModel.DataAnnotations -``` +| Goal | What to do | +|---|---| +| Validate top-level properties with built-in attributes | Nothing further. Add a component to the form, as shown earlier in this article. | +| Express a rule that built-in attributes can't | Write a [custom validation attribute or implement `IValidatableObject`](xref:fundamentals/validation#write-custom-validation-rules). For validation logic that isn't declared on the model, see . | +| Validate properties of nested objects and collection items | Call `AddValidation` and annotate the root model type. See . | +| Validate against a database or web API | Use [asynchronous validation](xref:fundamentals/validation#asynchronous-validation-support), or a [validator component](xref:blazor/forms/validation-advanced). | +| Display error messages in the user's language | See [Localize validation messages](xref:fundamentals/validation#localize-validation-messages). | +| Give immediate feedback in a static SSR form | Supported automatically. See . | -Add an `Id` property to the `Holodeck` model with a validation attribute to limit the string's length to six characters: +:::moniker-end -```csharp -[StringLength(6)] -public string? Id { get; set; } -``` +:::moniker range="= aspnetcore-10.0" -Add a component (``) to the form. Typically, the component is placed immediately under the `` tag, but you can place it anywhere in the form: +| Goal | What to do | +|---|---| +| Validate top-level properties with built-in attributes | Nothing further. Add a component to the form, as shown earlier in this article. | +| Express a rule that built-in attributes can't | Write a [custom validation attribute](xref:mvc/models/validation#custom-attributes) or implement [`IValidatableObject`](xref:mvc/models/validation#ivalidatableobject). For validation logic that isn't declared on the model, see . | +| Validate properties of nested objects and collection items | Call `AddValidation` and annotate the root model type. See . | +| Validate against a database or web API | Use a [validator component](xref:blazor/forms/validation-advanced). | -```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-10.0" -```diff -- OnSubmit="Submit" -+ OnValidSubmit="Submit" -``` +| Goal | What to do | +|---|---| +| Validate top-level properties with built-in attributes | Nothing further. Add a component to the form, as shown earlier in this article. | +| Express a rule that built-in attributes can't | Write a [custom validation attribute](xref:mvc/models/validation#custom-attributes) or implement [`IValidatableObject`](xref:mvc/models/validation#ivalidatableobject). For validation logic that isn't declared on the model, see . | +| Validate properties of nested objects and collection items | See [Nested objects, collection types, and complex types](#nested-objects-collection-types-and-complex-types). | +| Validate against a database or web API | Use a [validator component](xref:blazor/forms/validation-advanced). | -In the ``, add a field for the `Id` property: +:::moniker-end -```razor -
    - - -
    -``` +:::moniker range=">= aspnetcore-10.0" -After making the preceding changes, the form's behavior matches the following specification: +### Nested objects and collections require additional configuration -* 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. +By default, the component validates the top-level properties of the model. Validation attributes on the properties of a nested object, or on the items of a collection, aren't evaluated. -:::moniker range=">= aspnetcore-11.0" +To validate a nested object graph, opt into by calling and annotating the root model type with . The model types must be declared in C# files (`.cs`), not in Razor component files (`.razor`). -## Asynchronous validation +For the full guidance and an example, see . - 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: +> [!WARNING] +> A model that isn't discovered by the validation source generator doesn't produce a build error or a log entry. The form silently validates only the top-level properties, and validation messages are not localized. If nested validation or localization appears to have no effect, see [Validation when `AddValidation` isn't called](xref:fundamentals/validation#validation-when-addvalidation-isnt-called). - +When the built-in validation attributes can't express a rule, declare the rule on the model with a custom or by implementing . Both are executed by the component wherever the form runs. -* `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. +:::moniker range=">= aspnetcore-10.0" - awaits any registered async work before invoking . Sync-only forms continue to work without changes. +For guidance on writing these rules, which is shared with Minimal APIs, see . -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. +:::moniker-end -> [!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. +:::moniker range="< aspnetcore-10.0" -### Form-level async validation +For guidance on writing these rules, see [Custom attributes](xref:mvc/models/validation#custom-attributes) and [`IValidatableObject`](xref:mvc/models/validation#ivalidatableobject). -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. +:::moniker-end -In the following example, a custom validator component checks a username against a remote endpoint when the form is submitted: +When validation logic can't be declared on the model, for example when messages come from a web API response, use a validator component or drive validation directly with . See . -```razor -@implements IDisposable -@inject HttpClient Http +Of the [built-in data annotations validators](xref:mvc/models/validation#built-in-attributes), only the [`[Remote]` validation attribute](xref:mvc/models/validation#remote-attribute) isn't supported in Blazor. -@code { - [CascadingParameter] - private EditContext? CurrentEditContext { get; set; } +### Associate a validation result with a field - [Parameter, EditorRequired] - public RegistrationModel Model { get; set; } = default!; +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 . Without a member name, the message is associated with the model rather than the field, so it doesn't appear in the field's component. - private ValidationMessageStore? _messages; +`CustomValidator.cs`: - protected override void OnInitialized() - { - ArgumentNullException.ThrowIfNull(CurrentEditContext); - _messages = new ValidationMessageStore(CurrentEditContext); - CurrentEditContext.OnValidationRequested += OnValidationRequested; - } +:::moniker range=">= aspnetcore-8.0" - private void OnValidationRequested( - object? sender, ValidationRequestedEventArgs e) => - e.AddAsyncValidator(ValidateUsernameAsync); +```csharp +using System; +using System.ComponentModel.DataAnnotations; - private async Task ValidateUsernameAsync(CancellationToken token) +public class CustomValidator : ValidationAttribute +{ + protected override ValidationResult IsValid(object? value, + ValidationContext validationContext) { - var field = CurrentEditContext!.Field(nameof(Model.Username)); - _messages!.Clear(field); - - var available = await Http.GetFromJsonAsync( - $"api/usernames/available?value={Uri.EscapeDataString(Model.Username)}", - token); - - if (!available) - { - _messages.Add(field, "The username is already taken."); - } - - CurrentEditContext!.NotifyValidationStateChanged(); - } + ... - public void Dispose() - { - if (CurrentEditContext is not null) - { - CurrentEditContext.OnValidationRequested -= OnValidationRequested; - } + return new ValidationResult("Validation message to user.", + [ validationContext.MemberName! ]); } } ``` -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: - -```razor - - - - - - -``` - -### Per-field async validation - -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. - -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-end -Add the following members to the validator component shown in the previous section to re-run the uniqueness check whenever the `Username` field changes: +:::moniker range=">= aspnetcore-6.0 < aspnetcore-8.0" ```csharp -protected override void OnInitialized() -{ - ArgumentNullException.ThrowIfNull(CurrentEditContext); - _messages = new ValidationMessageStore(CurrentEditContext); - CurrentEditContext.OnValidationRequested += OnValidationRequested; - CurrentEditContext.OnFieldChanged += OnFieldChanged; -} +using System; +using System.ComponentModel.DataAnnotations; -private void OnFieldChanged(object? sender, FieldChangedEventArgs e) +public class CustomValidator : ValidationAttribute { - if (e.FieldIdentifier.FieldName != nameof(RegistrationModel.Username)) + protected override ValidationResult IsValid(object? value, + ValidationContext validationContext) { - return; - } + ... - CurrentEditContext!.RegisterAsyncFieldValidator( - e.FieldIdentifier, - token => CheckAsync(e.FieldIdentifier, token)); + return new ValidationResult("Validation message to user.", + new[] { validationContext.MemberName! }); + } } +``` -private async Task CheckAsync(FieldIdentifier field, CancellationToken token) -{ - _messages!.Clear(field); +:::moniker-end - var available = await Http.GetFromJsonAsync( - $"api/usernames/available?value={Uri.EscapeDataString(Model.Username)}", - token); +:::moniker range="< aspnetcore-6.0" - if (!available) - { - _messages.Add(field, "The username is already taken."); - } - - CurrentEditContext!.NotifyValidationStateChanged(); -} +```csharp +using System; +using System.ComponentModel.DataAnnotations; -public void Dispose() +public class CustomValidator : ValidationAttribute { - if (CurrentEditContext is not null) + protected override ValidationResult IsValid(object value, + ValidationContext validationContext) { - CurrentEditContext.OnValidationRequested -= OnValidationRequested; - CurrentEditContext.OnFieldChanged -= OnFieldChanged; + ... + + return new ValidationResult("Validation message to user.", + new[] { validationContext.MemberName }); } } ``` -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. +:::moniker-end + +### Inject services into a custom validation attribute -### Pending and faulted state +Inject services into custom validation attributes through the . The following example demonstrates a salad chef form that validates user input with dependency injection (DI). -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: +The `SaladChef` class indicates the approved starship ingredient list for a Ten Forward salad. -| State | Per-field | Form-level (any field) | -|----------|----------------------------------------------------|----------------------------------| -| Pending | `EditContext.IsValidationPending(fieldIdentifier)` | `EditContext.IsValidationPending()` | -| Faulted | `EditContext.IsValidationFaulted(fieldIdentifier)` | `EditContext.IsValidationFaulted()` | +`SaladChef.cs`: -The per-field overloads accept either a or a `() => model.Property` lambda for convenient use in Razor markup: +:::code language="csharp" source="~/../blazor-samples/8.0/BlazorSample_BlazorWebApp/SaladChef.cs"::: -```razor - - +Register `SaladChef` in the app's DI container in the `Program` file: -@if (EditContext.IsValidationPending(() => Model.Username)) -{ - Checking… -} -else if (EditContext.IsValidationFaulted(() => Model.Username)) -{ - - Validation could not be completed. - -} +```csharp +builder.Services.AddTransient(); ``` -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: +The `IsValid` method of the following `SaladChefValidatorAttribute` class obtains the `SaladChef` service from DI to check the user's input. -```razor - -``` - - 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: - -```css -.pending { - background-image: url('spinner.gif'); - background-repeat: no-repeat; - background-position: right center; -} - -.modified.pending { - border-color: lightblue; -} - -.modified.faulted { - border-color: orange; -} -``` - -### Calling `ValidateAsync` from a custom submit handler - - - -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: - -```razor - - - - - - -@code { - private EditContext _editContext = default!; - - protected override void OnInitialized() => - _editContext = new EditContext(Model); - - private async Task HandleSubmitAsync() - { - if (await _editContext.ValidateAsync(CancellationToken.None)) - { - await RegisterAsync(); - } - } -} -``` - - - -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`. - -### Async validation across rendering modes - -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. - -:::moniker-end - -## Validator components - -Validator components support form validation by managing a for a form's . - -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: - -* [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) - -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. - -> [!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. - -Create a validator component from : - -* 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. - -Update the namespace in the following class to match your app's namespace. - -`CustomValidation.cs`: - -:::code language="csharp" source="~/../blazor-samples/8.0/BlazorSample_BlazorWebApp/CustomValidation.cs"::: - -> [!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`. - -> [!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 . - -## Business logic validation with a validator component - -For general business logic validation, use a [validator component](#validator-components) that receives form errors in a dictionary. - -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. - -In the following example: - -* 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`). - -When validation messages are set in the component, they're added to the validator's and shown in the 's validation summary. - -`Starship9.razor`: - -:::moniker range=">= aspnetcore-9.0" - -:::code language="razor" source="~/../blazor-samples/9.0/BlazorSample_BlazorWebApp/Components/Pages/Starship9.razor"::: - -:::moniker-end - -:::moniker range=">= aspnetcore-8.0 < aspnetcore-9.0" - -:::code language="razor" source="~/../blazor-samples/8.0/BlazorSample_BlazorWebApp/Components/Pages/Starship9.razor"::: - -:::moniker-end - -:::moniker range="< aspnetcore-8.0" - -```razor -@page "/starship-9" -@inject ILogger Logger - -

    Starfleet Starship Database

    - -

    New Ship Entry Form

    - - - - -
    - -
    -
    - -
    -
    - -
    -
    - -@code { - private CustomValidation? customValidation; - - public Starship? Model { get; set; } - - protected override void OnInitialized() => - Model ??= new() { ProductionDate = DateTime.UtcNow }; - - private void Submit() - { - customValidation?.ClearErrors(); - - var errors = new Dictionary>(); - - if (Model!.Classification == "Defense" && - string.IsNullOrEmpty(Model.Description)) - { - errors.Add(nameof(Model.Description), - new() { "For a 'Defense' ship classification, " + - "'Description' is required." }); - } - - if (errors.Any()) - { - customValidation?.DisplayErrors(errors); - } - else - { - Logger.LogInformation("Submit called: Processing the form"); - } - } -} -``` - - - -:::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 - -``` - -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`: +`SaladChefValidatorAttribute.cs`: -:::code language="razor" source="~/../blazor-samples/8.0/BlazorSample_BlazorWebApp/Components/CustomInputText.razor"::: +:::code language="csharp" source="~/../blazor-samples/8.0/BlazorSample_BlazorWebApp/SaladChefValidatorAttribute.cs"::: -The `CustomInputText` component can be used anywhere is used. The following component uses the shared `CustomInputText` component. +The following component validates user input by applying the `SaladChefValidatorAttribute` (`[SaladChefValidator]`) to the salad ingredient string (`SaladIngredient`). -`Starship11.razor`: +`Starship12.razor`: :::moniker range=">= aspnetcore-9.0" -:::code language="razor" source="~/../blazor-samples/9.0/BlazorSample_BlazorWebApp/Components/Pages/Starship11.razor"::: +:::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/Starship11.razor"::: +:::code language="razor" source="~/../blazor-samples/8.0/BlazorSample_BlazorWebApp/Components/Pages/Starship12.razor"::: :::moniker-end :::moniker range="< aspnetcore-8.0" ```razor -@page "/starship-11" -@using System.ComponentModel.DataAnnotations -@inject ILogger Logger +@page "/starship-12" +@inject SaladChef SaladChef - + - - +

    + +

    +
      + @foreach (var message in context.GetValidationMessages()) + { +
    • @message
    • + } +
    -
    - CurrentValue: @Model?.Id -
    - @code { - public Starship? Model { get; set; } - - protected override void OnInitialized() => Model ??= new(); + private string? saladToppers; - private void Submit() - { - Logger.LogInformation("Submit called: Processing the form"); - } + [SaladChefValidator] + public string? SaladIngredient { get; set; } - public class Starship - { - [Required] - [StringLength(10, ErrorMessage = "Id is too long.")] - public string? Id { get; set; } - } + protected override void OnInitialized() => + saladToppers ??= string.Join(", ", SaladChef.SaladToppers); } ``` - - :::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; -} -``` +## Class-level validation with `IValidatableObject` -:::moniker range=">= aspnetcore-8.0" +[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. -## Determine if a form field is valid +:::moniker range="< aspnetcore-10.0" -Use to determine if a field is valid without obtaining validation messages. +## Nested objects, collection types, and complex types - Supported, but not recommended: +> [!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. -```csharp -var isValid = !editContext.GetValidationMessages(fieldIdentifier).Any(); -``` +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. - Recommended: +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: -```csharp -var isValid = editContext.IsValid(fieldIdentifier); +```razor + + + ... + ``` -:::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`: +Annotate model properties with `[ValidateComplexType]`. In the following model classes, the `ShipDescription` class contains additional data annotations to validate when the model is bound to the form: -:::moniker range=">= aspnetcore-8.0" +`Starship.cs`: ```csharp using System; using System.ComponentModel.DataAnnotations; -public class CustomValidator : ValidationAttribute +public class Starship { - protected override ValidationResult IsValid(object? value, - ValidationContext validationContext) - { - ... + ... - return new ValidationResult("Validation message to user.", - [ validationContext.MemberName! ]); - } + [ValidateComplexType] + public ShipDescription ShipDescription { get; set; } = new(); + + ... } ``` -:::moniker-end - -:::moniker range=">= aspnetcore-6.0 < aspnetcore-8.0" +`ShipDescription.cs`: ```csharp using System; using System.ComponentModel.DataAnnotations; -public class CustomValidator : ValidationAttribute +public class ShipDescription { - protected override ValidationResult IsValid(object? value, - ValidationContext validationContext) - { - ... + [Required] + [StringLength(40, ErrorMessage = "Description too long (40 char).")] + public string? ShortDescription { get; set; } - return new ValidationResult("Validation message to user.", - new[] { validationContext.MemberName! }); - } + [Required] + [StringLength(240, ErrorMessage = "Description too long (240 char).")] + public string? LongDescription { get; set; } } ``` :::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 range="< aspnetcore-10.0" -:::moniker-end +## Blazor data annotations validation package -Inject services into custom validation attributes through the . The following example demonstrates a salad chef form that validates user input with dependency injection (DI). +> [!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 `SaladChef` class indicates the approved starship ingredient list for a Ten Forward salad. +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*. -`SaladChef.cs`: +> [!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. -:::code language="csharp" source="~/../blazor-samples/8.0/BlazorSample_BlazorWebApp/SaladChef.cs"::: +:::moniker-end -Register `SaladChef` in the app's DI container in the `Program` file: +:::moniker range="< aspnetcore-6.0" -```csharp -builder.Services.AddTransient(); -``` +## `[CompareProperty]` attribute -The `IsValid` method of the following `SaladChefValidatorAttribute` class obtains the `SaladChef` service from DI to check the user's input. +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). -`SaladChefValidatorAttribute.cs`: +:::moniker-end -:::code language="csharp" source="~/../blazor-samples/8.0/BlazorSample_BlazorWebApp/SaladChefValidatorAttribute.cs"::: +:::moniker range=">= aspnetcore-11.0" -The following component validates user input by applying the `SaladChefValidatorAttribute` (`[SaladChefValidator]`) to the salad ingredient string (`SaladIngredient`). +## Display pending and faulted validation state -`Starship12.razor`: +Asynchronous validation, such as a uniqueness check against a database, doesn't complete immediately. Blazor tracks the state of in-flight validation per field so that the UI can show progress and report failures. -:::moniker range=">= aspnetcore-9.0" +To author asynchronous validation rules, see for attribute-based rules, or for validator components. -:::code language="razor" source="~/../blazor-samples/9.0/BlazorSample_BlazorWebApp/Components/Pages/Starship12.razor"::: +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: -:::moniker-end +| State | Per-field | Form-level (any field) | +|----------|----------------------------------------------------|----------------------------------| +| Pending | `EditContext.IsValidationPending(fieldIdentifier)` | `EditContext.IsValidationPending()` | +| Faulted | `EditContext.IsValidationFaulted(fieldIdentifier)` | `EditContext.IsValidationFaulted()` | -:::moniker range=">= aspnetcore-8.0 < aspnetcore-9.0" +The per-field overloads accept either a or a `() => model.Property` lambda for convenient use in Razor markup: -:::code language="razor" source="~/../blazor-samples/8.0/BlazorSample_BlazorWebApp/Components/Pages/Starship12.razor"::: +```razor + + -:::moniker-end +@if (EditContext.IsValidationPending(() => Model.Username)) +{ + Checking… +} +else if (EditContext.IsValidationFaulted(() => Model.Username)) +{ + + Validation could not be completed. + +} +``` -:::moniker range="< aspnetcore-8.0" +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: ```razor -@page "/starship-12" -@inject SaladChef SaladChef + +``` - - -

      - -

      - -
        - @foreach (var message in context.GetValidationMessages()) - { -
      • @message
      • - } -
      -
      + 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: -@code { - private string? saladToppers; +```css +.pending { + background-image: url('spinner.gif'); + background-repeat: no-repeat; + background-position: right center; +} - [SaladChefValidator] - public string? SaladIngredient { get; set; } +.modified.pending { + border-color: lightblue; +} - protected override void OnInitialized() => - saladToppers ??= string.Join(", ", SaladChef.SaladToppers); +.modified.faulted { + border-color: orange; } ``` @@ -2427,205 +882,6 @@ Using `CustomFieldClassProvider3`: :::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`: - -```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 - -> [!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: - -```razor - - - ... - -``` - -Annotate model properties with `[ValidateComplexType]`. In the following model classes, the `ShipDescription` class contains additional data annotations to validate when the model is bound to the form: - -`Starship.cs`: - -```csharp -using System; -using System.ComponentModel.DataAnnotations; - -public class Starship -{ - ... - - [ValidateComplexType] - public ShipDescription ShipDescription { get; set; } = new(); - - ... -} -``` - -`ShipDescription.cs`: - -```csharp -using System; -using System.ComponentModel.DataAnnotations; - -public class ShipDescription -{ - [Required] - [StringLength(40, ErrorMessage = "Description too long (40 char).")] - public string? ShortDescription { get; set; } - - [Required] - [StringLength(240, ErrorMessage = "Description too long (240 char).")] - public string? LongDescription { get; set; } -} -``` - -:::moniker-end - ## Enable the submit button based on form validation To enable and disable the submit button based on form validation, the following example: @@ -2638,6 +894,23 @@ To enable and disable the submit button based on form validation, the following > [!NOTE] > When assigning to the , don't also assign an to the . +:::moniker range=">= aspnetcore-11.0" + +> [!IMPORTANT] +> The synchronous method used by the following example is obsolete as of .NET 11. In new code, call `EditContext.ValidateAsync` and `await` the result, which also awaits any asynchronous validators registered for the form: +> +> ```csharp +> private async Task HandleFieldChanged(object? sender, FieldChangedEventArgs e) +> { +> formInvalid = !await editContext!.ValidateAsync(); +> StateHasChanged(); +> } +> ``` +> +> For more information, see . + +:::moniker-end + `Starship14.razor`: :::moniker range=">= aspnetcore-9.0" @@ -2753,16 +1026,15 @@ A side effect of the preceding approach is that a validation summary ( component has the same validation order and short-circuiting behavior as . The following rules are applied when validating an instance of type `T`: +* +* +* +* -1. Member properties of `T` are validated, including recursively validating nested objects. -1. Type-level attributes of `T` are validated. -1. The method is executed, if `T` implements it. +:::moniker range=">= aspnetcore-10.0" -If one of the preceding steps produces a validation error, the remaining steps are skipped. +* :::moniker-end diff --git a/aspnetcore/blazor/globalization-localization.md b/aspnetcore/blazor/globalization-localization.md index 8f7d0064d06e..06ad50a3ec1d 100644 --- a/aspnetcore/blazor/globalization-localization.md +++ b/aspnetcore/blazor/globalization-localization.md @@ -35,7 +35,7 @@ For Blazor apps, localization of validation messages for [forms validation using For Blazor apps, localized validation messages for [forms validation using data annotations]() are supported through two paths: * The static resource path using for display names and for localized error messages. This approach is supported in every release. -* The `Microsoft.Extensions.Validation` package, which resolves validation messages and display names through . Available for Blazor apps that enable the new validation pipeline using `AddValidation()`. For details, see . +* , which resolves validation messages and display names through . Available for Blazor apps that enable the validation pipeline with `AddValidation()`. For details, see . :::moniker-end diff --git a/aspnetcore/fundamentals/localization/make-content-localizable.md b/aspnetcore/fundamentals/localization/make-content-localizable.md index c0af76fc0b55..8d7ff67242b2 100644 --- a/aspnetcore/fundamentals/localization/make-content-localizable.md +++ b/aspnetcore/fundamentals/localization/make-content-localizable.md @@ -116,96 +116,18 @@ In the preceding code, `SharedResource` is the class corresponding to the *.resx ## DataAnnotations localization in Minimal APIs and Blazor -Validation localization is available for Minimal API and Blazor apps that opt into the `Microsoft.Extensions.Validation` pipeline by calling `AddValidation()` in `Program.cs`. Localization activates automatically when an is registered, so calling is all that's required to localize validation error messages and the display names of validated properties and parameters: +Validation error messages and the display names of validated members are localized by , which is the validation pipeline used by Minimal APIs and Blazor forms. -```csharp -builder.Services.AddLocalization(); -builder.Services.AddValidation(); -``` - -The localization integration does not apply to MVC and Razor Pages apps, or to Blazor forms that don't include `AddValidation`. - -> [!NOTE] -> The integration is provided by the `Microsoft.Extensions.Validation` package, which is included in the Web SDK (`Microsoft.NET.Sdk.Web`) and the Razor SDK (`Microsoft.NET.Sdk.Razor`), so apps that use those SDKs don't need an explicit package reference. Standalone Blazor WebAssembly apps and other projects that don't use the Web SDK or the Razor SDK must reference the package explicitly: -> -> ```xml -> -> ``` - -### Resource file lookup - -By default, validation localization resolves messages and display names from *.resx* resource files using ASP.NET Core's standard infrastructure. For an overview of authoring and naming *.resx* files, see . - -To use a shared resource file for every validated type, set `ValidationOptions.LocalizerProvider` to create a localizer from a marker type: +Localization activates automatically when an is registered. Call together with : ```csharp -builder.Services.AddValidation(options => -{ - options.LocalizerProvider = (_, factory) => factory.Create(typeof(ValidationResources)); -}); -``` - -The marker type identifies the *.resx* file the framework uses (for example, `ValidationResources.resx` for the default culture and `ValidationResources.fr.resx` for French). - -> [!IMPORTANT] -> A shared resource file is necessary for Minimal APIs, because top-level parameters on Minimal API endpoints don't have a containing type that the default per-type convention can key on. - -Per-type resource file resolution is the default and needs no additional configuration: - -```csharp -builder.Services.AddLocalization(options => options.ResourcesPath = "Resources"); -builder.Services.AddValidation(); -``` - -This approach follows the standard ASP.NET Core convention: under the project's configured `ResourcesPath`, the type's full name (without the project's root namespace prefix) is used as a dotted path. For example, with `ResourcesPath = "Resources"`, a project whose root namespace is `Contoso` looks up validation messages for `Contoso.Models.Customer` in `Resources/Models/Customer.fr.resx` (or equivalently `Resources/Models.Customer.fr.resx`) for French. For a full description of the *.resx* naming and placement conventions, see . - -#### Customize the localizer creation - -For full control over which *.resx* file to use for a given validated type, set `ValidationOptions.LocalizerProvider`. The delegate receives the validated type and an , and returns the to use: - -```csharp -builder.Services.AddValidation(options => -{ - options.LocalizerProvider = (type, factory) => - type is not null && type.Namespace?.StartsWith("Contoso.Admin") == true - ? factory.Create(typeof(AdminValidationResources)) - : factory.Create(typeof(SharedValidationResources)); -}); -``` - -### Localizing from other sources - -The localization data doesn't have to come from *.resx* files. Validation localization resolves strings through whichever is registered in DI. Registering a custom factory implementation switches validation messages to that factory's backing store, with no further configuration: - -```csharp -builder.Services.AddSingleton(); +builder.Services.AddLocalization(); builder.Services.AddValidation(); ``` -This can be used to load localized messages from JSON files, databases, remote translation services, and other sources. - -### What gets localized - -When validation localization is configured: - -* Error messages whose property is set to a resource key are looked up by that key. If no resource entry matches, the literal value of `ErrorMessage` is used as the error message. -* Display names supplied as literal strings through `[Display(Name = "...")]` or `[DisplayName("...")]` are looked up by the literal value as a resource key. If no resource entry matches, the literal value is used as the display name. - -Attributes that use static resource localization (via the `DisplayAttribute.ResourceType` and `ValidationAttribute.ErrorMessageResourceType` properties) are not processed by the validation localizer. - -### Localize the built-in validation messages - -Some applications might find it useful to translate or override the default error messages of attributes like and without setting on every attribute instance. - -When `ErrorMessage` isn't set, conventional lookup keys are tried in order from most to least specific: - -1. `{DeclaringType}_{MemberName}_{AttributeType}_Error` -1. `{DeclaringType}_{AttributeType}_Error` -1. `{AttributeType}_Error` - -With these conventions, a `[Required]` attribute with no `ErrorMessage` on the `Name` property of `CustomerModel` looks up `CustomerModel_Name_RequiredAttribute_Error`, then `CustomerModel_RequiredAttribute_Error`, then `RequiredAttribute_Error`. If none of the keys resolve, the attribute's built-in error message is used. +For the message lookup key conventions, shared resource files, custom message formatting, and the full set of options, see . -The conventions run only when `ErrorMessage` isn't set on the attribute instance, so model-specific overrides via `ErrorMessage = "MyKey"` continue to take precedence. +The integration doesn't apply to MVC and Razor Pages apps. For those frameworks, see . :::moniker-end diff --git a/aspnetcore/fundamentals/minimal-apis.md b/aspnetcore/fundamentals/minimal-apis.md index aa1b47e11a17..e3417a5dbc5b 100644 --- a/aspnetcore/fundamentals/minimal-apis.md +++ b/aspnetcore/fundamentals/minimal-apis.md @@ -134,19 +134,25 @@ For more information on customizing validation error responses with `IProblemDet ### Localizing validation messages -Localization activates automatically when an is registered. Register the standard ASP.NET Core localization services and the validation pipeline in the `Program` file: +Validation error messages and the display names of validated parameters and properties are localized by . + +Register the standard ASP.NET Core localization services together with the validation pipeline in the `Program` file: ```csharp builder.Services.AddLocalization(options => options.ResourcesPath = "Resources"); +builder.Services.AddValidation(); +``` + +By default, lookup keys are resolved against the resources of the type that declares the validated member. Top-level parameters on Minimal API endpoints don't have a containing type, so use `ValidationOptions.LocalizerProvider` to resolve messages for them from a shared resource file: + +```csharp builder.Services.AddValidation(options => { options.LocalizerProvider = (_, factory) => factory.Create(typeof(ValidationResources)); }); ``` -Set `ValidationOptions.LocalizerProvider` for Minimal APIs. Top-level parameters on Minimal API endpoints don't have a containing type, so the default per-type resource lookup has no type to key on—the provider supplies one explicitly. A shared resource file resolves messages and display names against one `.resx` file (for example, `Resources/ValidationResources.fr.resx`). - -For the full set of options, including loading messages from sources other than resource files, see . +For the message lookup key conventions, custom message formatting, and loading messages from sources other than resource files, see . :::moniker-end diff --git a/aspnetcore/fundamentals/validation.md b/aspnetcore/fundamentals/validation.md index b7eb87a64ee4..75ae63b7f5c4 100644 --- a/aspnetcore/fundamentals/validation.md +++ b/aspnetcore/fundamentals/validation.md @@ -5,15 +5,22 @@ author: Youssef1313 description: Use Microsoft.Extensions.Validation in ASP.NET Core to validate models. monikerRange: '>= aspnetcore-10.0' ms.author: ygerges -ms.date: 08/14/2026 +ms.date: 08/17/2026 uid: fundamentals/validation --- # Validation in ASP.NET Core supports complex model validation in Blazor and Minimal API projects. +Validation rules are declared the same way in both frameworks, using [data annotations attributes](xref:System.ComponentModel.DataAnnotations) on a model type, and this article describes the behavior that both frameworks share: + +* Minimal APIs validate a request before the endpoint handler runs. For how validation is surfaced in an endpoint, see . +* Blazor validates a form model through the component. For how validation is surfaced in a form, see . + While the API in the [`Microsoft.Extensions.Validation` NuGet package](https://www.nuget.org/packages/Microsoft.Extensions.Validation) can be used in scenarios outside ASP.NET Core, this article focuses on ASP.NET Core. The API isn't supported for MVC or Razor Pages. For validation guidance that applies to MVC and Razor Pages, see . +## Enable validation + To enable validation, call on in the app's `Program` file: ```csharp @@ -24,6 +31,434 @@ For Minimal APIs, the implementation automatically discovers types that are defi Validation uses a source generator that only discovers validatable types in the assembly where `AddValidation` is called. If Minimal API endpoints are defined in a referenced assembly rather than the assembly where `AddValidation` is called, register validation as shown in the [Register validation in multi-assembly apps](#register-validation-in-multi-assembly-apps) section. +### Validation when `AddValidation` isn't called + +The consequence of omitting , or of calling it but not having a type discovered by the source generator, differs by framework: + +:::moniker range=">= aspnetcore-11.0" + +| Framework | Behavior without `Microsoft.Extensions.Validation` | +|---|---| +| Minimal APIs | No validation runs. Invalid requests reach the endpoint handler and return a `200 - OK` response instead of `400 - Bad Request`. | +| Blazor | The component falls back to , which validates top-level properties only. Nested objects, collection items, and [localized messages](#localize-validation-messages) aren't supported on the fallback path. | + +:::moniker-end + +:::moniker range="< aspnetcore-11.0" + +| Framework | Behavior without `Microsoft.Extensions.Validation` | +|---|---| +| Minimal APIs | No validation runs. Invalid requests reach the endpoint handler and return a `200 - OK` response instead of `400 - Bad Request`. | +| Blazor | The component falls back to , which validates top-level properties only. Nested objects and collection items aren't validated on the fallback path. | + +:::moniker-end + +In both cases there's no build error, exception, or log entry indicating that a type isn't validated. If validation appears to be skipped, confirm all of the following: + +* is called from the assembly that declares the validatable types. See [Register validation in multi-assembly apps](#register-validation-in-multi-assembly-apps). +* The model type is declared in a C# file (`.cs`), not in a Razor component file (`.razor`). See [Nested objects and collections](#nested-objects-and-collections). +* The root type is annotated with when the source generator can't reach it from an endpoint handler signature. See [Force-generate validatable type information](#force-generate-validatable-type-information). + +## Validatable entities + +Three types of entities can be validated: + +* [Parameters](#parameter-validation) (specific to Minimal API endpoint parameters) +* [Types](#type-validation) +* [Properties](#property-validation) + +### Parameter validation + +Parameter validation is the first step in the validation pipeline for Minimal API endpoints. It involves the following steps: + +1. Validate instances applied to the Minimal API parameter. +1. If the parameter type is `IEnumerable`, validate the type for all non-`null` elements. Otherwise, validate the type for the value. + +:::moniker range="< aspnetcore-11.0" + +> [!NOTE] +> Prior to the release of .NET 11, there's a known limitation where nullable value types declared as Minimal API parameters aren't validated. For more information, see [Validation attributes are ignored for nullable value types when passing a null value (`dotnet/aspnetcore` #67033)](https://github.com/dotnet/aspnetcore/issues/67033). + +:::moniker-end + +### Type validation + +Type validation is the next step after parameter validation (and is the first step in Blazor). It involves the following steps: + +1. Validate properties on the type. If any errors are found, the validation process stops. +1. Validate type-level instances. If any errors are found, the validation process stops. +1. Validate implementations. + +### Property validation + +Property validation happens as part of the type validation as explained in the previous section. It involves the following steps: + +1. Validate instances applied to the property. +1. If the property value is `IEnumerable`, perform type validation for all non-`null` elements. Otherwise, perform a single type validation for the value. + +## Write custom validation rules + +When the [built-in validation attributes](xref:mvc/models/validation#built-in-attributes) don't express a rule, write a custom or implement on the model. Both are discovered and executed by in Blazor and Minimal API apps. + +### Custom validation attributes + +Derive from and override to validate a single value. + +Pass the validation context's when creating the . Without a member name, the result isn't associated with a field, which prevents the error from being displayed next to the corresponding input in a Blazor form: + +```csharp +using System.ComponentModel.DataAnnotations; + +public class EvenNumberAttribute : ValidationAttribute +{ + protected override ValidationResult? IsValid(object? value, + ValidationContext validationContext) + { + if (value is int number && number % 2 != 0) + { + return new ValidationResult( + "The value must be an even number.", + [ validationContext.MemberName! ]); + } + + return ValidationResult.Success; + } +} +``` + +Apply the attribute to a property in the same way as a built-in attribute: + +```csharp +public class Order +{ + [EvenNumber] + public int Quantity { get; set; } +} +``` + +### Resolve services in a validation attribute + +A validation attribute obtains services from dependency injection (DI) through the validation context, which makes rules that require a database lookup or a configured option possible: + +```csharp +protected override ValidationResult? IsValid(object? value, + ValidationContext validationContext) +{ + var catalog = validationContext.GetService(); + + ... +} +``` + +For a service that must be resolved, use . Services resolved this way must be registered in the app's service container. + +### Class-level validation with `IValidatableObject` + +Implement for a rule that spans several properties, because an attribute applied to one property can't reliably observe the others. Class-level validation runs after property validation and only if property validation succeeds: + +```csharp +using System.ComponentModel.DataAnnotations; + +public class DateRange : IValidatableObject +{ + public DateOnly Start { get; set; } + public DateOnly End { get; set; } + + public IEnumerable Validate(ValidationContext validationContext) + { + if (End < Start) + { + yield return new ValidationResult( + "End date must fall on or after the start date.", + [ nameof(End) ]); + } + } +} +``` + +:::moniker range=">= aspnetcore-11.0" + +For rules that require I/O, such as a database or web API call, see the [Asynchronous validation support](#asynchronous-validation-support) section instead. + +:::moniker-end + +> [!NOTE] +> In a Blazor form that uses static server-side rendering (static SSR), custom attributes aren't enforced by the browser unless the attribute also supplies a client-side rule. For more information, see . + +:::moniker range=">= aspnetcore-11.0" + + + +## Asynchronous validation support + + supports asynchronous validation. Apply custom implementations of `AsyncValidationAttribute` to parameters, types, or properties, and they're called asynchronously. In addition, types can implement `IAsyncValidatableObject` as well. + +When validating properties on a type, all validation tasks are started concurrently. Similarly, elements of `IEnumerable` collections are validated concurrently. + +`IAsyncValidatableObject` and `AsyncValidationAttribute` require synchronous **and** asynchronous validation logic. For example, the `Validate` and `ValidateAsync` methods of `IAsyncValidatableObject` must be implemented for objects that use the interface. However, validation never calls both methods. If validation is called through an asynchronous code path, only `ValidateAsync` is called. If validation is called through a synchronous code path, only `Validate` is called. + +For Minimal API validation, always calls the asynchronous path and never the synchronous path. + +Blazor form validation calls the asynchronous path for per-field validation and when the form is validated with , which is what uses on submit. The synchronous path is only reached through the method, which is obsolete as of .NET 11. Asynchronous rules therefore work in Blazor forms without additional configuration. + +If your implementation can't support the synchronous path, throw . + +The following example demonstrates a validation class that implements the `IAsyncValidatableObject` interface. In the following scenario, validation requires an asynchronous call path to check a database for a valid email username via a hypothetical `IUserService` service. Because validation requires an asynchronous database call in this scenario, the synchronous `Validate` method, which is required by the interface's contract, shouldn't be called by developer code elsewhere and throws if it ever is called. + +```csharp +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Threading; +using System.Threading.Tasks; + +public class ValidateUser : IAsyncValidatableObject +{ + [Required, EmailAddress] + public string Email { get; set; } = string.Empty; + + // Asynchronous validation path + public async IAsyncEnumerable ValidateAsync( + ValidationContext validationContext, + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + var userService = validationContext.GetService(); + + if (userService is not null) + { + // Asynchronous call that checks a database via a service + if (await userService.IsEmailExistsAsync(Email, cancellationToken)) + { + yield return new ValidationResult( + "Email is already registered.", new[] { nameof(Email) }); + } + } + } + + // Synchronous validation path that throws InvalidOperationException + public IEnumerable Validate(ValidationContext validationContext) + { + throw new InvalidOperationException("Synchronous validation isn't supported."); + } +} +``` + +:::moniker-end + +## Nested objects and collections + +Validation recurses into nested objects and collection items, so a rule declared on a property of a nested type is enforced when the root model is validated. This is one of the main reasons to adopt : without it, only the top-level properties of a model are validated. + +To validate a nested object graph: + +1. Call in the `Program` file where services are registered. +1. Declare the model types in C# files (`.cs`), not in Razor component files (`.razor`). +1. Annotate the root model type with (`[ValidatableType]`). Types reachable from the root are discovered automatically. + +In the following example, only the root `Order` type is annotated. The `Customer`, `ShippingAddress`, and `OrderItem` types are discovered from it, and their validation attributes are enforced when an `Order` is validated. + +`Order.cs`: + +```csharp +using System.ComponentModel.DataAnnotations; +using Microsoft.Extensions.Validation; + +[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(); +} + +public class ShippingAddress +{ + [Required(ErrorMessage = "Street is required.")] + public string? Street { get; set; } + + [Required(ErrorMessage = "City is required.")] + public string? City { get; set; } +} + +public class OrderItem +{ + [Required(ErrorMessage = "Description is required.")] + public string? Description { get; set; } + + [Range(1, 1000, ErrorMessage = "Quantity must be between 1 and 1,000.")] + public int Quantity { get; set; } +} +``` + +Errors from nested members are reported with a path that identifies the member, such as `Customer.ShippingAddress.Street` or `OrderItems[0].Description`. + +### Model types can't be declared in Razor component files + +The requirement to declare model types outside of Razor components (`.razor`) exists because both the validation feature and the Razor compiler use source generators. Currently, the output of one source generator can't be used as the input to another source generator, so a type declared in a `.razor` file isn't discovered. + +A model declared in a `.razor` file doesn't produce a build error. In a Blazor app, the form silently validates only the top-level properties of the model. For more information, see [Validation when `AddValidation` isn't called](#validation-when-addvalidation-isnt-called). + +For model types defined in a class library or in the `.Client` project of a Blazor Web App, see [Register validation in multi-assembly apps](#register-validation-in-multi-assembly-apps). + +:::moniker range=">= aspnetcore-11.0" + +## Localize validation messages + +Validation error messages and the display names of validated members are localized by . The same rules apply wherever the model is validated, so a message localizes identically in a Minimal API endpoint and in a Blazor form. + +### Activate localization + +Localization activates automatically when an is available in the service container. Call to register the standard localization services, then call : + +```csharp +builder.Services.AddLocalization(); +builder.Services.AddValidation(); +``` + +There's no separate package or additional opt-in call. The validation source generator emits the localization lookup into the app's assembly. + +```csharp +using System.ComponentModel.DataAnnotations; +using Microsoft.Extensions.Validation; + +[ValidatableType] +public class CustomerModel +{ + // "CustomerName" is looked up as the resource key for the display name. + [Display(Name = "CustomerName")] + // "NameRequired" is looked up as the resource key for the error message. + [Required(ErrorMessage = "NameRequired")] + public string? Name { get; set; } +} +``` + +By default, keys are resolved against the resources of the type that declares the member. If a key doesn't resolve, the attribute's built-in error message is used, so a missing resource degrades to the non-localized message rather than surfacing the key to the user. + +### Message lookup keys + +When is set, its value is the lookup key and it takes precedence. + +When `ErrorMessage` isn't set, conventional keys are tried in order from most to least specific: + +1. `{DeclaringType}_{MemberName}_{AttributeType}_Error` +1. `{DeclaringType}_{AttributeType}_Error` +1. `{AttributeType}_Error` + +For example, a on the `Name` property of `CustomerModel` is looked up as `CustomerModel_Name_RequiredAttribute_Error`, then `CustomerModel_RequiredAttribute_Error`, then `RequiredAttribute_Error`. If none resolve, the attribute's built-in message is used. + +This makes it possible to translate or override the default message of an attribute across an entire app without setting `ErrorMessage` on every attribute instance: + +```csharp +[ValidatableType] +public class CustomerModel +{ + // Resolves the localized string for 'RequiredAttribute_Error'. + [Required] + public string? Name { get; set; } +} +``` + +Two details affect key construction: + +* The member segment is skipped for type-level attributes that report no member names. +* A nullable value type contributes its underlying type name. + +### Where resource files are located + +Keys are resolved from *.resx* files through ASP.NET Core's standard infrastructure, so the usual naming and placement conventions apply. Per-type resolution is the default and needs no configuration beyond the resources path: + +```csharp +builder.Services.AddLocalization(options => options.ResourcesPath = "Resources"); +builder.Services.AddValidation(); +``` + +Under the configured `ResourcesPath`, the type's full name minus the project's root namespace is used as a dotted path. For example, in a project whose root namespace is `Contoso`, French messages for `Contoso.Models.Customer` are read from *Resources/Models/Customer.fr.resx* (equivalently *Resources/Models.Customer.fr.resx*). For a full description of the conventions, see . + +### Use a shared resource file + +To resolve keys from one resource file for every validated type instead of per-type resources, set `ValidationOptions.LocalizerProvider`: + +```csharp +builder.Services.AddValidation(options => +{ + options.LocalizerProvider = (_, factory) => factory.Create(typeof(ValidationMessages)); +}); +``` + +The delegate also receives the validated type, so an app can select a different resource file per type: + +```csharp +builder.Services.AddValidation(options => +{ + options.LocalizerProvider = (type, factory) => + type?.Namespace?.StartsWith("Contoso.Admin") == true + ? factory.Create(typeof(AdminValidationMessages)) + : factory.Create(typeof(ValidationMessages)); +}); +``` + +### Localize from a source other than resource files + +To read localized strings from a database, JSON files, or another source, register a custom . A user-registered factory takes precedence over the default resource file implementation: + +```csharp +builder.Services.AddSingleton(); +builder.Services.AddValidation(); +``` + +### Attributes that localize themselves + +Attributes that already perform their own resource lookup bypass this pipeline entirely, because they're localized before validation reports the message. This applies to and to . + +### Format a custom attribute's message + +A custom attribute that substitutes its own values into a message template implements `IValidationMessageFormatter`. The framework calls `FormatMessage` with the culture, the localized template, and the resolved display name: + +```csharp +using System.Globalization; +using System.ComponentModel.DataAnnotations; +using Microsoft.Extensions.Validation; + +public sealed class DivisibleByAttribute : ValidationAttribute, IValidationMessageFormatter +{ + public int Divisor { get; init; } + + // Fills {0} with the display name and {1} with the divisor. + public string FormatMessage(CultureInfo culture, string template, string displayName) => + string.Format(culture, template, displayName, Divisor); +} +``` + +> [!NOTE] +> Localization requires . A Blazor form whose model isn't discovered by the validation source generator falls back to , which reports the attribute's raw `ErrorMessage` without localizing it. For more information, see [Validation when `AddValidation` isn't called](#validation-when-addvalidation-isnt-called). + +:::moniker-end + +## Explicit validation skipping + +When needed, you can skip validation for a specific parameter, type, or property by applying the . + +## Force-generate validatable type information + + works via a Roslyn source generator that detects the object graph and types for Minimal API endpoint parameters. + +In some cases, not all of the types that are part of the object graph can be determined at compile time. In these cases, you can force the source generator to consider a type for validation by applying to the type. + ## Register validation in multi-assembly apps To validate types from separate assemblies: @@ -171,129 +606,16 @@ Whichever approach is adopted, denote the presence of the workaround for a futur :::moniker-end -## Validatable entities - -Three types of entities can be validated: - -* [Parameters](#parameter-validation) (specific to Minimal API endpoint parameters) -* [Types](#type-validation) -* [Properties](#property-validation) - -### Parameter validation - -Parameter validation is the first step in the validation pipeline for Minimal API endpoints. It involves the following steps: - -1. Validate instances applied to the Minimal API parameter. -1. If the parameter type is `IEnumerable`, validate the type for all non-`null` elements. Otherwise, validate the type for the value. - -:::moniker range="< aspnetcore-11.0" - -> [!NOTE] -> Prior to the release of .NET 11, there's a known limitation where nullable value types declared as Minimal API parameters aren't validated. For more information, see [Validation attributes are ignored for nullable value types when passing a null value (`dotnet/aspnetcore` #67033)](https://github.com/dotnet/aspnetcore/issues/67033). - -:::moniker-end - -### Type validation - -Type validation is the next step after parameter validation (and is the first step in Blazor). It involves the following steps: - -1. Validate properties on the type. If any errors are found, the validation process stops. -1. Validate type-level instances. If any errors are found, the validation process stops. -1. Validate implementations. - -### Property validation - -Property validation happens as part of the type validation as explained in the previous section. It involves the following steps: - -1. Validate instances applied to the property. -1. If the property value is `IEnumerable`, perform type validation for all non-`null` elements. Otherwise, perform a single type validation for the value. - -## Explicit validation skipping - -When needed, you can skip validation for a specific parameter, type, or property by applying the . - -## Force-generate validatable type information - - works via a Roslyn source generator that detects the object graph and types for Minimal API endpoint parameters. - -In some cases, not all of the types that are part of the object graph can be determined at compile time. In these cases, you can force the source generator to consider a type for validation by applying to the type. - -:::moniker range=">= aspnetcore-11.0" - - - -## Asynchronous validation support - - supports asynchronous validation. Apply custom implementations of `AsyncValidationAttribute` to parameters, types, or properties, and they're called asynchronously. In addition, types can implement `IAsyncValidatableObject` as well. - -When validating properties on a type, all validation tasks are started concurrently. Similarly, elements of `IEnumerable` collections are validated concurrently. - -`IAsyncValidatableObject` and `AsyncValidationAttribute` require synchronous **and** asynchronous validation logic. For example, the `Validate` and `ValidateAsync` methods of `IAsyncValidatableObject` must be implemented for objects that use the interface. However, validation never calls both methods. If validation is called through an asynchronous code path, only `ValidateAsync` is called. If validation is called through a synchronous code path, only `Validate` is called. - -For Minimal API validation, always calls the asynchronous path and never the synchronous path. - -Blazor form validation calls the synchronous path through the (obsoleted as of .NET 11) method. - -If your implementation can't support the synchronous path, throw . - -The following example demonstrates a validation class that implements the `IAsyncValidatableObject` interface. In the following scenario, validation requires an asynchronous call path to check a database for a valid email username via a hypothetical `IUserService` service. Because validation requires an asynchronous database call in this scenario, the synchronous `Validate` method, which is required by the interface's contract, shouldn't be called by developer code elsewhere and throws if it ever is called. - -```csharp -using System; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using System.Threading; -using System.Threading.Tasks; - -public class ValidateUser : IAsyncValidatableObject -{ - [Required, EmailAddress] - public string Email { get; set; } = string.Empty; - - // Asynchronous validation path - public async IAsyncEnumerable ValidateAsync( - ValidationContext validationContext, - [EnumeratorCancellation] CancellationToken cancellationToken = default) - { - var userService = validationContext.GetService(); - - if (userService is not null) - { - // Asynchronous call that checks a database via a service - if (await userService.IsEmailExistsAsync(Email, cancellationToken)) - { - yield return new ValidationResult( - "Email is already registered.", new[] { nameof(Email) }); - } - } - } - - // Synchronous validation path that throws InvalidOperationException - public IEnumerable Validate(ValidationContext validationContext) - { - throw new InvalidOperationException("Synchronous validation isn't supported."); - } -} -``` - -:::moniker-end - ## Additional resources :::moniker range=">= aspnetcore-11.0" * - * [Localized validation messages](xref:blazor/forms/validation#localized-validation-messages) - * [Nested objects and collection types](xref:blazor/forms/validation#nested-objects-and-collection-types) -* + * + * * * [Validation support in Minimal APIs](xref:fundamentals/minimal-apis#validation-support-in-minimal-apis) - * [Localizing validation messages](xref:fundamentals/minimal-apis#localizing-validation-messages) +* * :::moniker-end @@ -301,8 +623,8 @@ public class ValidateUser : IAsyncValidatableObject :::moniker range="< aspnetcore-11.0" * -* [Nested objects and collection types (Blazor)](xref:blazor/forms/validation#nested-objects-and-collection-types) * [Validation support in Minimal APIs](xref:fundamentals/minimal-apis#validation-support-in-minimal-apis) * :::moniker-end + diff --git a/aspnetcore/release-notes/aspnetcore-11/includes/blazor.md b/aspnetcore/release-notes/aspnetcore-11/includes/blazor.md index aca58f2882d5..046c2a08807b 100644 --- a/aspnetcore/release-notes/aspnetcore-11/includes/blazor.md +++ b/aspnetcore/release-notes/aspnetcore-11/includes/blazor.md @@ -708,7 +708,7 @@ Blazor static server-side rendering (static SSR) forms now get instant, in-brows The feature is enabled by default for all static SSR forms that include the `DataAnnotationsValidator` component. Both enhanced and non-enhanced forms are supported. -Complete feature coverage is available in . +Complete feature coverage is available in . For more information, see the following resources: @@ -719,12 +719,14 @@ Please don't comment on closed issues and PRs. If you have feedback on this feat ### Asynchronous form validation support -Blazor forms receive support for async validation rules, such as database lookups or remote API calls. In any rendering mode, `EditForm` submit validation now properly awaits async validators end-to-end. In interactive modes, validator components can register per-field async validation via `EditContext.RegisterAsyncFieldValidator`. The framework tracks them, cancels superseded validations, and exposes progress status via `IsValidationPending(field)` and `IsValidationFaulted(field)`. +Blazor forms receive support for async validation rules, such as database lookups or remote API calls. In any rendering mode, `EditForm` submit validation awaits async validators end-to-end. The built-in `DataAnnotationsValidator` component runs the asynchronous `DataAnnotations` APIs (`AsyncValidationAttribute` and `IAsyncValidatableObject`), so asynchronous rules declared on the model work without additional configuration. +Validator components register asynchronous work with `ValidationRequestedEventArgs.AddAsyncValidator` for the whole form and `EditContext.RegisterAsyncFieldValidator` for a single field. The framework owns the cancellation token source, cancels superseded validations, and exposes progress with `IsValidationPending(field)` and `IsValidationFaulted(field)`. + ```razor - + @if (editContext.IsValidationPending(() => model.Username)) { @@ -765,75 +767,16 @@ The built-in `DataAnnotationsValidator` component runs the asynchronous `DataAnn editContext.NotifyValidationStateChanged(); } - private async Task HandleSubmit() => await editContext.ValidateAsync(); + private Task HandleSubmit() => RegisterAsync(); } ``` -Complete feature coverage is available in . +Complete feature coverage is available in . For more information, see [Add built-in support for async form validation in Blazor (`dotnet/aspnetcore` #66526)](https://github.com/dotnet/aspnetcore/pull/66526). Please don't comment on closed issues and PRs. If you have feedback on this feature, please open a new issue on the `dotnet/aspnetcore` GitHub repository. -### Blazor and Minimal APIs support error localization - -Validation of Blazor forms and Minimal API endpoints receives first-class support for localization of error messages and property names. Localization activates automatically once an `IStringLocalizerFactory` is available. By default, localization registered by `AddLocalization` uses language-specific RESX files deployed as part of the assembly. - -```csharp -builder.Services.AddLocalization(); -builder.Services.AddValidation(); -``` - -```csharp -[ValidatableType] -public class ContactModel -{ - // Values of ErrorMessage are used as localization keys. - [Required(ErrorMessage = "RequiredError")] - [EmailAddress(ErrorMessage = "EmailError")] - [Display(Name = "ContactEmail")] - public string? Email { get; set; } -} -``` - -Apps can also register custom `IStringLocalizerFactory` implementations to read the localized strings from other sources, such as databases or JSON files. A user registered type takes precedence over the default RESX localization. - -```csharp -builder.Services.AddSingleton(); -builder.Services.AddValidation(); -``` - -To resolve keys from a shared resource file instead of the validated type's own resources, set `ValidationOptions.LocalizerProvider`: - -```csharp -builder.Services.AddValidation(options => -{ - options.LocalizerProvider = (_, factory) => factory.Create(typeof(ValidationMessages)); -}); -``` - -When an attribute doesn't set `ErrorMessage`, conventional lookup keys are tried from most to least specific, removing the need to specify localization keys on every validation attribute: - -```csharp -[ValidatableType] -public class ContactModel -{ - // Looks up 'ContactModel_Username_RequiredAttribute_Error', then - // 'ContactModel_RequiredAttribute_Error', then 'RequiredAttribute_Error'. - [Required] - public string? Username { get; set; } -} -``` - -Complete feature coverage is available in the following articles: - -* -* - -For more information, see [Add localization support to Microsoft.Extensions.Validation (`dotnet/aspnetcore` #66646)](https://github.com/dotnet/aspnetcore/pull/66646). - -Please don't comment on closed issues and PRs. If you have feedback on this feature, please open a new issue on the `dotnet/aspnetcore` GitHub repository. - ### Fixes to TempData and `[SupplyParameterFromSession]` persistence for streaming SSR When a page uses session-backed features, where a component has a `[SupplyParameterFromSession]` parameter (which creates a subscription) or the session-storage TempData provider is active, the session cookie (`.AspNetCore.Session`) is now issued before streaming begins, even if no value is ultimately written. Pages that don't use session-backed features are unaffected. diff --git a/aspnetcore/release-notes/aspnetcore-11/includes/validation-localization.md b/aspnetcore/release-notes/aspnetcore-11/includes/validation-localization.md index aac1c914078c..6ad1d2aaf527 100644 --- a/aspnetcore/release-notes/aspnetcore-11/includes/validation-localization.md +++ b/aspnetcore/release-notes/aspnetcore-11/includes/validation-localization.md @@ -19,13 +19,13 @@ public class CustomerModel } ``` -An explicit `ErrorMessage` value, such as `NameRequired` in the preceding example, is the first resource key that localization tries. When an attribute doesn't specify `ErrorMessage`, localization instead tries built-in resource-name conventions from most to least specific: +By default, keys resolve against the resources of the type that declares the validated member. An explicit `ErrorMessage` value, such as `NameRequired` in the preceding example, is the first resource key that localization tries. When an attribute doesn't specify `ErrorMessage`, localization instead tries built-in resource-name conventions from most to least specific: 1. `{DeclaringType}_{MemberName}_{AttributeType}_Error` 1. `{DeclaringType}_{AttributeType}_Error` 1. `{AttributeType}_Error` -For example, a `[Required]` attribute on `CustomerModel.Name` resolves against `CustomerModel_Name_RequiredAttribute_Error`, `CustomerModel_RequiredAttribute_Error`, or the shared `RequiredAttribute_Error` resource. If no resource resolves, validation falls back to the attribute's built-in message. Use `ValidationOptions.LocalizerProvider` to resolve keys from a shared resource file instead: +For example, a `[Required]` attribute on `CustomerModel.Name` resolves against `CustomerModel_Name_RequiredAttribute_Error`, `CustomerModel_RequiredAttribute_Error`, or the shared `RequiredAttribute_Error` resource. This allows the default message of an attribute to be translated once for the whole app. If no resource resolves, validation falls back to the attribute's built-in message. Use `ValidationOptions.LocalizerProvider` to resolve keys from a shared resource file instead: ```csharp builder.Services.AddValidation(options => @@ -34,6 +34,13 @@ builder.Services.AddValidation(options => }); ``` +Localized strings don't have to come from resource files. Registering a custom `IStringLocalizerFactory` switches validation messages to that factory's backing store, such as a database or JSON files. A user-registered factory takes precedence over the default resource file implementation: + +```csharp +builder.Services.AddSingleton(); +builder.Services.AddValidation(); +``` + Attributes that already localize themselves (`ErrorMessageResourceType`, `[Display(ResourceType = ...)]`) bypass the pipeline entirely. A custom attribute that needs to substitute its own values into the message template can implement `IValidationMessageFormatter`: ```csharp @@ -47,3 +54,10 @@ public sealed class DivisibleByAttribute : ValidationAttribute, IValidationMessa ``` The same localization rules apply to validation for minimal APIs and Blazor, so a message localizes identically wherever the model is used. + +Complete feature coverage is available in the following articles: + +* +* + +For more information, see [Add localization support to Microsoft.Extensions.Validation (`dotnet/aspnetcore` #66646)](https://github.com/dotnet/aspnetcore/pull/66646). (Please don't comment on closed issues and PRs.) diff --git a/aspnetcore/toc.yml b/aspnetcore/toc.yml index c1e87ac552bd..0759e75c062a 100644 --- a/aspnetcore/toc.yml +++ b/aspnetcore/toc.yml @@ -862,6 +862,10 @@ items: uid: blazor/forms/binding - name: Validation uid: blazor/forms/validation + - name: Client-side validation (static SSR) + uid: blazor/forms/validation-client-side + - name: Advanced validation + uid: blazor/forms/validation-advanced - name: Troubleshoot uid: blazor/forms/troubleshoot - name: File uploads From 4747969c7cea1fc3a7d80bad7b303b02eb721d51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Rozto=C4=8Dil?= Date: Mon, 17 Aug 2026 17:54:22 +0200 Subject: [PATCH 02/11] Trim and restructure the Blazor validation articles Follow-up to the validation docs restructure. Reduces article length, orders sections from the simplest and most common cases to the most advanced, and moves sample code out of the articles. Blazor forms validation * Merges the three sections that described the DataAnnotationsValidator component into one, with validation order and short-circuiting as a subsection instead of the last section in the article. * Folds the IValidatableObject stub and "Determine if a form field is valid" into the sections they belong to. * Moves the table that routes readers to nested objects, localization, client-side validation, and the advanced article near the top, so a reader looking for one of those finds it early. * Documents the default validation CSS classes, which were previously described only for static SSR. * Moves the FieldCssClassProvider guidance to the advanced article, since it customizes validation rather than explaining it. Advanced form validation * The remote validation walkthrough now references sample projects instead of carrying its code inline, and gains subsections for each step. * The per-field asynchronous validation example shows only what differs from the form-level example rather than repeating it. * The FieldCssClassProvider section arrives here, references its sample files, and gains subsections. Client-side validation in static SSR * The custom rule example references its sample files. Validation in ASP.NET Core * Condenses multi-assembly registration to one pattern with short notes for Minimal APIs and Blazor, instead of two near-identical walkthroughs. Rendered length, .NET 11: 607 to 438, 904 to 714, 283 to 232, and 521 to 483 lines respectively. Verified across all supported versions that no in-page anchor is broken, moniker blocks are balanced, and every active sample reference resolves. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 608096b5-db3e-4006-adaa-b4da0309ff87 --- .../blazor/forms/validation-advanced.md | 587 +++++++----------- .../blazor/forms/validation-client-side.md | 59 +- aspnetcore/blazor/forms/validation.md | 360 +---------- aspnetcore/fundamentals/validation.md | 106 +--- aspnetcore/release-notes/aspnetcore-5.0.md | 2 +- 5 files changed, 304 insertions(+), 810 deletions(-) diff --git a/aspnetcore/blazor/forms/validation-advanced.md b/aspnetcore/blazor/forms/validation-advanced.md index 0bb2879c88bb..33c99e5714c5 100644 --- a/aspnetcore/blazor/forms/validation-advanced.md +++ b/aspnetcore/blazor/forms/validation-advanced.md @@ -436,17 +436,9 @@ For asynchronous work that should run when the user edits a single field, call ` 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. -Add the following members to the validator component shown in the previous section to re-run the uniqueness check whenever the `Username` field changes: +Add a handler for the event to the validator component shown in the previous section. Subscribe to the event in `OnInitialized` and unsubscribe in `Dispose` alongside the existing `OnValidationRequested` subscription. The rest of the component is unchanged: ```csharp -protected override void OnInitialized() -{ - ArgumentNullException.ThrowIfNull(CurrentEditContext); - _messages = new ValidationMessageStore(CurrentEditContext); - CurrentEditContext.OnValidationRequested += OnValidationRequested; - CurrentEditContext.OnFieldChanged += OnFieldChanged; -} - private void OnFieldChanged(object? sender, FieldChangedEventArgs e) { if (e.FieldIdentifier.FieldName != nameof(RegistrationModel.Username)) @@ -458,32 +450,13 @@ private void OnFieldChanged(object? sender, FieldChangedEventArgs e) e.FieldIdentifier, token => CheckAsync(e.FieldIdentifier, token)); } +``` -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); +The `CheckAsync` method performs the same work as `ValidateUsernameAsync` in the preceding example but takes the field to validate as a parameter, so the same logic serves both the form-level and per-field passes. - if (!available) - { - _messages.Add(field, "The username is already taken."); - } +For a complete validator component that combines form-level and per-field asynchronous validation, see the following sample: - CurrentEditContext!.NotifyValidationStateChanged(); -} - -public void Dispose() -{ - if (CurrentEditContext is not null) - { - CurrentEditContext.OnValidationRequested -= OnValidationRequested; - CurrentEditContext.OnFieldChanged -= OnFieldChanged; - } -} -``` +:::code language="razor" source="~/../blazor-samples/11.0/BlazorSample_BlazorWebApp/Components/UsernameUniquenessValidator.razor"::: Write the validator as an `async` method so that an exception thrown before the first `await` is captured into the returned task rather than thrown from `RegisterAsyncFieldValidator`. To cancel from an additional source, create a linked token source inside the validator with . @@ -585,6 +558,8 @@ The validation for the `Defense` ship classification only occurs on the server b > * (and the other articles in the Blazor *Security and Identity* node) > * [Microsoft identity platform documentation](/entra/identity-platform/) +### Create the shared model + 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. @@ -594,37 +569,13 @@ Place the following `StarshipModel` model (`StarshipModel.cs`) into the `Starshi > > [!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; } +The following `StarshipModel` model is placed in the `Starship` folder of the `.Client` project ***and*** in the Minimal API project of the solution. Set the namespace appropriately for each project: the sample uses `BlazorSample.Client.Starship` in the Blazor Web App and `MinimalApiJwt.Models` in the Minimal API project. Some developers prefer a different folder scheme. If you position the classes in different locations, set the namespaces appropriately. - [Required] - public string? Classification { get; set; } +`Starship/StarshipModel.cs` (Blazor Web App) or `StarshipModel.cs` (Minimal API project): - [Range(1, 100000, ErrorMessage = "Accommodation invalid (1-100000).")] - public int MaximumAccommodation { get; set; } +:::code language="csharp" source="~/../blazor-samples/10.0/BlazorWebAppRemoteValidation/BlazorSample.Client/Starship/StarshipModel.cs"::: - [Required] - [Range(typeof(bool), "true", "true", ErrorMessage = "Approval required.")] - public bool IsValidatedDesign { get; set; } - - [Required] - public DateTime ProductionDate { get; set; } -} -``` +### Create the validation abstraction 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. @@ -644,49 +595,9 @@ Add a client form validator class to the `.Client` project's `Starship` folder. `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; - } -} -``` +:::code language="csharp" source="~/../blazor-samples/10.0/BlazorWebAppRemoteValidation/BlazorSample.Client/Starship/ClientFormValidation.cs"::: -Confirm or update the namespace of the preceding class. +### Create the server form validator Create a `Starship` folder in the server project of the Blazor Web App. @@ -694,83 +605,9 @@ In the Blazor Web App, create a server form validator that implements the `IForm `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"); +:::code language="csharp" source="~/../blazor-samples/10.0/BlazorWebAppRemoteValidation/BlazorSample/Starship/ServerFormValidation.cs"::: - 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; - } -} -``` +### Register the server form validator In the `Program` file of the Blazor Web App: @@ -789,6 +626,8 @@ app.MapPost("/starship-validation", (IFormValidation formValidator, }).RequireAuthorization(); ``` +### Register the client form validator + 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 @@ -800,6 +639,8 @@ builder.Services.AddHttpClient(httpClient 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. +### Add the validation endpoint to the Minimal API + 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: @@ -839,63 +680,18 @@ 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. +### Add the validator component + 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; } +:::code language="csharp" source="~/../blazor-samples/10.0/BlazorWebAppRemoteValidation/BlazorSample.Client/CustomValidation.cs"::: - 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(); - } - } +> [!NOTE] +> This is the same `CustomValidation` component described in the [Validator components](#validator-components) section. - public void ClearErrors() - { - messageStore?.Clear(); - CurrentEditContext?.NotifyValidationStateChanged(); - } -} -``` +### Update the form to display validation errors 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`. @@ -904,128 +700,13 @@ Note that the form requires authorization, so the user must be signed into the a > [!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."; - } - } -} -``` +:::code language="razor" source="~/../blazor-samples/10.0/BlazorWebAppRemoteValidation/BlazorSample.Client/Pages/Starship10.razor"::: > [!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 . +### Add a navigation entry + To reach the form easily, add the following entry to the `NavMenu` component (`Layout/NavMenu.razor`) in the `.Client` project: ```razor @@ -1683,6 +1364,218 @@ In the following component, update the namespace of the **`Shared`** project (`@ :::moniker-end +:::moniker range=">= aspnetcore-7.0" + +## Customize validation CSS classes + +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; +} +``` + +### Style all fields + +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" + +:::code language="csharp" source="~/../blazor-samples/8.0/BlazorSample_BlazorWebApp/CustomFieldClassProvider.cs"::: + +:::moniker-end + +:::moniker range=">= aspnetcore-7.0 < aspnetcore-8.0" + +:::code language="csharp" source="~/../blazor-samples/7.0/BlazorSample_WebAssembly/CustomFieldClassProvider.cs"::: + +:::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" + +### Style a single field + +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" + +:::code language="csharp" source="~/../blazor-samples/8.0/BlazorSample_BlazorWebApp/CustomFieldClassProvider2.cs"::: + +:::moniker-end + +:::moniker range=">= aspnetcore-7.0 < aspnetcore-8.0" + +:::code language="csharp" source="~/../blazor-samples/7.0/BlazorSample_WebAssembly/CustomFieldClassProvider2.cs"::: + +:::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. + +### Apply Blazor's default classes to other fields + +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" + +:::code language="csharp" source="~/../blazor-samples/8.0/BlazorSample_BlazorWebApp/CustomFieldClassProvider3.cs"::: + +:::moniker-end + +:::moniker range=">= aspnetcore-7.0 < aspnetcore-8.0" + +:::code language="csharp" source="~/../blazor-samples/7.0/BlazorSample_WebAssembly/CustomFieldClassProvider3.cs"::: + +:::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 + ## Additional resources * diff --git a/aspnetcore/blazor/forms/validation-client-side.md b/aspnetcore/blazor/forms/validation-client-side.md index f8af1e2af082..fc8b77b0ecef 100644 --- a/aspnetcore/blazor/forms/validation-client-side.md +++ b/aspnetcore/blazor/forms/validation-client-side.md @@ -171,51 +171,13 @@ Implement `IClientValidationRuleProvider` on the validation attribute and return The framework attaches each rule's resolved error message, including the localized message when localization is configured, so the attribute supplies only the rule's shape. -The following `StartsWithAttribute` validates server-side in `IsValid` and contributes a `startsWith` client-side rule with a `prefix` parameter: +The following `StartsWithAttribute` validates server-side in `IsValid` and contributes a `startswith` client-side rule with a `prefix` parameter: -```csharp -using System.ComponentModel.DataAnnotations; -using Microsoft.AspNetCore.Components.Forms; - -public sealed class StartsWithAttribute : ValidationAttribute, IClientValidationRuleProvider -{ - private readonly string prefix; - - public StartsWithAttribute(string prefix) - { - this.prefix = prefix; - ErrorMessage = $"The value must start with '{prefix}'."; - } - - protected override ValidationResult? IsValid(object? value, - ValidationContext validationContext) - { - if (value is string text && !text.StartsWith(prefix, StringComparison.Ordinal)) - { - return new ValidationResult(ErrorMessage, [ validationContext.MemberName! ]); - } - - return ValidationResult.Success; - } - - public IEnumerable GetClientValidationRules() - { - yield return new ClientValidationRule( - "startswith", - new Dictionary { ["prefix"] = prefix }); - } -} -``` +:::code language="csharp" source="~/../blazor-samples/11.0/BlazorSample_BlazorWebApp/Validation/StartsWithAttribute.cs"::: Apply the attribute to the model in the usual way: -```csharp -public class ShipModel -{ - [StartsWith("NCC-")] - public string? Registry { get; set; } -} -``` +:::code language="csharp" source="~/../blazor-samples/11.0/BlazorSample_BlazorWebApp/Validation/ShipModel.cs"::: ### Register the matching client-side validator @@ -225,20 +187,7 @@ The `Blazor.formValidation` service is created while Blazor starts, so it isn't In a JavaScript initializer file named `{APP NAMESPACE}.lib.module.js` placed in the app's `wwwroot` folder, where the `{APP NAMESPACE}` placeholder is the app's namespace: -```javascript -export function afterWebStarted(blazor) { - blazor.formValidation.addValidator('startswith', (context) => { - const value = context.value; - - // An empty value is valid. Use [Required] to require a value. - if (!value) { - return { success: true }; - } - - return { success: value.startsWith(context.params.prefix) }; - }); -} -``` +:::code language="javascript" source="~/../blazor-samples/11.0/BlazorSample_BlazorWebApp/wwwroot/BlazorSample.lib.module.js"::: Rule names are matched exactly, so the name passed to `addValidator` must match the `ClientValidationRule` `Name` value, including casing. diff --git a/aspnetcore/blazor/forms/validation.md b/aspnetcore/blazor/forms/validation.md index e29014a57d98..a8799e9ed843 100644 --- a/aspnetcore/blazor/forms/validation.md +++ b/aspnetcore/blazor/forms/validation.md @@ -107,7 +107,7 @@ Blazor performs two types of validation: :::moniker range=">= aspnetcore-10.0" -## `DataAnnotationsValidator` validation behavior +### `DataAnnotationsValidator` validation behavior The component has the same validation order and short-circuiting behavior as . The following rules are applied when validating an instance of type `T`: @@ -119,7 +119,7 @@ If one of the preceding steps produces a validation error, the remaining steps a :::moniker-end -## Data Annotations Validator component and custom validation +### Data Annotations Validator component and custom validation 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: @@ -170,9 +170,29 @@ Control the style of validation messages in the app's stylesheet (`wwwroot/css/a } ``` +### Validation state CSS classes + +Blazor applies CSS classes to input elements and validation components to reflect validation state. The classes make it possible to style validation without writing any C#: + +| 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` | + +The stylesheet included in the Blazor project templates styles these classes, so a form gets validation styling with no additional configuration. For example, the following rule outlines a field that the user has edited and that's currently valid: + +```css +.valid.modified:not([type=checkbox]) { + outline: 1px solid #26b050; +} +``` + +To supply different class names, for example to integrate with a CSS framework such as [Bootstrap](https://getbootstrap.com/), see . + :::moniker range=">= aspnetcore-8.0" -## Determine if a form field is valid +### Determine if a form field is valid Use to determine if a field is valid without obtaining validation messages. @@ -407,7 +427,7 @@ The following component validates user input by applying the `SaladChefValidator :::moniker-end -## Class-level validation with `IValidatableObject` +### 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. @@ -551,337 +571,6 @@ The form-level parameterless overloads return `true` when any field is currently :::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 - ## Enable the submit button based on form validation To enable and disable the submit button based on form validation, the following example: @@ -1038,3 +727,4 @@ A side effect of the preceding approach is that a validation summary ( :::moniker-end + diff --git a/aspnetcore/fundamentals/validation.md b/aspnetcore/fundamentals/validation.md index 75ae63b7f5c4..79ee5d0d5069 100644 --- a/aspnetcore/fundamentals/validation.md +++ b/aspnetcore/fundamentals/validation.md @@ -461,94 +461,56 @@ In some cases, not all of the types that are part of the object graph can be det ## Register validation in multi-assembly apps -To validate types from separate assemblies: - -* If the assembly is a plain class library (it isn't based on the `Microsoft.NET.Sdk.Web` or `Microsoft.NET.Sdk.Razor` SDKs), add a package reference to the project for the [`Microsoft.Extensions.Validation` NuGet package](https://www.nuget.org/packages/Microsoft.Extensions.Validation). -* Create an extension method in each external assembly that calls . -* Call each of those extension methods from the host app. - -### Minimal API example - -When endpoint handler types are defined for endpoints in a separate Minimal API assembly but is only called from the host app assembly, validation doesn't execute: Invalid requests are processed and return a `200 - OK` response instead of the expected `400 - Bad Request` response, even though `AddValidation` is registered and the request types use validation attributes. - -Create a service collection extension method in an assembly that defines Minimal API endpoints and call it from the host app. - -`ServiceCollectionExtensions.cs` in the assembly that defines the endpoints, which uses the example namespace `MinimalApisAssembly.Extensions`: - -```csharp -namespace MinimalApisAssembly.Extensions; - -public static class ServiceCollectionExtensions -{ - public static IServiceCollection AddApiValidation( - this IServiceCollection services) - { - return services.AddValidation(); - } -} -``` - -In the host app's `Program` file, call the extension method instead of calling `AddValidation` directly: - -```csharp -using MinimalApisAssembly.Extensions; - -... - -builder.Services.AddApiValidation(); - -... +The validation source generator only discovers validatable types in the assembly where is called. Types declared in a referenced assembly, such as a class library or the `.Client` project of a Blazor Web App, aren't validated when `AddValidation` is only called from the host app. -var app = builder.Build(); +There's no error or log entry when this happens. In a Minimal API, invalid requests return a `200 - OK` response instead of `400 - Bad Request`. In Blazor, the form doesn't honor the validation attributes of the models. -app.MapApi(); -``` - -In the preceding example, `MapApi` is an extension method defined in the endpoints assembly that maps the Minimal API endpoints. Define it alongside `AddApiValidation` so both the endpoint mappings and validation are registered from the same assembly. - -### Blazor Web App example +To validate types from separate assemblies: -When form model types are defined in a separate library or the `.Client` project of a Blazor Web App but is only called from the server app's assembly, form validation doesn't honor the validation attributes of the models. +* If the assembly is a plain class library (it isn't based on the `Microsoft.NET.Sdk.Web` or `Microsoft.NET.Sdk.Razor` SDKs), add a package reference to the project for the [`Microsoft.Extensions.Validation` NuGet package](https://www.nuget.org/packages/Microsoft.Extensions.Validation). +* Create an extension method in each assembly that declares validatable types. The method calls so that the source generator runs in that assembly: -Create a service collection extension method in the assembly that defines the validatable types and call it from the host app. + ```csharp + namespace ValidatableTypesAssembly.Extensions; + + public static class ServiceCollectionExtensions + { + public static IServiceCollection AddValidationForLibraryTypes( + this IServiceCollection services) + { + return services.AddValidation(); + } + } + ``` -For model validation defined in the `.Client` project of a Blazor Web App: +* Call each of those extension methods from the host app, along with `AddValidation` for the host app's own types: -* Create a method in the `.Client` project that receives an instance as an argument and calls on it. -* In the app, call both the method and . + ```csharp + using ValidatableTypesAssembly.Extensions; -The preceding approach results in validation of the types from both assemblies. + ... -In the following example, the `AddValidationForClientTypes` method is created for the `.Client` project of a Blazor Web App for validation using types defined in the `.Client` project. + builder.Services.AddValidationForLibraryTypes(); + builder.Services.AddValidation(); + ``` -`ServiceCollectionExtensions.cs` in the `.Client` project that defines validatable types, which uses the example namespace `BlazorSample.Client.Extensions`: +The preceding approach validates the types from both assemblies. -```csharp -namespace BlazorSample.Client.Extensions; +Two framework-specific notes: -public static class ServiceCollectionExtensions -{ - public static IServiceCollection AddValidationForClientTypes( - this IServiceCollection services) - { - return services.AddValidation(); - } -} -``` +* **Minimal APIs:** when endpoints are mapped from the referenced assembly, define the endpoint-mapping extension method (`MapApi` in the following example) alongside the validation extension method so both are registered from the same assembly: -In the server project's `Program` file: + ```csharp + builder.Services.AddApiValidation(); -* Call the `.Client` project's service collection extension method to validate types in the `.Client` project. -* Call to validate types in the server project. + ... -```csharp -using BlazorSample.Client.Extensions; + var app = builder.Build(); -... + app.MapApi(); + ``` -builder.Services.AddValidationForClientTypes(); -builder.Services.AddValidation(); -``` +* **Blazor Web Apps:** form model types are commonly declared in the `.Client` project. Create the extension method there and call it from the server project's `Program` file. :::moniker range="= aspnetcore-10.0" diff --git a/aspnetcore/release-notes/aspnetcore-5.0.md b/aspnetcore/release-notes/aspnetcore-5.0.md index db0b38cc5ad0..8afa4d8f2659 100644 --- a/aspnetcore/release-notes/aspnetcore-5.0.md +++ b/aspnetcore/release-notes/aspnetcore-5.0.md @@ -141,7 +141,7 @@ Use the `FocusAsync` convenience method on element references to set the UI focu ### Custom validation CSS class attributes -Custom validation CSS class attributes are useful when integrating with CSS frameworks, such as Bootstrap. For more information, see . +Custom validation CSS class attributes are useful when integrating with CSS frameworks, such as Bootstrap. For more information, see . ### IAsyncDisposable support From 861b965297d30516fdca96ec6bc17e4c0004fa6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Rozto=C4=8Dil?= Date: Tue, 25 Aug 2026 13:25:54 +0200 Subject: [PATCH 03/11] Fix after main updates --- aspnetcore/blazor/forms/validation-advanced.md | 2 +- .../release-notes/aspnetcore-11/includes/blazor.md | 9 --------- .../validation-attributes-no-longer-experimental.md | 7 ++++++- aspnetcore/release-notes/aspnetcore-7.0.md | 2 +- 4 files changed, 8 insertions(+), 12 deletions(-) diff --git a/aspnetcore/blazor/forms/validation-advanced.md b/aspnetcore/blazor/forms/validation-advanced.md index 33c99e5714c5..46d8a040e4ec 100644 --- a/aspnetcore/blazor/forms/validation-advanced.md +++ b/aspnetcore/blazor/forms/validation-advanced.md @@ -519,7 +519,7 @@ The implementation automatically discovers types that are defined in Minimal API Built-in validation also supports [custom validation attributes](xref:mvc/models/validation#custom-attributes). -For more information, see . +For more information, see . :::moniker-end diff --git a/aspnetcore/release-notes/aspnetcore-11/includes/blazor.md b/aspnetcore/release-notes/aspnetcore-11/includes/blazor.md index 046c2a08807b..fd80c27205bc 100644 --- a/aspnetcore/release-notes/aspnetcore-11/includes/blazor.md +++ b/aspnetcore/release-notes/aspnetcore-11/includes/blazor.md @@ -857,15 +857,6 @@ The following new [`QuickGrid` component](xref:Microsoft.AspNetCore.Components.Q For more information, see . -### `ValidatableTypeAttribute` and `SkipValidationAttribute` are no longer experimental - -The and attributes from the [`Microsoft.Extensions.Validation` NuGet package](https://www.nuget.org/packages/Microsoft.Extensions.Validation) are no longer experimental. - -For more information, see the following resources: - -* -* - ### Cache rendered output of a component subtree during static SSR The new `CacheView` component caches the rendered output of a Razor component subtree during static server-side rendering (static SSR). On a cache hit, cached markup is replayed without instantiating or running the lifecycle of the child components that were included in the cached output. diff --git a/aspnetcore/release-notes/aspnetcore-11/includes/validation-attributes-no-longer-experimental.md b/aspnetcore/release-notes/aspnetcore-11/includes/validation-attributes-no-longer-experimental.md index 733eb1fb93f4..c1f4d257806a 100644 --- a/aspnetcore/release-notes/aspnetcore-11/includes/validation-attributes-no-longer-experimental.md +++ b/aspnetcore/release-notes/aspnetcore-11/includes/validation-attributes-no-longer-experimental.md @@ -1,3 +1,8 @@ ### Validation attributes are no longer experimental -`ValidatableTypeAttribute` and `SkipValidationAttribute` are no longer marked experimental. If you suppressed `ASP0029` to use either attribute, remove the suppression. +The and attributes from the [`Microsoft.Extensions.Validation` NuGet package](https://www.nuget.org/packages/Microsoft.Extensions.Validation) are no longer marked experimental. If you suppressed `ASP0029` to use either attribute, remove the suppression. + +For more information, see the following resources: + +* +* diff --git a/aspnetcore/release-notes/aspnetcore-7.0.md b/aspnetcore/release-notes/aspnetcore-7.0.md index d7dd7281d460..74e1f1e7fe96 100644 --- a/aspnetcore/release-notes/aspnetcore-7.0.md +++ b/aspnetcore/release-notes/aspnetcore-7.0.md @@ -436,7 +436,7 @@ For more information, see [Developers targeting browser-wasm can use Web Crypto You can now inject services into custom validation attributes. Blazor sets up the `ValidationContext` so that it can be used as a service provider. -For more information, see . +For more information, see . ### `Input*` components outside of an `EditContext`/`EditForm` From 4c818f51aa2294c404439b37477b9ab8bce34ce4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Rozto=C4=8Dil?= Date: Wed, 16 Sep 2026 19:48:30 +0200 Subject: [PATCH 04/11] Rework Blazor and MEV validation docs --- .../blazor/forms/validation-advanced.md | 1637 +++-------------- .../blazor/forms/validation-client-side.md | 148 +- aspnetcore/blazor/forms/validation.md | 762 ++++---- aspnetcore/fundamentals/validation.md | 300 +-- 4 files changed, 776 insertions(+), 2071 deletions(-) diff --git a/aspnetcore/blazor/forms/validation-advanced.md b/aspnetcore/blazor/forms/validation-advanced.md index 46d8a040e4ec..b0041d573242 100644 --- a/aspnetcore/blazor/forms/validation-advanced.md +++ b/aspnetcore/blazor/forms/validation-advanced.md @@ -2,7 +2,7 @@ title: ASP.NET Core Blazor advanced form validation ai-usage: ai-assisted author: guardrex -description: Learn how to control Blazor form validation directly with EditContext, validator components, and remote validation. +description: Learn how to implement validator components and remote validation for Blazor forms. monikerRange: '>= aspnetcore-3.1' ms.author: wpickett ms.date: 08/17/2026 @@ -12,1585 +12,398 @@ uid: blazor/forms/validation-advanced [!INCLUDE[](~/includes/not-latest-version.md)] -This article explains how to take direct control of Blazor form validation using , validator components, and remote validation. +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 . -The techniques in this article are for scenarios that validation attributes on the model can't express, most commonly when validation messages come from outside the model, such as a web API response or a business rule that requires server-side data. - -For validation with data annotations attributes and the component, see . Writing custom rules as attributes on the model is simpler than the approaches in this article. For more information, see [Custom attributes](xref:mvc/models/validation#custom-attributes). - -## Validate with `EditContext` and `ValidationMessageStore` - -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. - -This approach 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 range=">= aspnetcore-8.0 < aspnetcore-11.0" +:::moniker range=">= aspnetcore-10.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 model-based validation rules shared by Blazor and Minimal APIs, see . :::moniker-end :::moniker range=">= aspnetcore-11.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 browser validation in static server-side rendering (static SSR), see . :::moniker-end -In the following component, the `HandleValidationRequested` handler method clears any existing validation messages by calling before validating the form. - -`Starship8.razor`: - -:::moniker range=">= aspnetcore-9.0" + + + + -:::code language="razor" source="~/../blazor-samples/9.0/BlazorSample_BlazorWebApp/Components/Pages/Starship8.razor"::: +## Build a validator component -:::moniker-end - -:::moniker range=">= aspnetcore-8.0 < aspnetcore-9.0" +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. -:::code language="razor" source="~/../blazor-samples/8.0/BlazorSample_BlazorWebApp/Components/Pages/Starship8.razor"::: +The component: -:::moniker-end +* 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. -:::moniker range="< aspnetcore-8.0" +`CustomValidation.razor`: ```razor -@page "/starship-8" @implements IDisposable -@inject ILogger Logger - -

      Holodeck Configuration

      - - -
      - -
      -
      - -
      -
      - -
      -
      - -
      -
      @code { - private EditContext? editContext; - - public Holodeck? Model { get; set; } + [CascadingParameter] + private EditContext? CurrentEditContext { get; set; } - private ValidationMessageStore? messageStore; + private ValidationMessageStore _messages = default!; protected override void OnInitialized() { - Model ??= new(); - editContext = new(Model); - editContext.OnValidationRequested += HandleValidationRequested; - messageStore = new(editContext); + if (CurrentEditContext is null) + { + throw new InvalidOperationException( + "CustomValidation requires a cascading EditContext."); + } + + _messages = new ValidationMessageStore(CurrentEditContext); + CurrentEditContext.OnValidationRequested += + HandleValidationRequested; + CurrentEditContext.OnFieldChanged += HandleFieldChanged; } - private void HandleValidationRequested(object? sender, - ValidationRequestedEventArgs args) + public void DisplayErrors(IDictionary errors) { - messageStore?.Clear(); - - // Custom validation logic - if (!Model!.Options) + foreach (var error in errors) { - messageStore?.Add(() => Model.Options, "Select at least one."); + _messages.Add( + CurrentEditContext!.Field(error.Key), + error.Value); } + + CurrentEditContext!.NotifyValidationStateChanged(); } - private void Submit() + public void ClearErrors() { - Logger.LogInformation("Submit called: Processing the form"); + _messages.Clear(); + CurrentEditContext!.NotifyValidationStateChanged(); } - public class Holodeck + private void HandleValidationRequested( + object? sender, ValidationRequestedEventArgs e) => + ClearErrors(); + + private void HandleFieldChanged( + object? sender, FieldChangedEventArgs e) { - public bool Subsystem1 { get; set; } - public bool Subsystem2 { get; set; } - public bool Options => Subsystem1 || Subsystem2; + _messages.Clear(e.FieldIdentifier); + CurrentEditContext!.NotifyValidationStateChanged(); } public void Dispose() { - if (editContext is not null) + if (CurrentEditContext is not null) { - editContext.OnValidationRequested -= HandleValidationRequested; + CurrentEditContext.OnValidationRequested -= + HandleValidationRequested; + CurrentEditContext.OnFieldChanged -= HandleFieldChanged; } } } ``` - - -:::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). - -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. - -Reference the namespace in the component's Razor directives at the top of the component definition file: - -```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; } -``` - -Add a component (``) to the form. Typically, the component is placed immediately under the `` tag, but you can place it anywhere in the form: - -```razor - -``` - -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: - -```diff -- OnSubmit="Submit" -+ OnValidSubmit="Submit" -``` - -In the ``, add a field for the `Id` property: - -```razor -
      - - -
      -``` - -After making the preceding changes, the form's behavior matches the following specification: - -* 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. - -## Validator components - -Validator components support form validation by managing a for a form's . - -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: - -* [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) - -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. - -> [!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 . - -Create a validator component from : - -* 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. - -Update the namespace in the following class to match your app's namespace. - -`CustomValidation.cs`: - -:::code language="csharp" source="~/../blazor-samples/8.0/BlazorSample_BlazorWebApp/CustomValidation.cs"::: - -> [!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`. - -> [!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 . - -## Business logic validation with a validator component - -For general business logic validation, use a [validator component](#validator-components) that receives form errors in a dictionary. - -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. - -In the following example: - -* 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`). - -When validation messages are set in the component, they're added to the validator's and shown in the 's validation summary. - -`Starship9.razor`: - -:::moniker range=">= aspnetcore-9.0" - -:::code language="razor" source="~/../blazor-samples/9.0/BlazorSample_BlazorWebApp/Components/Pages/Starship9.razor"::: - -:::moniker-end - -:::moniker range=">= aspnetcore-8.0 < aspnetcore-9.0" - -:::code language="razor" source="~/../blazor-samples/8.0/BlazorSample_BlazorWebApp/Components/Pages/Starship9.razor"::: - -:::moniker-end - -:::moniker range="< aspnetcore-8.0" +Place the component inside an `EditForm` and capture a component reference when the form or a service needs to display errors: ```razor -@page "/starship-9" -@inject ILogger Logger - -

      Starfleet Starship Database

      - -

      New Ship Entry Form

      - - + + -
      - -
      -
      - -
      -
      - -
      + + ...
      @code { - private CustomValidation? customValidation; - - public Starship? Model { get; set; } - - protected override void OnInitialized() => - Model ??= new() { ProductionDate = DateTime.UtcNow }; - - private void Submit() - { - customValidation?.ClearErrors(); - - var errors = new Dictionary>(); - - if (Model!.Classification == "Defense" && - string.IsNullOrEmpty(Model.Description)) - { - errors.Add(nameof(Model.Description), - new() { "For a 'Defense' ship classification, " + - "'Description' is required." }); - } - - if (errors.Any()) - { - customValidation?.DisplayErrors(errors); - } - else - { - Logger.LogInformation("Submit called: Processing the form"); - } - } + private CustomValidation? _customValidation; } ``` - - -:::moniker-end +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. -> [!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 . +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" -## Asynchronous validation - - exposes an asynchronous validation pipeline that custom validator components and custom submit handlers 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: - -* `ValidationRequestedEventArgs.AddAsyncValidator`: registers asynchronous work to run as part of the current validation pass. 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. -* `EditContext.ValidateAsync`: an asynchronous counterpart to that invokes the registered validators and awaits them. It accepts a . - - awaits any registered asynchronous work before invoking . Forms with only synchronous validators continue to work without changes. + -To author asynchronous rules as data annotations attributes on the model instead of writing a validator component, see . The built-in component runs asynchronous attributes without any additional configuration. +### Add asynchronous validation -> [!IMPORTANT] -> Asynchronous work can only be registered during an asynchronous validation pass. If a form is validated with the obsolete synchronous method, `AddAsyncValidator` throws an that directs the caller to `ValidateAsync`. This guarantees that an asynchronous validator is never silently skipped. - -### Form-level async validation - -Subscribe to and call `AddAsyncValidator` from the handler to run asynchronous 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. - -In the following example, a custom validator component checks a username against a remote endpoint when the form is submitted: - -```razor -@implements IDisposable -@inject HttpClient Http +The same component pattern supports asynchronous work: -@code { - [CascadingParameter] - private EditContext? CurrentEditContext { get; set; } +* 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. - [Parameter, EditorRequired] - public RegistrationModel Model { get; set; } = default!; +For form-level asynchronous validation: - private ValidationMessageStore? _messages; +```csharp +private void HandleValidationRequested( + object? sender, ValidationRequestedEventArgs e) => + e.AddAsyncValidator(ValidateAsync); - protected override void OnInitialized() - { - ArgumentNullException.ThrowIfNull(CurrentEditContext); - _messages = new ValidationMessageStore(CurrentEditContext); - CurrentEditContext.OnValidationRequested += OnValidationRequested; - } +private async Task ValidateAsync(CancellationToken cancellationToken) +{ + var field = CurrentEditContext!.Field(nameof(Model.Username)); + _messages.Clear(field); - private void OnValidationRequested( - object? sender, ValidationRequestedEventArgs e) => - e.AddAsyncValidator(ValidateUsernameAsync); + var available = await Http.GetFromJsonAsync( + $"api/usernames/available?value={Uri.EscapeDataString(Model.Username)}", + cancellationToken); - private async Task ValidateUsernameAsync(CancellationToken token) + if (!available) { - var field = CurrentEditContext!.Field(nameof(Model.Username)); - _messages!.Clear(field); - - var available = await Http.GetFromJsonAsync( - $"api/usernames/available?value={Uri.EscapeDataString(Model.Username)}", - token); - - if (!available) - { - _messages.Add(field, "The username is already taken."); - } - - CurrentEditContext!.NotifyValidationStateChanged(); + _messages.Add(field, "The username is already taken."); } - public void Dispose() - { - if (CurrentEditContext is not null) - { - CurrentEditContext.OnValidationRequested -= OnValidationRequested; - } - } + CurrentEditContext.NotifyValidationStateChanged(); } ``` -Place the component inside an alongside the form's inputs. Because awaits the asynchronous validators before invoking , the submit handler runs only after the remote check completes successfully: - -```razor - - - - - - -``` - -### Per-field async validation - -For asynchronous 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 that the field's pending and faulted state can be queried and displayed independently of other fields. - -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. - -Add a handler for the event to the validator component shown in the previous section. Subscribe to the event in `OnInitialized` and unsubscribe in `Dispose` alongside the existing `OnValidationRequested` subscription. The rest of the component is unchanged: +For field-level asynchronous validation: ```csharp -private void OnFieldChanged(object? sender, FieldChangedEventArgs e) +private void HandleFieldChanged( + object? sender, FieldChangedEventArgs e) { - if (e.FieldIdentifier.FieldName != nameof(RegistrationModel.Username)) - { - return; - } - CurrentEditContext!.RegisterAsyncFieldValidator( e.FieldIdentifier, - token => CheckAsync(e.FieldIdentifier, token)); + token => ValidateFieldAsync(e.FieldIdentifier, token)); } ``` -The `CheckAsync` method performs the same work as `ValidateUsernameAsync` in the preceding example but takes the field to validate as a parameter, so the same logic serves both the form-level and per-field passes. +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 validator component that combines form-level and per-field asynchronous validation, see the following sample: +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"::: -Write the validator as an `async` method so that an exception thrown before the first `await` is captured into the returned task rather than thrown from `RegisterAsyncFieldValidator`. To cancel from an additional source, create a linked token source inside the validator with . +For asynchronous validation attributes on the model, see . -Validators should clear prior messages for the field up front, as the preceding example does, and avoid writing partial results on a path that might throw. +:::moniker-end -### Cancellation and faults +Validator component code runs where the component runs. In Interactive WebAssembly it runs in the browser, and in Interactive Server it runs on the server over the circuit. -A validation that's canceled because it was superseded, or because the caller's token was canceled, is discarded silently and doesn't change the field's faulted state. +:::moniker range=">= aspnetcore-8.0" -A validation that fails for any other reason places the field in the *faulted* state. This includes a validation that completes as canceled due to an unrelated source, such as an or database timeout. Such a cancellation is treated as an infrastructure fault rather than as success, so a field is never reported as valid because its validation didn't finish. +In static SSR, validator component code runs on the server during the form post and doesn't provide live .NET field validation between requests. -For how to display pending and faulted state in the UI, see . +:::moniker-end -### Calling `ValidateAsync` from a custom submit handler + + -When a form uses instead of , call `ValidateAsync` from the handler to await any registered asynchronous work before deciding whether to proceed: +## Remote validation from Interactive WebAssembly -```razor - - - - - +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 is useful when a rule requires private server data, an external service, or other logic that shouldn't run in the browser. -@code { - private EditContext _editContext = default!; +The form: - protected override void OnInitialized() => - _editContext = new EditContext(Model); +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. - private async Task HandleSubmitAsync() - { - if (await _editContext.ValidateAsync(CancellationToken.None)) - { - await RegisterAsync(); - } - } -} -``` +`OnValidSubmit` only means that local validation succeeded. Process or save the model only after remote validation also succeeds. -The synchronous method is obsolete as of .NET 11. Call `ValidateAsync` instead. `Validate` continues to work for forms that only have synchronous validators, but it throws an if a handler attempts to register asynchronous work during the pass. +> [!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. -### Async validation across rendering modes +:::moniker range=">= aspnetcore-11.0" -The asynchronous 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 asynchronous validation completes. +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-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). +:::moniker range=">= aspnetcore-8.0" -For more information, see . +If the WebAssembly form is prerendered, its client-side services must also be available during prerendering. For the available approaches, 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.* +### Validate with a Minimal API -Remote validation is supported in addition to Blazor Web App client/server-side validation: +Call in the server project to validate supported endpoint parameters before the handler runs. -* 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. +:::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. The approach demonstrated by the following guidance uses a validator component. +:::moniker range="= aspnetcore-10.0" -The following example is based on: +The `Microsoft.Extensions.Validation` APIs used for generated validation metadata are experimental in .NET 10. For details, see . -* 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). +:::moniker-end -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. +:::moniker range=">= aspnetcore-10.0" -> [!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/) +If the model is declared in the `.Client` project, register its generated validation metadata in both projects as described in . -### Create the shared model +The endpoint adds a private business rule and returns errors keyed by model member name: -Create a `Starship` folder in the `.Client` project of the Blazor Web App. +```csharp +app.MapPost("/api/starships/validate", (StarshipModel model) => +{ + Dictionary errors = []; -Place the following `StarshipModel` model (`StarshipModel.cs`) into the `Starship` folder ***and*** into the Minimal API project of the solution. + if (model.Classification == "Defense" && + string.IsNullOrWhiteSpace(model.Description)) + { + errors[nameof(model.Description)] = + ["A defense ship requires a description."]; + } -> [!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)] + if (errors.Count > 0) + { + return Results.ValidationProblem(errors); + } -The following `StarshipModel` model is placed in the `Starship` folder of the `.Client` project ***and*** in the Minimal API project of the solution. Set the namespace appropriately for each project: the sample uses `BlazorSample.Client.Starship` in the Blazor Web App and `MinimalApiJwt.Models` in the Minimal API project. Some developers prefer a different folder scheme. If you position the classes in different locations, set the namespaces appropriately. + return Results.NoContent(); +}); +``` -`Starship/StarshipModel.cs` (Blazor Web App) or `StarshipModel.cs` (Minimal API project): +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`. -:::code language="csharp" source="~/../blazor-samples/10.0/BlazorWebAppRemoteValidation/BlazorSample.Client/Starship/StarshipModel.cs"::: +:::moniker-end -### Create the validation abstraction +:::moniker range="< aspnetcore-10.0" -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. +### Validate with an API controller -`Starship/IFormValidation.cs`: +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 -namespace BlazorSample.Client.Starship; - -public interface IFormValidation +[ApiController] +[Route("api/starships/validate")] +public class StarshipValidationController : ControllerBase { - 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`: - -:::code language="csharp" source="~/../blazor-samples/10.0/BlazorWebAppRemoteValidation/BlazorSample.Client/Starship/ClientFormValidation.cs"::: - -### Create the server form validator - -Create a `Starship` folder in the server project of the Blazor Web App. + [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."); + } -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. + if (!ModelState.IsValid) + { + return ValidationProblem(ModelState); + } -`Starship/ServerFormValidation.cs`: + return NoContent(); + } +} +``` -:::code language="csharp" source="~/../blazor-samples/10.0/BlazorWebAppRemoteValidation/BlazorSample/Starship/ServerFormValidation.cs"::: +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. -### Register the server form validator +:::moniker-end -In the `Program` file of the Blazor Web App: +### Call the endpoint and display errors -* 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. +Register an `HttpClient` in the WebAssembly project with the app's base address: ```csharp -builder.Services.AddScoped(); +builder.Services.AddScoped(sp => + new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) }); +``` -... +Place the `CustomValidation` component from [Build a validator component](#build-a-validator-component) in the form: -app.MapPost("/starship-validation", (IFormValidation formValidator, - StarshipModel model) => -{ - return formValidator.ValidateStarshipFormAsync(model); -}).RequireAuthorization(); -``` +```razor +@using System.Net +@using System.Net.Http.Json +@inject HttpClient Http -### Register the client form validator + + + + -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); -}); -``` +@code { + private StarshipModel Model { get; } = new StarshipModel(); + private CustomValidation? _remoteErrors; -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. + private async Task Submit() + { + using var response = await Http.PostAsJsonAsync( + "api/starships/validate", Model); -### Add the validation endpoint to the Minimal API + if (response.IsSuccessStatusCode) + { + // Process or save the model. + return; + } -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. + if (response.StatusCode == HttpStatusCode.BadRequest) + { + var problem = await response.Content + .ReadFromJsonAsync(); -In the `Program` file of the Minimal API project: + if (problem is not null) + { + _remoteErrors!.DisplayErrors(problem.Errors); + } -```csharp -app.MapPost("/api-starship-validation", ( - StarshipModel model, ILogger logger) => -{ - Dictionary errors = []; + return; + } - if (model.Classification == "Defense" && string.IsNullOrEmpty(model.Description)) - { - errors.Add(nameof(model.Description), - ["For a 'Defense' ship, 'Description' is required."]); + response.EnsureSuccessStatusCode(); } - if (errors.Count > 0) + private sealed class ValidationProblemResponse { - 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"); + public Dictionary Errors { get; set; } = + new Dictionary(); } - - 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): +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. -```csharp -builder.Services.AddValidation(); -``` +:::moniker range=">= aspnetcore-11.0" -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. +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. -### Add the validator component +:::moniker-end -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. +:::moniker range="= aspnetcore-10.0" -`CustomValidation.cs`: +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. -:::code language="csharp" source="~/../blazor-samples/10.0/BlazorWebAppRemoteValidation/BlazorSample.Client/CustomValidation.cs"::: +:::moniker-end -> [!NOTE] -> This is the same `CustomValidation` component described in the [Validator components](#validator-components) section. + -### Update the form to display validation errors +Validation CSS class customization is covered in . -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`. +## Additional resources -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). +:::moniker range=">= aspnetcore-10.0" -:::code language="razor" source="~/../blazor-samples/10.0/BlazorWebAppRemoteValidation/BlazorSample.Client/Pages/Starship10.razor"::: +* +* -> [!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 . +:::moniker-end -### Add a navigation entry - -To reach the form easily, add the following entry to the `NavMenu` component (`Layout/NavMenu.razor`) in the `.Client` project: - -```razor - -``` - -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 . - -:::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 . - -> [!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 - -:::moniker range=">= aspnetcore-7.0" - -## Customize validation CSS classes - -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; -} -``` - -### Style all fields - -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" - -:::code language="csharp" source="~/../blazor-samples/8.0/BlazorSample_BlazorWebApp/CustomFieldClassProvider.cs"::: - -:::moniker-end - -:::moniker range=">= aspnetcore-7.0 < aspnetcore-8.0" - -:::code language="csharp" source="~/../blazor-samples/7.0/BlazorSample_WebAssembly/CustomFieldClassProvider.cs"::: - -:::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" - -### Style a single field - -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" - -:::code language="csharp" source="~/../blazor-samples/8.0/BlazorSample_BlazorWebApp/CustomFieldClassProvider2.cs"::: - -:::moniker-end - -:::moniker range=">= aspnetcore-7.0 < aspnetcore-8.0" - -:::code language="csharp" source="~/../blazor-samples/7.0/BlazorSample_WebAssembly/CustomFieldClassProvider2.cs"::: - -:::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. - -### Apply Blazor's default classes to other fields - -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" - -:::code language="csharp" source="~/../blazor-samples/8.0/BlazorSample_BlazorWebApp/CustomFieldClassProvider3.cs"::: - -:::moniker-end - -:::moniker range=">= aspnetcore-7.0 < aspnetcore-8.0" - -:::code language="csharp" source="~/../blazor-samples/7.0/BlazorSample_WebAssembly/CustomFieldClassProvider3.cs"::: - -:::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 - -## Additional resources - -* -* -* - -:::moniker range=">= aspnetcore-10.0" - -* - -:::moniker-end - -:::moniker range=">= aspnetcore-11.0" +:::moniker range=">= aspnetcore-11.0" * :::moniker-end - diff --git a/aspnetcore/blazor/forms/validation-client-side.md b/aspnetcore/blazor/forms/validation-client-side.md index fc8b77b0ecef..8d43f5b9697c 100644 --- a/aspnetcore/blazor/forms/validation-client-side.md +++ b/aspnetcore/blazor/forms/validation-client-side.md @@ -12,51 +12,24 @@ uid: blazor/forms/validation-client-side [!INCLUDE[](~/includes/not-latest-version.md)] -This article explains how Blazor validates forms in the browser when the form uses [static server-side rendering (static SSR)](xref:blazor/components/render-modes#static-server-side-rendering-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 validate through the live pipeline and don't use the feature described in this article. For validation that applies to every render mode, see . +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 in the browser before the form is submitted. The user sees validation errors without a round trip to the server. +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 both of the following conditions are met: +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. -No JavaScript configuration, additional package, or service registration is required. +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 a security boundary. It can be bypassed by disabling or modifying the browser's JavaScript. Server-side validation runs after the form is posted and remains authoritative. Never rely on client-side validation to protect data integrity. - -### The .NET model remains the source of truth - -Validation rules aren't authored separately for the client. The server derives them from the data annotations attributes on the form's model and renders them into the page, so the client-side rules can't drift from the server-side rules. - -The rules are carried in a single inert custom element that Blazor appends to the form: - -```html - -``` - -Because the payload is held in an attribute rather than as element content, the element renders nothing and needs no CSS to remain hidden. - -> [!NOTE] -> Although the carrier element is invisible, it's a real element in the DOM and is a child of the form. CSS selectors that depend on element position, such as `:last-child`, `:nth-child()`, and adjacent sibling combinators (`+`), can match differently in a form that has client-side validation enabled. - -## Fields that receive client-side rules - -Client-side rules are only emitted for fields that the server also validates when the form is submitted. A field that the server ignores never receives a client-side rule. - -This matters for models with nested objects and collections. Validating nested members requires , so: - -* When the app calls and the model is discovered, nested members are validated on the server and receive client-side rules. -* Otherwise, only top-level properties are validated on the server, so only top-level properties receive client-side rules. - -Adopting therefore changes which fields are validated in the browser. For more information, see . - -The rule prevents client-side validation from suggesting coverage that the authoritative server-side pass doesn't provide, which would give a false sense of security. +> 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 @@ -75,12 +48,7 @@ The following * -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. To supply a client-side rule for a custom attribute, see the [Custom client-side validation rules](#custom-client-side-validation-rules) section. - -> [!NOTE] -> A with a non-numeric operand type, such as a date range, doesn't produce a client-side rule. The range is still enforced server-side. - -The and client-side validators intentionally accept the same input as their .NET counterparts rather than applying stricter rules. Apps that require stricter checks can register a custom validator. +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 @@ -90,15 +58,17 @@ After a field has shown a validation error, or after the form has been submitted 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 and accessibility +## Validation messages, localization, and accessibility -The and components display client-side validation errors without any changes. +Client-side validation uses to display messages for individual fields and to display messages for the whole form, as interactive validation does. -ARIA attributes on input elements and on validation message containers are managed by Blazor automatically, so assistive technologies announce validation errors without additional configuration. +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, so one stylesheet covers both: +The client-side validation engine applies the same CSS classes as Blazor's interactive validation: | Element | Classes | |---|---| @@ -106,23 +76,11 @@ The client-side validation engine applies the same CSS classes as Blazor's inter | Validation message | `validation-message` | | Validation summary | `validation-summary-errors` or `validation-summary-valid` | -Because the class names match the interactive render modes, the stylesheet included in the Blazor project templates styles static SSR validation and interactive validation identically with no additional configuration. - 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. -## Enhanced navigation - -Client-side validation is preserved across [enhanced navigation](xref:blazor/fundamentals/navigation#enhanced-navigation-and-form-handling). When a user navigates to a page that contains a static SSR form, the form is wired up automatically, including when the page update replaces one form with another. Multiple forms on the same page validate independently of each other. - -## Streaming rendering - -Inputs added to a form by a later [streaming rendering](xref:blazor/components/rendering#streaming-rendering) update aren't covered by client-side validation. They're still validated on the server when the form is submitted. - -A form that's delivered in a single streamed batch is covered normally. This limitation only applies when inputs are added to a form that has already rendered. - ## Opt out of client-side validation -Server-side validation is unaffected by every option in this section. Only the in-browser check is disabled. +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 @@ -143,7 +101,7 @@ builder.Services.AddRazorComponents(options => }); ``` -The global option takes precedence. When it's set to `true`, no form emits client-side validation rules, and a form can't opt back in with `DisableClientValidation="false"` on its component. +The global option takes precedence. When it's set to `true`, no form emits client-side validation rules. ### Opt out for a single submit button @@ -153,24 +111,16 @@ Use the standard HTML `formnovalidate` attribute on the button. The form is post ``` -This is useful for a "save draft" or "back" button that shouldn't require a completely valid form. - -## Localized validation messages - -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 . +This can be used to implement a "save draft" or "back" button that do not require a completely valid form for the submit to succeed. ## Custom client-side validation rules -A custom validation attribute isn't enforced in the browser by default because the framework has no way to execute arbitrary .NET validation logic on the client. To enforce a custom rule client-side, supply the rule on the server and register a matching validator function on the client. Both halves are required: a rule with no matching validator has no effect, and a validator with no matching rule is never called. +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 a rule from a validation attribute +### 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 framework attaches each rule's resolved error message, including the localized message when localization is configured, so the attribute supplies only the rule's shape. - 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"::: @@ -179,19 +129,46 @@ 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 matching client-side validator +### 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. -Register a validator function with the same rule name using `addValidator`. +> [!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. -The `Blazor.formValidation` service is created while Blazor starts, so it isn't available to script that runs before start-up completes. Register the validator from a [JavaScript initializer](xref:blazor/fundamentals/startup#javascript-initializers), which receives the `Blazor` instance after start-up. +Register custom validators once from app startup code. Choose the registration location based on how Blazor starts: -In a JavaScript initializer file named `{APP NAMESPACE}.lib.module.js` placed in the app's `wwwroot` folder, where the `{APP NAMESPACE}` placeholder is the app's namespace: +| 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"::: -Rule names are matched exactly, so the name passed to `addValidator` must match the `ClientValidationRule` `Name` value, including casing. +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, place the same registration calls in the promise continuation: + +```javascript +Blazor.start().then(() => { + registerCustomValidators(Blazor); +}); +``` + +In the preceding example, `registerCustomValidators` contains the app's `addValidator` calls. -Registering the validator after start-up is sufficient even for a form that's already on the page. The rule is already present in the rendered metadata, and the engine resolves the validator function by name when validation runs. +> [!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: @@ -201,28 +178,27 @@ The validator receives a context object with the following members: | `element` | The `input`, `select`, or `textarea` element being validated. | | `params` | The rule's `Parameters` as a string dictionary. | -The validator returns `{ success: true }` when the value is valid. Return `{ success: false }` to use the rule's server-supplied message, or `{ success: false, message: '...' }` to override the message for that call. +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 `{ success: false, message: '...' }` to override the message for that call. -> [!NOTE] -> A validator function is synchronous. Client-side validation is intended for immediate feedback, so rules that require a network call or other asynchronous work should be validated on the server. For asynchronous validation in interactive render modes, see . +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. -Empty values are conventionally treated as valid by rules other than `required`, which allows an optional field to remain empty while still being validated when a value is present. +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 programmatically +## Validate form on demand -The `Blazor.formValidation` API also exposes methods for validating on demand: +The `Blazor.formValidation` API also exposes JavaScript methods for validating on demand: | Method | Description | |---|---| -| `addValidator(name, validator)` | Registers a custom validator for a rule name. | | `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. | -## Replace rule generation - -To take complete control of the validation metadata rendered for a form, implement `ClientValidationProvider` and register it in the service container. The provider returns a that renders the metadata for the fields that were rendered in the form, or `null` when there's nothing to emit. +## Limitations -This is an advanced extensibility point for scenarios such as sourcing rules from a system other than data annotations. Most apps use the built-in provider and, when a custom rule is needed, implement `IClientValidationRuleProvider` instead. +* 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 a8799e9ed843..4154143d0efa 100644 --- a/aspnetcore/blazor/forms/validation.md +++ b/aspnetcore/blazor/forms/validation.md @@ -14,40 +14,38 @@ uid: blazor/forms/validation This article explains how to validate user input in Blazor forms. -Blazor validates a form's model using [data annotations attributes](xref:System.ComponentModel.DataAnnotations), the same attributes used elsewhere in ASP.NET Core. Most forms only require adding a component to an and annotating the model. +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. -More advanced scenarios are covered in separate articles: +Related articles provide more detail: :::moniker range=">= aspnetcore-11.0" -* : How forms that use static server-side rendering (static SSR) are validated in the browser before submission. -* : Driving validation directly with , writing validator components, and remote validation. -* : Behavior shared with Minimal APIs, including writing custom rules, validating nested objects and collections, and localizing messages. +* 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 . :::moniker-end :::moniker range="= aspnetcore-10.0" -* : Driving validation directly with , writing validator components, and remote validation. -* : Behavior shared with Minimal APIs, including validating nested objects and collections. +* 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-10.0" -* : Driving validation directly with , writing validator components, and remote validation. +* For complete validator-component and remote-validation implementations, see . :::moniker-end -## Validate a form with data annotations + -To validate a form: +## Validate with data annotations -1. Annotate the model's properties with [validation attributes](xref:mvc/models/validation#built-in-attributes). -1. Add a component inside the component. -1. Display errors with or components. +The following model uses and : -The following model uses the and attributes: +`Starship.cs`: ```csharp using System.ComponentModel.DataAnnotations; @@ -62,7 +60,7 @@ public class Starship } ``` -The following form validates the model. The callback is only invoked when validation passes: +Add the model to an `EditForm`, include `DataAnnotationsValidator`, and display errors with or . The callback is invoked only when validation succeeds: ```razor @@ -72,655 +70,552 @@ The following form validates the model. The - +

      +

      - +

      @code { - private Starship? Model { get; set; } - - protected override void OnInitialized() => Model ??= new(); + private Starship Model { get; } = new Starship(); - private void Submit() { /* Process the valid form. */ } + private void Submit() + { + // Process the valid form. + } } ``` -Without a component, the model's validation attributes have no effect on the form. +:::moniker range=">= aspnetcore-8.0" -### When validation runs +For a static SSR form post, assign a unique `FormName` and receive the posted model with `[SupplyParameterFromForm]`: -Blazor performs two types of validation: +```razor + + ... + -* *Field validation* runs when the user changes a field and moves out of it. The component associates all reported validation results with that field. -* *Model validation* runs when the form is submitted. The component determines the field for each result from the member name that the result reports. Results that aren't associated with an individual member are associated with the model rather than a field. +@code { + [SupplyParameterFromForm] + private Starship? Model { get; set; } -:::moniker range=">= aspnetcore-10.0" + protected override void OnInitialized() => Model ??= new(); +} +``` -### `DataAnnotationsValidator` validation behavior +For more information about form submission and model binding across render modes, see and . -The component has the same validation order and short-circuiting behavior as . The following rules are applied when validating an instance of type `T`: +:::moniker-end -1. Member properties of `T` are validated, including recursively validating nested objects. -1. Type-level attributes of `T` are validated. -1. The method is executed, if `T` implements it. +Without a `DataAnnotationsValidator` component, validation attributes on the model don't participate in the form's validation. -If one of the preceding steps produces a validation error, the remaining steps are skipped. +### When validation runs -:::moniker-end +Blazor performs field validation and full-form validation: -### Data Annotations Validator component and custom validation +* 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). -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: +:::moniker range=">= aspnetcore-11.0" -* [`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) +A static SSR form can provide live browser feedback with . The form is validated again authoritatively on the server when posted. -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 . +:::moniker-end -[!INCLUDE[](~/includes/aspnetcore-repo-ref-source-links.md)] +:::moniker range=">= aspnetcore-8.0 < aspnetcore-11.0" -In custom validation scenarios: +A static SSR form is validated on the server when posted and doesn't provide live field validation between requests. -* 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). +:::moniker-end -Two general approaches are available for validation logic that isn't declared on the model, both described in : +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. -* Manual validation using the 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: 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). +:::moniker range=">= aspnetcore-10.0" -## Validation Summary and Validation Message components +### Configure data annotations validation -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): +`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: -```razor - +```csharp +builder.Services.AddValidation(); ``` -Output validation messages for a specific model with the `Model` parameter: - -```razor - -``` +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: -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: +:::moniker-end -```razor - -``` +:::moniker range=">= aspnetcore-11.0" -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. +| 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. | -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: +:::moniker-end -```css -.validation-message { - color: red; -} -``` +:::moniker range="= aspnetcore-10.0" -### Validation state CSS classes +| Configuration | Behavior | +|---|---| +| Generated metadata is available | Validates nested objects and collections. | +| Generated metadata isn't available | Validates top-level properties only. | -Blazor applies CSS classes to input elements and validation components to reflect validation state. The classes make it possible to style validation without writing any C#: +The `ValidatableTypeAttribute` and `SkipValidationAttribute` APIs are experimental in .NET 10. For details and available workarounds, see . -| 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` | +:::moniker-end -The stylesheet included in the Blazor project templates styles these classes, so a form gets validation styling with no additional configuration. For example, the following rule outlines a field that the user has edited and that's currently valid: +:::moniker range=">= aspnetcore-10.0" -```css -.valid.modified:not([type=checkbox]) { - outline: 1px solid #26b050; -} -``` +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. -To supply different class names, for example to integrate with a CSS framework such as [Bootstrap](https://getbootstrap.com/), see . +For configuration requirements, validation order, custom rules, nested object graphs, and generated metadata, see . -:::moniker range=">= aspnetcore-8.0" +:::moniker-end -### Determine if a form field is valid +:::moniker range="< aspnetcore-10.0" -Use to determine if a field is valid without obtaining validation messages. +### Validate nested object graphs - Supported, but not recommended: +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): -```csharp -var isValid = !editContext.GetValidationMessages(fieldIdentifier).Any(); +```razor + + + ... + ``` - Recommended: - ```csharp -var isValid = editContext.IsValid(fieldIdentifier); +public class Starship +{ + [ValidateComplexType] + public ShipDescription Description { get; set; } = + new ShipDescription(); +} ``` +The package remains experimental in these framework versions. + :::moniker-end -## Choose the validation your form needs +:::moniker range="< aspnetcore-6.0" -The default configuration validates the top-level properties of the form's model. Some scenarios require additional setup. Use the following table to find the guidance for a goal: +### `[CompareProperty]` attribute -:::moniker range=">= aspnetcore-11.0" - -| Goal | What to do | -|---|---| -| Validate top-level properties with built-in attributes | Nothing further. Add a component to the form, as shown earlier in this article. | -| Express a rule that built-in attributes can't | Write a [custom validation attribute or implement `IValidatableObject`](xref:fundamentals/validation#write-custom-validation-rules). For validation logic that isn't declared on the model, see . | -| Validate properties of nested objects and collection items | Call `AddValidation` and annotate the root model type. See . | -| Validate against a database or web API | Use [asynchronous validation](xref:fundamentals/validation#asynchronous-validation-support), or a [validator component](xref:blazor/forms/validation-advanced). | -| Display error messages in the user's language | See [Localize validation messages](xref:fundamentals/validation#localize-validation-messages). | -| Give immediate feedback in a static SSR form | Supported automatically. See . | +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. :::moniker-end -:::moniker range="= aspnetcore-10.0" - -| Goal | What to do | -|---|---| -| Validate top-level properties with built-in attributes | Nothing further. Add a component to the form, as shown earlier in this article. | -| Express a rule that built-in attributes can't | Write a [custom validation attribute](xref:mvc/models/validation#custom-attributes) or implement [`IValidatableObject`](xref:mvc/models/validation#ivalidatableobject). For validation logic that isn't declared on the model, see . | -| Validate properties of nested objects and collection items | Call `AddValidation` and annotate the root model type. See . | -| Validate against a database or web API | Use a [validator component](xref:blazor/forms/validation-advanced). | + -:::moniker-end +:::moniker range=">= aspnetcore-10.0" -:::moniker range="< aspnetcore-10.0" +### Write model-based custom rules -| Goal | What to do | -|---|---| -| Validate top-level properties with built-in attributes | Nothing further. Add a component to the form, as shown earlier in this article. | -| Express a rule that built-in attributes can't | Write a [custom validation attribute](xref:mvc/models/validation#custom-attributes) or implement [`IValidatableObject`](xref:mvc/models/validation#ivalidatableobject). For validation logic that isn't declared on the model, see . | -| Validate properties of nested objects and collection items | See [Nested objects, collection types, and complex types](#nested-objects-collection-types-and-complex-types). | -| Validate against a database or web API | Use a [validator component](xref:blazor/forms/validation-advanced). | +When built-in attributes can't express a rule, use a custom or . For detailed guidance, see . :::moniker-end -:::moniker range=">= aspnetcore-10.0" - -### Nested objects and collections require additional configuration - -By default, the component validates the top-level properties of the model. Validation attributes on the properties of a nested object, or on the items of a collection, aren't evaluated. +:::moniker range="< aspnetcore-10.0" -To validate a nested object graph, opt into by calling and annotating the root model type with . The model types must be declared in C# files (`.cs`), not in Razor component files (`.razor`). +### Write model-based custom rules -For the full guidance and an example, see . +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`. -> [!WARNING] -> A model that isn't discovered by the validation source generator doesn't produce a build error or a log entry. The form silently validates only the top-level properties, and validation messages are not localized. If nested validation or localization appears to have no effect, see [Validation when `AddValidation` isn't called](xref:fundamentals/validation#validation-when-addvalidation-isnt-called). +When returning a from a custom attribute, include the validated member name so the result can appear in that field's `ValidationMessage` component. :::moniker-end -## Custom validation rules +:::moniker range=">= aspnetcore-7.0 < aspnetcore-10.0" -When the built-in validation attributes can't express a rule, declare the rule on the model with a custom or by implementing . Both are executed by the component wherever the form runs. + -:::moniker range=">= aspnetcore-10.0" - -For guidance on writing these rules, which is shared with Minimal APIs, see . +Custom attributes can resolve registered services through . :::moniker-end -:::moniker range="< aspnetcore-10.0" +## Add validation through `EditContext` -For guidance on writing these rules, see [Custom attributes](xref:mvc/models/validation#custom-attributes) and [`IValidatableObject`](xref:mvc/models/validation#ivalidatableobject). - -:::moniker-end +`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. -When validation logic can't be declared on the model, for example when messages come from a web API response, use a validator component or drive validation directly with . See . +Custom validation commonly uses: -Of the [built-in data annotations validators](xref:mvc/models/validation#built-in-attributes), only the [`[Remote]` validation attribute](xref:mvc/models/validation#remote-attribute) isn't supported in Blazor. +* for full-form validation. +* for field validation. +* to add and clear messages. +* to notify the UI after messages change. -### Associate a validation result with a field +The following interactive-form pattern adds a form-level business rule alongside data annotations validation and rechecks the rule when either relevant field changes: -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 . Without a member name, the message is associated with the model rather than the field, so it doesn't appear in the field's component. +```razor +@implements IDisposable -`CustomValidator.cs`: + + + -:::moniker range=">= aspnetcore-8.0" + ... + -```csharp -using System; -using System.ComponentModel.DataAnnotations; +@code { + private Starship Model { get; } = new Starship(); + private EditContext _editContext = default!; + private ValidationMessageStore _messages = default!; -public class CustomValidator : ValidationAttribute -{ - protected override ValidationResult IsValid(object? value, - ValidationContext validationContext) + protected override void OnInitialized() { - ... - - return new ValidationResult("Validation message to user.", - [ validationContext.MemberName! ]); + _editContext = new EditContext(Model); + _messages = new ValidationMessageStore(_editContext); + _editContext.OnValidationRequested += ValidateBusinessRules; + _editContext.OnFieldChanged += ValidateChangedField; } -} -``` - -:::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) + private void ValidateBusinessRules( + object? sender, ValidationRequestedEventArgs e) { - ... - - return new ValidationResult("Validation message to user.", - new[] { validationContext.MemberName! }); + _messages.Clear(); + ValidateIdentifier(); + _editContext.NotifyValidationStateChanged(); } -} -``` -:::moniker-end + private void ValidateChangedField( + object? sender, FieldChangedEventArgs e) + { + if (e.FieldIdentifier.FieldName != nameof(Starship.Identifier) && + e.FieldIdentifier.FieldName != nameof(Starship.MaximumAccommodation)) + { + return; + } -:::moniker range="< aspnetcore-6.0" + _messages.Clear( + _editContext.Field(nameof(Starship.Identifier))); + ValidateIdentifier(); + _editContext.NotifyValidationStateChanged(); + } -```csharp -using System; -using System.ComponentModel.DataAnnotations; + private void ValidateIdentifier() + { + if (Model.MaximumAccommodation == 1 && + string.IsNullOrWhiteSpace(Model.Identifier)) + { + _messages.Add( + _editContext.Field(nameof(Starship.Identifier)), + "An identifier is required for a single-occupant ship."); + } + } -public class CustomValidator : ValidationAttribute -{ - protected override ValidationResult IsValid(object value, - ValidationContext validationContext) + private void Submit() { - ... + // Process the valid form. + } - return new ValidationResult("Validation message to user.", - new[] { validationContext.MemberName }); + public void Dispose() + { + _editContext.OnValidationRequested -= ValidateBusinessRules; + _editContext.OnFieldChanged -= ValidateChangedField; } } ``` -:::moniker-end - -### Inject services into a custom validation attribute - -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: +An `OnFieldChanged` handler receives the changed field in `e.FieldIdentifier`. Clear or replace the affected messages and call `NotifyValidationStateChanged`, as the preceding example demonstrates. -```csharp -builder.Services.AddTransient(); -``` +:::moniker range=">= aspnetcore-8.0" -The `IsValid` method of the following `SaladChefValidatorAttribute` class obtains the `SaladChef` service from DI to check the user's input. +Static SSR doesn't provide live .NET field validation between requests. -`SaladChefValidatorAttribute.cs`: +:::moniker-end -:::code language="csharp" source="~/../blazor-samples/8.0/BlazorSample_BlazorWebApp/SaladChefValidatorAttribute.cs"::: +:::moniker range=">= aspnetcore-11.0" -The following component validates user input by applying the `SaladChefValidatorAttribute` (`[SaladChefValidator]`) to the salad ingredient string (`SaladIngredient`). +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. -`Starship12.razor`: +For model-based asynchronous validation attributes, see . For a complete reusable validator component, see . -:::moniker range=">= aspnetcore-9.0" +:::moniker-end -:::code language="razor" source="~/../blazor-samples/9.0/BlazorSample_BlazorWebApp/Components/Pages/Starship12.razor"::: +For a reusable implementation that encapsulates event subscriptions and its message store, see . -:::moniker-end + -:::moniker range=">= aspnetcore-8.0 < aspnetcore-9.0" +## Display validation messages -:::code language="razor" source="~/../blazor-samples/8.0/BlazorSample_BlazorWebApp/Components/Pages/Starship12.razor"::: +Use to display messages associated with one field: -:::moniker-end +```razor + +``` -:::moniker range="< aspnetcore-8.0" +Use to display messages for the form: ```razor -@page "/starship-12" -@inject SaladChef SaladChef + +``` - - -

        - -

        - -
          - @foreach (var message in context.GetValidationMessages()) - { -
        • @message
        • - } -
        -
        +Assign the summary's `Model` parameter to restrict it to messages associated with a particular model: -@code { - private string? saladToppers; +```razor + +``` - [SaladChefValidator] - public string? SaladIngredient { get; set; } +To inspect current messages in code, call : - protected override void OnInitialized() => - saladToppers ??= string.Join(", ", SaladChef.SaladToppers); -} +```csharp +var allMessages = _editContext.GetValidationMessages(); +var fieldMessages = _editContext.GetValidationMessages( + _editContext.Field(nameof(Starship.Identifier))); ``` -:::moniker-end +These methods read the current validation state. They don't initiate validation. -### Class-level validation with `IValidatableObject` +## Customize validation appearance -[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. +Blazor applies CSS classes that represent field and message state: -:::moniker range="< aspnetcore-10.0" +| 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` | + +:::moniker range=">= aspnetcore-11.0" -## Nested objects, collection types, and complex types +Inputs with asynchronous field validation use `pending` or `faulted`, optionally with `modified`, instead of `valid` or `invalid` while the corresponding state applies. -> [!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. +:::moniker-end -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. +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. -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: +:::moniker range=">= aspnetcore-5.0" -```razor - - - ... - -``` +To change the classes applied to input components, derive from . -Annotate model properties with `[ValidateComplexType]`. In the following model classes, the `ShipDescription` class contains additional data annotations to validate when the model is bound to the form: +:::moniker-end -`Starship.cs`: +:::moniker range=">= aspnetcore-8.0" ```csharp -using System; -using System.ComponentModel.DataAnnotations; +using Microsoft.AspNetCore.Components.Forms; -public class Starship +public sealed class BootstrapFieldCssClassProvider : FieldCssClassProvider { - ... - - [ValidateComplexType] - public ShipDescription ShipDescription { get; set; } = new(); + public override string GetFieldCssClass( + EditContext editContext, + in FieldIdentifier fieldIdentifier) + { + if (!editContext.IsModified(fieldIdentifier)) + { + return string.Empty; + } - ... + return editContext.IsValid(fieldIdentifier) + ? "is-valid" + : "is-invalid"; + } } ``` -`ShipDescription.cs`: +:::moniker-end + +:::moniker range=">= aspnetcore-5.0 < aspnetcore-8.0" ```csharp -using System; -using System.ComponentModel.DataAnnotations; +using System.Linq; +using Microsoft.AspNetCore.Components.Forms; -public class ShipDescription +public sealed class BootstrapFieldCssClassProvider : FieldCssClassProvider { - [Required] - [StringLength(40, ErrorMessage = "Description too long (40 char).")] - public string? ShortDescription { get; set; } + public override string GetFieldCssClass( + EditContext editContext, + in FieldIdentifier fieldIdentifier) + { + if (!editContext.IsModified(fieldIdentifier)) + { + return string.Empty; + } - [Required] - [StringLength(240, ErrorMessage = "Description too long (240 char).")] - public string? LongDescription { get; set; } + return editContext.GetValidationMessages(fieldIdentifier).Any() + ? "is-invalid" + : "is-valid"; + } } ``` :::moniker-end -:::moniker range="< aspnetcore-10.0" +:::moniker range=">= aspnetcore-11.0" -## Blazor data annotations validation package +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. -> [!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. +:::moniker-end -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*. +:::moniker range=">= aspnetcore-5.0" + +Assign the provider to the form's `EditContext`: + +```csharp +_editContext.SetFieldCssClassProvider( + new BootstrapFieldCssClassProvider()); +``` -> [!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. +For custom input markup, call to obtain the class selected by the current provider. :::moniker-end -:::moniker range="< aspnetcore-6.0" + -## `[CompareProperty]` attribute +## Respond to validation state -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). +`EditContext` exposes the current validation state without initiating validation. -:::moniker-end +* 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. -:::moniker range=">= aspnetcore-11.0" +:::moniker range=">= aspnetcore-8.0" -## Display pending and faulted validation state +Use `IsValid(field)` to determine whether a field currently has validation messages. -Asynchronous validation, such as a uniqueness check against a database, doesn't complete immediately. Blazor tracks the state of in-flight validation per field so that the UI can show progress and report failures. +:::moniker-end -To author asynchronous validation rules, see for attribute-based rules, or for validator components. +:::moniker range="< aspnetcore-8.0" -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: +For a field, the absence of messages can be checked with `!editContext.GetValidationMessages(field).Any()`. -| State | Per-field | Form-level (any field) | -|----------|----------------------------------------------------|----------------------------------| -| Pending | `EditContext.IsValidationPending(fieldIdentifier)` | `EditContext.IsValidationPending()` | -| Faulted | `EditContext.IsValidationFaulted(fieldIdentifier)` | `EditContext.IsValidationFaulted()` | +:::moniker-end -The per-field overloads accept either a or a `() => model.Property` lambda for convenient use in Razor markup: +The following example displays custom UI only after a field is modified and invalid: -```razor - - +:::moniker range=">= aspnetcore-8.0" -@if (EditContext.IsValidationPending(() => Model.Username)) -{ - Checking… +```razor +@{ + var identifier = _editContext.Field(nameof(Starship.Identifier)); } -else if (EditContext.IsValidationFaulted(() => Model.Username)) + +@if (_editContext.IsModified(identifier) && + !_editContext.IsValid(identifier)) { - - Validation could not be completed. - +

        Correct the identifier before continuing.

        } ``` -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: - -```razor - -``` - - 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: +:::moniker-end -```css -.pending { - background-image: url('spinner.gif'); - background-repeat: no-repeat; - background-position: right center; -} +:::moniker range="< aspnetcore-8.0" -.modified.pending { - border-color: lightblue; +```razor +@{ + var identifier = _editContext.Field(nameof(Starship.Identifier)); } -.modified.faulted { - border-color: orange; +@if (_editContext.IsModified(identifier) && + _editContext.GetValidationMessages(identifier).Any()) +{ +

        Correct the identifier before continuing.

        } ``` :::moniker-end -## Enable the submit button based on form validation - -To enable and disable the submit button based on form validation, the following example: +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`: -* Uses a shortened version of the earlier `Starfleet Starship Database` form (`Starship3` component) of the [Example form](xref:blazor/forms/input-components#example-form) section of the *Input components* article that only accepts a value for the ship's Id. The other `Starship` properties receive valid default values when an instance of the `Starship` type is created. -* Uses the form's to assign the model when the component is initialized. -* Validates the form in the context's callback to enable and disable the submit button. -* Implements and unsubscribes the event handler in the `Dispose` method. For more information, see . +```csharp +private void HandleValidationStateChanged( + object? sender, ValidationStateChangedEventArgs e) => + _ = InvokeAsync(StateHasChanged); +``` -> [!NOTE] -> When assigning to the , don't also assign an to the . +Unsubscribe from `OnValidationStateChanged` when the component is disposed. :::moniker range=">= aspnetcore-11.0" -> [!IMPORTANT] -> The synchronous method used by the following example is obsolete as of .NET 11. In new code, call `EditContext.ValidateAsync` and `await` the result, which also awaits any asynchronous validators registered for the form: -> -> ```csharp -> private async Task HandleFieldChanged(object? sender, FieldChangedEventArgs e) -> { -> formInvalid = !await editContext!.ValidateAsync(); -> StateHasChanged(); -> } -> ``` -> -> For more information, see . - -:::moniker-end +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. -`Starship14.razor`: +Live pending indicators require an interactive render mode. During a static SSR form post, server-side validation completes before the response is rendered. -:::moniker range=">= aspnetcore-9.0" +:::moniker-end -:::code language="razor" source="~/../blazor-samples/9.0/BlazorSample_BlazorWebApp/Components/Pages/Starship14.razor"::: +## Control form submission -:::moniker-end +`EditForm` provides three submission callbacks: -:::moniker range=">= aspnetcore-8.0 < aspnetcore-9.0" +| Callback | Behavior | +|---|---| +| | Runs after automatic validation succeeds. | +| | Runs after automatic validation fails. | +| | Gives the handler control of validation and submission. | -:::code language="razor" source="~/../blazor-samples/8.0/BlazorSample_BlazorWebApp/Components/Pages/Starship14.razor"::: +`OnValidSubmit` and `OnInvalidSubmit` can be used together. Don't combine `OnSubmit` with either of them. -:::moniker-end +:::moniker range=">= aspnetcore-11.0" -:::moniker range="< aspnetcore-8.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 -@page "/starship-14" -@implements IDisposable -@inject ILogger Logger - - - - -
        - -
        -
        - -
        + + ... @code { - private bool formInvalid = false; - private EditContext? editContext; - - private Starship? Model { get; set; } - - protected override void OnInitialized() + private async Task HandleSubmit(EditContext editContext) { - Model ??= - new() - { - Id = "NCC-1701", - Classification = "Exploration", - MaximumAccommodation = 150, - IsValidatedDesign = true, - ProductionDate = new DateTime(2245, 4, 11) - }; - editContext = new(Model); - editContext.OnFieldChanged += HandleFieldChanged; - } - - private void HandleFieldChanged(object? sender, FieldChangedEventArgs e) - { - if (editContext is not null) - { - formInvalid = !editContext.Validate(); - StateHasChanged(); - } - } - - private void Submit() - { - Logger.LogInformation("Submit called: Processing the form"); - } - - public void Dispose() - { - if (editContext is not null) + if (await editContext.ValidateAsync()) { - editContext.OnFieldChanged -= HandleFieldChanged; + 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 +For interactive forms, the form-level pending state can be used to disable submission while `ValidateAsync` is running: -If a form isn't preloaded with valid values and you wish to disable the **`Submit`** button on form load, set `formInvalid` to `true`. +```razor + +``` -A side effect of the preceding approach is that a validation summary ( component) is populated with invalid fields after the user interacts with any one field. Address this scenario in either of the following ways: +:::moniker-end -* Don't use a component on the form. -* Make the component visible when the submit button is selected (for example, in a `Submit` method). +:::moniker range="< aspnetcore-11.0" -```razor - - - +When handling `OnSubmit`, call before processing the form: +```razor + ... - - @code { - private string displaySummary = "display:none"; - - ... - - private void Submit() + private void HandleSubmit(EditContext editContext) { - displaySummary = "display:block"; + if (editContext.Validate()) + { + Save(); + } } } ``` +:::moniker-end + ## Additional resources -* * +* * -* +* :::moniker range=">= aspnetcore-10.0" @@ -728,3 +623,8 @@ A side effect of the preceding approach is that a validation summary ( + +:::moniker-end diff --git a/aspnetcore/fundamentals/validation.md b/aspnetcore/fundamentals/validation.md index 79ee5d0d5069..bc7fbf0fcfcc 100644 --- a/aspnetcore/fundamentals/validation.md +++ b/aspnetcore/fundamentals/validation.md @@ -10,69 +10,79 @@ uid: fundamentals/validation --- # Validation in ASP.NET Core - supports complex model validation in Blazor and Minimal API projects. + provides model validation for Blazor and Minimal API projects. -Validation rules are declared the same way in both frameworks, using [data annotations attributes](xref:System.ComponentModel.DataAnnotations) on a model type, and this article describes the behavior that both frameworks share: +Validation rules are declared the same way in both frameworks, using [data annotations attributes](xref:System.ComponentModel.DataAnnotations) and . This article describes the validation behavior that both frameworks share: -* Minimal APIs validate a request before the endpoint handler runs. For how validation is surfaced in an endpoint, see . -* Blazor validates a form model through the component. For how validation is surfaced in a form, see . +:::moniker range=">= aspnetcore-11.0" + +Asynchronous validation attributes and are also supported. + +:::moniker-end + +* Minimal APIs use the service to validate a request before the endpoint handler runs. For how validation is surfaced in an endpoint, see . +* Blazor uses the service through the component. For how validation is surfaced in a form, see . While the API in the [`Microsoft.Extensions.Validation` NuGet package](https://www.nuget.org/packages/Microsoft.Extensions.Validation) can be used in scenarios outside ASP.NET Core, this article focuses on ASP.NET Core. The API isn't supported for MVC or Razor Pages. For validation guidance that applies to MVC and Razor Pages, see . -## Enable validation +## Register validation services -To enable validation, call on in the app's `Program` file: +Call on in the app's `Program` file: ```csharp builder.Services.AddValidation(); ``` -For Minimal APIs, the implementation automatically discovers types that are defined in handlers or as base types of the types defined in handlers. An endpoint filter performs validation on these types and is added for each endpoint. +For Minimal APIs, this enables automatic validation of supported parameters before the endpoint handler runs. + +Blazor forms can perform basic top-level DataAnnotations validation without calling `AddValidation`. Registering the service enables nested object and collection validation when the form model is discovered by the validation source generator. + +:::moniker range=">= aspnetcore-11.0" + +Generated validation metadata also enables message localization. -Validation uses a source generator that only discovers validatable types in the assembly where `AddValidation` is called. If Minimal API endpoints are defined in a referenced assembly rather than the assembly where `AddValidation` is called, register validation as shown in the [Register validation in multi-assembly apps](#register-validation-in-multi-assembly-apps) section. +:::moniker-end + +Validation uses a source generator that creates metadata for validatable types in the assembly where `AddValidation` is called. For types declared in another assembly, see [Register validation across assemblies](#register-validation-across-assemblies). -### Validation when `AddValidation` isn't called +### Behavior without generated validation metadata -The consequence of omitting , or of calling it but not having a type discovered by the source generator, differs by framework: +The consequence of omitting , or of calling it without the required type being discovered by the source generator, differs by framework: :::moniker range=">= aspnetcore-11.0" -| Framework | Behavior without `Microsoft.Extensions.Validation` | +| Framework | Behavior without generated validation metadata | |---|---| -| Minimal APIs | No validation runs. Invalid requests reach the endpoint handler and return a `200 - OK` response instead of `400 - Bad Request`. | -| Blazor | The component falls back to , which validates top-level properties only. Nested objects, collection items, and [localized messages](#localize-validation-messages) aren't supported on the fallback path. | +| Minimal APIs | No automatic validation runs. Invalid input reaches the endpoint handler instead of being rejected before the handler executes. | +| Blazor | The component falls back to , which validates top-level properties only. Nested objects, collection items, and the [`Microsoft.Extensions.Validation` message-localization pipeline](#localize-validation-messages) aren't supported on the fallback path. | :::moniker-end :::moniker range="< aspnetcore-11.0" -| Framework | Behavior without `Microsoft.Extensions.Validation` | +| Framework | Behavior without generated validation metadata | |---|---| -| Minimal APIs | No validation runs. Invalid requests reach the endpoint handler and return a `200 - OK` response instead of `400 - Bad Request`. | +| Minimal APIs | No automatic validation runs. Invalid input reaches the endpoint handler instead of being rejected before the handler executes. | | Blazor | The component falls back to , which validates top-level properties only. Nested objects and collection items aren't validated on the fallback path. | :::moniker-end -In both cases there's no build error, exception, or log entry indicating that a type isn't validated. If validation appears to be skipped, confirm all of the following: +Missing metadata doesn't produce a runtime exception or log entry. Build analyzers report many unsupported configurations, but other missing-metadata cases might not produce a diagnostic. For checks to perform when expected validation is missing, see [Troubleshoot generated validation metadata](#troubleshoot-generated-validation-metadata). -* is called from the assembly that declares the validatable types. See [Register validation in multi-assembly apps](#register-validation-in-multi-assembly-apps). -* The model type is declared in a C# file (`.cs`), not in a Razor component file (`.razor`). See [Nested objects and collections](#nested-objects-and-collections). -* The root type is annotated with when the source generator can't reach it from an endpoint handler signature. See [Force-generate validatable type information](#force-generate-validatable-type-information). + -## Validatable entities +## How validation runs -Three types of entities can be validated: +Minimal APIs begin with endpoint parameter validation. Blazor begins with validation of the form's model. Both frameworks then use the same model validation order and object-graph traversal. -* [Parameters](#parameter-validation) (specific to Minimal API endpoint parameters) -* [Types](#type-validation) -* [Properties](#property-validation) + -### Parameter validation +### Minimal API parameter validation -Parameter validation is the first step in the validation pipeline for Minimal API endpoints. It involves the following steps: +For each supported endpoint parameter: -1. Validate instances applied to the Minimal API parameter. -1. If the parameter type is `IEnumerable`, validate the type for all non-`null` elements. Otherwise, validate the type for the value. +1. Validate instances applied directly to the parameter. +1. If the parameter value is an `IEnumerable`, validate each non-`null` element. Otherwise, validate the parameter value itself. :::moniker range="< aspnetcore-11.0" @@ -81,20 +91,75 @@ Parameter validation is the first step in the validation pipeline for Minimal AP :::moniker-end -### Type validation + + + +### Model validation order + +When validating a model: + +1. Validate the attributes on each property, then validate the property's value. If the value is an `IEnumerable`, validate each non-`null` element. If property validation produces an error, the remaining steps are skipped. +1. Validate attributes applied to the model type. If type-level validation produces an error, the remaining step is skipped. +1. Run if the model implements . + +### Nested objects and collections + +Validation recurses into nested objects and collection items, so a rule declared on a nested property is enforced when the root model is validated. Without generated validation metadata, Blazor only validates the top-level properties of a form model. + +To validate a nested object graph: + +1. Call in the `Program` file where services are registered. +1. Declare model types in C# files (`.cs`), not in Razor component files (`.razor`). +1. Annotate the root model type with (`[ValidatableType]`). Types reachable from the root are discovered automatically. -Type validation is the next step after parameter validation (and is the first step in Blazor). It involves the following steps: +In the following example, only the root `Order` type is annotated. The other model types are reachable from `Order` and are included in its validation graph. -1. Validate properties on the type. If any errors are found, the validation process stops. -1. Validate type-level instances. If any errors are found, the validation process stops. -1. Validate implementations. +`Order.cs`: -### Property validation +```csharp +using System.ComponentModel.DataAnnotations; +using Microsoft.Extensions.Validation; -Property validation happens as part of the type validation as explained in the previous section. It involves the following steps: +[ValidatableType] +public class Order +{ + public Customer Customer { get; set; } = new(); + public List OrderItems { get; set; } = []; +} -1. Validate instances applied to the property. -1. If the property value is `IEnumerable`, perform type validation for all non-`null` elements. Otherwise, perform a single type validation for the value. +public class Customer +{ + [Required(ErrorMessage = "Name is required.")] + public string? FullName { get; set; } + + public ShippingAddress ShippingAddress { get; set; } = new(); +} + +public class ShippingAddress +{ + [Required(ErrorMessage = "Street is required.")] + public string? Street { get; set; } +} + +public class OrderItem +{ + [Required(ErrorMessage = "Description is required.")] + public string? Description { get; set; } + + [Range(1, 1000)] + public int Quantity { get; set; } +} +``` + +Errors from nested members use paths such as `Customer.ShippingAddress.Street` or `OrderItems[0].Description`. + +For model types defined in another assembly or in a Blazor Web App's `.Client` project, see [Register validation across assemblies](#register-validation-across-assemblies). + + + +### Skip validation + +Apply to a parameter, type, or property that shouldn't be validated. ## Write custom validation rules @@ -182,23 +247,20 @@ For rules that require I/O, such as a database or web API call, see the [Asynchr :::moniker-end -> [!NOTE] -> In a Blazor form that uses static server-side rendering (static SSR), custom attributes aren't enforced by the browser unless the attribute also supplies a client-side rule. For more information, see . - :::moniker range=">= aspnetcore-11.0" - +:::moniker range=">= aspnetcore-11.0" ## Asynchronous validation support supports asynchronous validation. Apply custom implementations of `AsyncValidationAttribute` to parameters, types, or properties, and they're called asynchronously. In addition, types can implement `IAsyncValidatableObject` as well. -When validating properties on a type, all validation tasks are started concurrently. Similarly, elements of `IEnumerable` collections are validated concurrently. +Asynchronous validation operations can run in parallel, and their execution and completion order isn't guaranteed. Validation rules must not depend on a particular order. `IAsyncValidatableObject` and `AsyncValidationAttribute` require synchronous **and** asynchronous validation logic. For example, the `Validate` and `ValidateAsync` methods of `IAsyncValidatableObject` must be implemented for objects that use the interface. However, validation never calls both methods. If validation is called through an asynchronous code path, only `ValidateAsync` is called. If validation is called through a synchronous code path, only `Validate` is called. @@ -208,12 +270,13 @@ Blazor form validation calls the asynchronous path for per-field validation and If your implementation can't support the synchronous path, throw . -The following example demonstrates a validation class that implements the `IAsyncValidatableObject` interface. In the following scenario, validation requires an asynchronous call path to check a database for a valid email username via a hypothetical `IUserService` service. Because validation requires an asynchronous database call in this scenario, the synchronous `Validate` method, which is required by the interface's contract, shouldn't be called by developer code elsewhere and throws if it ever is called. +The following example shows object-level asynchronous validation with `IAsyncValidatableObject`. It uses a hypothetical `IUserService` to check a database for an existing email address. Because the rule requires asynchronous I/O, the required synchronous `Validate` implementation throws . ```csharp using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; +using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; @@ -250,71 +313,6 @@ public class ValidateUser : IAsyncValidatableObject :::moniker-end -## Nested objects and collections - -Validation recurses into nested objects and collection items, so a rule declared on a property of a nested type is enforced when the root model is validated. This is one of the main reasons to adopt : without it, only the top-level properties of a model are validated. - -To validate a nested object graph: - -1. Call in the `Program` file where services are registered. -1. Declare the model types in C# files (`.cs`), not in Razor component files (`.razor`). -1. Annotate the root model type with (`[ValidatableType]`). Types reachable from the root are discovered automatically. - -In the following example, only the root `Order` type is annotated. The `Customer`, `ShippingAddress`, and `OrderItem` types are discovered from it, and their validation attributes are enforced when an `Order` is validated. - -`Order.cs`: - -```csharp -using System.ComponentModel.DataAnnotations; -using Microsoft.Extensions.Validation; - -[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(); -} - -public class ShippingAddress -{ - [Required(ErrorMessage = "Street is required.")] - public string? Street { get; set; } - - [Required(ErrorMessage = "City is required.")] - public string? City { get; set; } -} - -public class OrderItem -{ - [Required(ErrorMessage = "Description is required.")] - public string? Description { get; set; } - - [Range(1, 1000, ErrorMessage = "Quantity must be between 1 and 1,000.")] - public int Quantity { get; set; } -} -``` - -Errors from nested members are reported with a path that identifies the member, such as `Customer.ShippingAddress.Street` or `OrderItems[0].Description`. - -### Model types can't be declared in Razor component files - -The requirement to declare model types outside of Razor components (`.razor`) exists because both the validation feature and the Razor compiler use source generators. Currently, the output of one source generator can't be used as the input to another source generator, so a type declared in a `.razor` file isn't discovered. - -A model declared in a `.razor` file doesn't produce a build error. In a Blazor app, the form silently validates only the top-level properties of the model. For more information, see [Validation when `AddValidation` isn't called](#validation-when-addvalidation-isnt-called). - -For model types defined in a class library or in the `.Client` project of a Blazor Web App, see [Register validation in multi-assembly apps](#register-validation-in-multi-assembly-apps). - :::moniker range=">= aspnetcore-11.0" ## Localize validation messages @@ -330,7 +328,10 @@ builder.Services.AddLocalization(); builder.Services.AddValidation(); ``` -There's no separate package or additional opt-in call. The validation source generator emits the localization lookup into the app's assembly. +There's no separate package or additional opt-in call. + +> [!IMPORTANT] +> `AddLocalization` registers localization services but doesn't select the culture for a request or circuit. Validation resource lookup uses . For request culture providers, including query string, cookie, and `Accept-Language` header selection, see . For configuring culture selection across Blazor render modes, see . ```csharp using System.ComponentModel.DataAnnotations; @@ -389,6 +390,8 @@ builder.Services.AddValidation(); Under the configured `ResourcesPath`, the type's full name minus the project's root namespace is used as a dotted path. For example, in a project whose root namespace is `Contoso`, French messages for `Contoso.Models.Customer` are read from *Resources/Models/Customer.fr.resx* (equivalently *Resources/Models.Customer.fr.resx*). For a full description of the conventions, see . +For per-type lookup, place the resource files in the project that declares the validated type. For example, if a Blazor Web App's form models are declared in its `.Client` project, place their per-type resources in that project. The model assembly's root namespace and the configured `ResourcesPath` determine the resource name. + ### Use a shared resource file To resolve keys from one resource file for every validated type instead of per-type resources, set `ValidationOptions.LocalizerProvider`: @@ -400,17 +403,9 @@ builder.Services.AddValidation(options => }); ``` -The delegate also receives the validated type, so an app can select a different resource file per type: +The delegate also receives the validated type, so an app can select different resources for different model types. -```csharp -builder.Services.AddValidation(options => -{ - options.LocalizerProvider = (type, factory) => - type?.Namespace?.StartsWith("Contoso.Admin") == true - ? factory.Create(typeof(AdminValidationMessages)) - : factory.Create(typeof(ValidationMessages)); -}); -``` +The marker type passed to `factory.Create` identifies both the resource name and the assembly containing the resource. This makes a shared resource in the host project an alternative to placing per-type resources in a referenced model assembly. ### Localize from a source other than resource files @@ -423,7 +418,7 @@ builder.Services.AddValidation(); ### Attributes that localize themselves -Attributes that already perform their own resource lookup bypass this pipeline entirely, because they're localized before validation reports the message. This applies to and to . +Attributes configured with or perform their own resource lookup and aren't processed by the `Microsoft.Extensions.Validation` localizer. ### Format a custom attribute's message @@ -445,25 +440,35 @@ public sealed class DivisibleByAttribute : ValidationAttribute, IValidationMessa ``` > [!NOTE] -> Localization requires . A Blazor form whose model isn't discovered by the validation source generator falls back to , which reports the attribute's raw `ErrorMessage` without localizing it. For more information, see [Validation when `AddValidation` isn't called](#validation-when-addvalidation-isnt-called). +> This localization pipeline requires . A Blazor form whose model isn't discovered by the validation source generator falls back to . Attributes configured to localize themselves with `ErrorMessageResourceType` continue to do so, but the generated lookup conventions and `ValidationOptions.LocalizerProvider` aren't available. For more information, see [Behavior without generated validation metadata](#behavior-without-generated-validation-metadata). :::moniker-end -## Explicit validation skipping +## Configure generated validation metadata -When needed, you can skip validation for a specific parameter, type, or property by applying the . + uses a Roslyn source generator to create validation metadata at build time. Minimal API parameter types are discovered from endpoint handler signatures. Blazor form model types are included by applying . -## Force-generate validatable type information + - works via a Roslyn source generator that detects the object graph and types for Minimal API endpoint parameters. +### Include root model types -In some cases, not all of the types that are part of the object graph can be determined at compile time. In these cases, you can force the source generator to consider a type for validation by applying to the type. +Apply `[ValidatableType]` to a Blazor form's root model type and to any other root type that the source generator can't discover from a Minimal API endpoint signature. Types reachable from the root are included automatically. -## Register validation in multi-assembly apps +### Model types can't be declared in Razor component files -The validation source generator only discovers validatable types in the assembly where is called. Types declared in a referenced assembly, such as a class library or the `.Client` project of a Blazor Web App, aren't validated when `AddValidation` is only called from the host app. +The Razor compiler and the validation feature both use source generators. A source generator can't inspect another generator's output, so the validation generator can't include model types declared in Razor component files (`.razor`). Declare model types in regular C# files (`.cs`) instead. -There's no error or log entry when this happens. In a Minimal API, invalid requests return a `200 - OK` response instead of `400 - Bad Request`. In Blazor, the form doesn't honor the validation attributes of the models. +:::moniker range=">= aspnetcore-11.0" + +Applying `[ValidatableType]` to a type in generated code produces warning ASP0037. + +:::moniker-end + + + +### Register validation across assemblies + +The source generator creates metadata only for the assembly where is called. Calling `AddValidation` only from the host app doesn't generate metadata for types declared in a referenced assembly, such as a class library or the `.Client` project of a Blazor Web App. To validate types from separate assemblies: @@ -496,21 +501,33 @@ To validate types from separate assemblies: The preceding approach validates the types from both assemblies. -Two framework-specific notes: +For a Blazor Web App whose form models are declared in the `.Client` project, create the validation-registration extension method in that project and call it from the server project's `Program` file. -* **Minimal APIs:** when endpoints are mapped from the referenced assembly, define the endpoint-mapping extension method (`MapApi` in the following example) alongside the validation extension method so both are registered from the same assembly: +For Minimal API endpoints and models defined in referenced assemblies, see . - ```csharp - builder.Services.AddApiValidation(); +### Troubleshoot generated validation metadata - ... +Missing generated validation metadata doesn't produce a runtime exception or log entry. If expected validation behavior is missing, confirm all of the following: - var app = builder.Build(); +* `AddValidation` is called from the assembly that declares the validatable types. +* Blazor root form models and other roots not discovered from Minimal API signatures have `[ValidatableType]`. +* Model types are declared in `.cs` files. +* Validated types and properties are accessible to generated code. - app.MapApi(); - ``` +:::moniker range=">= aspnetcore-11.0" + +Build analyzers report common unsupported configurations: + +* ASP0033 and ASP0034 report inaccessible validatable types and endpoint parameter types. +* ASP0035 and ASP0036 report inaccessible validated properties or property types. +* ASP0037 reports `[ValidatableType]` applied to generated code. +* ASP0038 reports `[ValidatableType]` used without a matching `AddValidation` call. -* **Blazor Web Apps:** form model types are commonly declared in the `.Client` project. Create the extension method there and call it from the server project's `Program` file. +Other missing-metadata cases might not produce a diagnostic. + +:::moniker-end + +For the runtime behavior when metadata isn't available, see [Behavior without generated validation metadata](#behavior-without-generated-validation-metadata). :::moniker range="= aspnetcore-10.0" @@ -589,4 +606,3 @@ Whichever approach is adopted, denote the presence of the workaround for a futur * :::moniker-end - From 79d720ef018137fec3dbb745099a0d765c1c1a90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Rozto=C4=8Dil?= Date: Wed, 16 Sep 2026 20:05:00 +0200 Subject: [PATCH 05/11] Clean up validation docs after rebase Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: b1616697-b97e-43c8-bba6-eadce0a68d2c --- aspnetcore/fundamentals/validation.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/aspnetcore/fundamentals/validation.md b/aspnetcore/fundamentals/validation.md index bc7fbf0fcfcc..87892a72aa40 100644 --- a/aspnetcore/fundamentals/validation.md +++ b/aspnetcore/fundamentals/validation.md @@ -176,7 +176,7 @@ using System.ComponentModel.DataAnnotations; public class EvenNumberAttribute : ValidationAttribute { - protected override ValidationResult? IsValid(object? value, + protected override ValidationResult? IsValid(object? value, ValidationContext validationContext) { if (value is int number && number % 2 != 0) @@ -206,7 +206,7 @@ public class Order A validation attribute obtains services from dependency injection (DI) through the validation context, which makes rules that require a database lookup or a configured option possible: ```csharp -protected override ValidationResult? IsValid(object? value, +protected override ValidationResult? IsValid(object? value, ValidationContext validationContext) { var catalog = validationContext.GetService(); @@ -234,7 +234,7 @@ public class DateRange : IValidatableObject if (End < Start) { yield return new ValidationResult( - "End date must fall on or after the start date.", + "End date must fall on or after the start date.", [ nameof(End) ]); } } From 8a59e0e5687cc531fcc4b355fc3bba70a074dfef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Rozto=C4=8Dil?= Date: Thu, 17 Sep 2026 16:46:06 +0200 Subject: [PATCH 06/11] Reference validation samples from docs Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: b1616697-b97e-43c8-bba6-eadce0a68d2c --- .../blazor/forms/validation-advanced.md | 34 +++++++++++++++++++ .../blazor/forms/validation-client-side.md | 14 ++++---- 2 files changed, 41 insertions(+), 7 deletions(-) diff --git a/aspnetcore/blazor/forms/validation-advanced.md b/aspnetcore/blazor/forms/validation-advanced.md index b0041d573242..82c99190ecbc 100644 --- a/aspnetcore/blazor/forms/validation-advanced.md +++ b/aspnetcore/blazor/forms/validation-advanced.md @@ -249,6 +249,16 @@ If the model is declared in the `.Client` project, register its generated valida 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) => { @@ -270,6 +280,10 @@ app.MapPost("/api/starships/validate", (StarshipModel model) => }); ``` +:::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 @@ -314,13 +328,31 @@ Register and map controllers in the server project. The controller returns `400 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 @@ -373,6 +405,8 @@ Place the `CustomValidation` component from [Build a validator component](#build } ``` +:::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" diff --git a/aspnetcore/blazor/forms/validation-client-side.md b/aspnetcore/blazor/forms/validation-client-side.md index 8d43f5b9697c..d2bd1fbce85a 100644 --- a/aspnetcore/blazor/forms/validation-client-side.md +++ b/aspnetcore/blazor/forms/validation-client-side.md @@ -155,15 +155,15 @@ With automatic startup, an app-specific validator script can instead be loaded i ``` -The second script can call `Blazor.formValidation.addValidator` directly. With manual startup, place the same registration calls in the promise continuation: +The second script can call `Blazor.formValidation.addValidator` directly. -```javascript -Blazor.start().then(() => { - registerCustomValidators(Blazor); -}); -``` +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: -In the preceding example, `registerCustomValidators` contains the app's `addValidator` calls. +:::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. From 1baf2bc6ad06738b5c751faacf239829bd03c859 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Rozto=C4=8Dil?= Date: Thu, 17 Sep 2026 17:40:11 +0200 Subject: [PATCH 07/11] Move Advanced article before Client-validation in TOC --- aspnetcore/toc.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/aspnetcore/toc.yml b/aspnetcore/toc.yml index 0759e75c062a..a3c0ba64d5a7 100644 --- a/aspnetcore/toc.yml +++ b/aspnetcore/toc.yml @@ -862,10 +862,10 @@ items: uid: blazor/forms/binding - name: Validation uid: blazor/forms/validation - - name: Client-side validation (static SSR) - uid: blazor/forms/validation-client-side - name: Advanced validation uid: blazor/forms/validation-advanced + - name: Client-side validation (static SSR) + uid: blazor/forms/validation-client-side - name: Troubleshoot uid: blazor/forms/troubleshoot - name: File uploads From 2e31922115d2206dcdfce17ea8857c5e34f4f7fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Rozto=C4=8Dil?= Date: Fri, 18 Sep 2026 10:50:14 +0200 Subject: [PATCH 08/11] Use prerelease notice for .NET 11 validation article Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: b1616697-b97e-43c8-bba6-eadce0a68d2c --- aspnetcore/blazor/forms/validation-client-side.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aspnetcore/blazor/forms/validation-client-side.md b/aspnetcore/blazor/forms/validation-client-side.md index d2bd1fbce85a..a5c9f1814cf4 100644 --- a/aspnetcore/blazor/forms/validation-client-side.md +++ b/aspnetcore/blazor/forms/validation-client-side.md @@ -10,7 +10,7 @@ uid: blazor/forms/validation-client-side --- # ASP.NET Core Blazor client-side form validation in static SSR -[!INCLUDE[](~/includes/not-latest-version.md)] +[!INCLUDE[](~/includes/not-ga-yet.md)] 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. From 9e2366782d1180a2a68123058c86fb75195b77f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Rozto=C4=8Dil?= Date: Fri, 18 Sep 2026 16:53:55 +0200 Subject: [PATCH 09/11] Remove closed PR warning from validation release note Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: b1616697-b97e-43c8-bba6-eadce0a68d2c --- .../aspnetcore-11/includes/validation-localization.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aspnetcore/release-notes/aspnetcore-11/includes/validation-localization.md b/aspnetcore/release-notes/aspnetcore-11/includes/validation-localization.md index 6ad1d2aaf527..d9e0ddb7d9bd 100644 --- a/aspnetcore/release-notes/aspnetcore-11/includes/validation-localization.md +++ b/aspnetcore/release-notes/aspnetcore-11/includes/validation-localization.md @@ -60,4 +60,4 @@ Complete feature coverage is available in the following articles: * * -For more information, see [Add localization support to Microsoft.Extensions.Validation (`dotnet/aspnetcore` #66646)](https://github.com/dotnet/aspnetcore/pull/66646). (Please don't comment on closed issues and PRs.) +For more information, see [Add localization support to Microsoft.Extensions.Validation (`dotnet/aspnetcore` #66646)](https://github.com/dotnet/aspnetcore/pull/66646). From 48eed108726a34c5931da9a9e5449f60d26ae931 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Rozto=C4=8Dil?= Date: Fri, 18 Sep 2026 17:18:30 +0200 Subject: [PATCH 10/11] Clarify async DataAnnotations validation state Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: b1616697-b97e-43c8-bba6-eadce0a68d2c --- aspnetcore/blazor/forms/validation.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/aspnetcore/blazor/forms/validation.md b/aspnetcore/blazor/forms/validation.md index 4154143d0efa..ebc28e122f5e 100644 --- a/aspnetcore/blazor/forms/validation.md +++ b/aspnetcore/blazor/forms/validation.md @@ -540,6 +540,8 @@ Unsubscribe from `OnValidationStateChanged` when the component is disposed. 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. +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. :::moniker-end From 131a0e2cccb240a77b0153be02dfa8aafb846d9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Rozto=C4=8Dil?= Date: Tue, 22 Sep 2026 17:54:57 +0200 Subject: [PATCH 11/11] Apply review feedback, expand SkipValidation --- aspnetcore/blazor/forms/binding.md | 6 +- aspnetcore/blazor/forms/index.md | 2 +- .../blazor/forms/validation-advanced.md | 62 ++-- .../blazor/forms/validation-client-side.md | 64 ++-- aspnetcore/blazor/forms/validation.md | 64 ++-- .../localization/make-content-localizable.md | 344 +++++++++--------- aspnetcore/fundamentals/minimal-apis.md | 2 +- aspnetcore/fundamentals/validation.md | 48 +-- aspnetcore/release-notes/aspnetcore-11.md | 2 +- aspnetcore/release-notes/aspnetcore-5.0.md | 2 +- 10 files changed, 298 insertions(+), 298 deletions(-) diff --git a/aspnetcore/blazor/forms/binding.md b/aspnetcore/blazor/forms/binding.md index 950c160b67b6..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 @@ -466,10 +466,6 @@ The `CustomInputText` component can be used anywhere - :::moniker-end ## Custom input components diff --git a/aspnetcore/blazor/forms/index.md b/aspnetcore/blazor/forms/index.md index b466854625d7..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 diff --git a/aspnetcore/blazor/forms/validation-advanced.md b/aspnetcore/blazor/forms/validation-advanced.md index 82c99190ecbc..049a667e650b 100644 --- a/aspnetcore/blazor/forms/validation-advanced.md +++ b/aspnetcore/blazor/forms/validation-advanced.md @@ -5,7 +5,7 @@ author: guardrex description: Learn how to implement validator components and remote validation for Blazor forms. monikerRange: '>= aspnetcore-3.1' ms.author: wpickett -ms.date: 08/17/2026 +ms.date: 09/22/2026 uid: blazor/forms/validation-advanced --- # ASP.NET Core Blazor advanced form validation @@ -26,10 +26,10 @@ For browser validation in static server-side rendering (static SSR), see - - - + + + + ## Build a validator component @@ -53,7 +53,7 @@ The component: [CascadingParameter] private EditContext? CurrentEditContext { get; set; } - private ValidationMessageStore _messages = default!; + private ValidationMessageStore messages = default!; protected override void OnInitialized() { @@ -63,7 +63,7 @@ The component: "CustomValidation requires a cascading EditContext."); } - _messages = new ValidationMessageStore(CurrentEditContext); + messages = new ValidationMessageStore(CurrentEditContext); CurrentEditContext.OnValidationRequested += HandleValidationRequested; CurrentEditContext.OnFieldChanged += HandleFieldChanged; @@ -73,7 +73,7 @@ The component: { foreach (var error in errors) { - _messages.Add( + messages.Add( CurrentEditContext!.Field(error.Key), error.Value); } @@ -83,7 +83,7 @@ The component: public void ClearErrors() { - _messages.Clear(); + messages.Clear(); CurrentEditContext!.NotifyValidationStateChanged(); } @@ -94,7 +94,7 @@ The component: private void HandleFieldChanged( object? sender, FieldChangedEventArgs e) { - _messages.Clear(e.FieldIdentifier); + messages.Clear(e.FieldIdentifier); CurrentEditContext!.NotifyValidationStateChanged(); } @@ -110,25 +110,25 @@ The component: } ``` -Place the component inside an `EditForm` and capture a component reference when the form or a service needs to display errors: +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; + 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 . +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" @@ -151,7 +151,7 @@ private void HandleValidationRequested( private async Task ValidateAsync(CancellationToken cancellationToken) { var field = CurrentEditContext!.Field(nameof(Model.Username)); - _messages.Clear(field); + messages.Clear(field); var available = await Http.GetFromJsonAsync( $"api/usernames/available?value={Uri.EscapeDataString(Model.Username)}", @@ -159,7 +159,7 @@ private async Task ValidateAsync(CancellationToken cancellationToken) if (!available) { - _messages.Add(field, "The username is already taken."); + messages.Add(field, "The username is already taken."); } CurrentEditContext.NotifyValidationStateChanged(); @@ -190,7 +190,7 @@ For asynchronous validation attributes on the model, see . +The APIs used for generated validation metadata are experimental in .NET 10. For details, see . :::moniker-end @@ -360,7 +360,7 @@ Place the `CustomValidation` component from [Build a validator component](#build - + ... @@ -368,7 +368,7 @@ Place the `CustomValidation` component from [Build a validator component](#build @code { private StarshipModel Model { get; } = new StarshipModel(); - private CustomValidation? _remoteErrors; + private CustomValidation? remoteErrors; private async Task Submit() { @@ -388,7 +388,7 @@ Place the `CustomValidation` component from [Build a validator component](#build if (problem is not null) { - _remoteErrors!.DisplayErrors(problem.Errors); + remoteErrors!.DisplayErrors(problem.Errors); } return; @@ -421,23 +421,27 @@ The [.NET 10 remote-validation sample](https://github.com/dotnet/blazor-samples/ :::moniker-end - - -Validation CSS class customization is covered in . - ## Additional resources +:::moniker range=">= aspnetcore-11.0" + * +* +* +* -:::moniker range=">= aspnetcore-10.0" +:::moniker-end +:::moniker range=">= aspnetcore-10.0 < aspnetcore-11.0" + +* * * :::moniker-end -:::moniker range=">= aspnetcore-11.0" +:::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 index a5c9f1814cf4..3ae51b5726cc 100644 --- a/aspnetcore/blazor/forms/validation-client-side.md +++ b/aspnetcore/blazor/forms/validation-client-side.md @@ -5,12 +5,12 @@ 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: 08/17/2026 +ms.date: 09/22/2026 uid: blazor/forms/validation-client-side --- # ASP.NET Core Blazor client-side form validation in static SSR -[!INCLUDE[](~/includes/not-ga-yet.md)] + 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. @@ -52,15 +52,15 @@ Validation attributes that don't appear in this list, including custom ` 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. +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, as interactive validation does. +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 . @@ -68,13 +68,13 @@ ARIA attributes on input elements and validation message containers are managed ## Validation state CSS classes -The client-side validation engine applies the same CSS classes as Blazor's interactive validation: +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` | +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. @@ -101,17 +101,17 @@ builder.Services.AddRazorComponents(options => }); ``` -The global option takes precedence. When it's set to `true`, no form emits client-side validation rules. +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 on the button. The form is posted without a client-side check, and server-side validation still runs after the post: +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 do not require a completely valid form for the submit to succeed. +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 @@ -136,13 +136,13 @@ In JavaScript, call `Blazor.formValidation.addValidator(name, validator)` to ass > [!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: +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 | +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: @@ -170,15 +170,15 @@ Load the scripts with automatic startup disabled, and register the validators af ### Write JavaScript validator functions -The validator receives a context object with the following members: +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 `input`, `select`, or `textarea` element being validated. | -| `params` | The rule's `Parameters` as a string dictionary. | +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 `{ success: false, message: '...' }` to override the message for that call. +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. @@ -186,12 +186,12 @@ Implement the same rule semantics in .NET and JavaScript, including case sensiti ## Validate form on demand -The `Blazor.formValidation` API also exposes JavaScript methods for validating 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. | +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 diff --git a/aspnetcore/blazor/forms/validation.md b/aspnetcore/blazor/forms/validation.md index ebc28e122f5e..9fcb3b289c72 100644 --- a/aspnetcore/blazor/forms/validation.md +++ b/aspnetcore/blazor/forms/validation.md @@ -5,7 +5,7 @@ author: guardrex description: Learn how to use validation in Blazor forms. monikerRange: '>= aspnetcore-3.1' ms.author: wpickett -ms.date: 08/17/2026 +ms.date: 09/22/2026 uid: blazor/forms/validation --- # ASP.NET Core Blazor forms validation @@ -26,7 +26,7 @@ Related articles provide more detail: :::moniker-end -:::moniker range="= aspnetcore-10.0" +:::moniker range=">= aspnetcore-10.0 < aspnetcore-11.0" * For writing and configuring model-based validation rules and nested object validation, see . * For complete validator-component and remote-validation implementations, see . @@ -163,7 +163,7 @@ The `AddValidation` call registers the package's validation services and activat :::moniker-end -:::moniker range="= aspnetcore-10.0" +:::moniker range=">= aspnetcore-10.0 < aspnetcore-11.0" | Configuration | Behavior | |---|---| @@ -260,7 +260,7 @@ The following interactive-form pattern adds a form-level business rule alongside ```razor @implements IDisposable - + @@ -269,23 +269,23 @@ The following interactive-form pattern adds a form-level business rule alongside @code { private Starship Model { get; } = new Starship(); - private EditContext _editContext = default!; - private ValidationMessageStore _messages = default!; + private EditContext editContext = default!; + private ValidationMessageStore messages = default!; protected override void OnInitialized() { - _editContext = new EditContext(Model); - _messages = new ValidationMessageStore(_editContext); - _editContext.OnValidationRequested += ValidateBusinessRules; - _editContext.OnFieldChanged += ValidateChangedField; + editContext = new EditContext(Model); + messages = new ValidationMessageStore(editContext); + editContext.OnValidationRequested += ValidateBusinessRules; + editContext.OnFieldChanged += ValidateChangedField; } private void ValidateBusinessRules( object? sender, ValidationRequestedEventArgs e) { - _messages.Clear(); + messages.Clear(); ValidateIdentifier(); - _editContext.NotifyValidationStateChanged(); + editContext.NotifyValidationStateChanged(); } private void ValidateChangedField( @@ -297,10 +297,10 @@ The following interactive-form pattern adds a form-level business rule alongside return; } - _messages.Clear( - _editContext.Field(nameof(Starship.Identifier))); + messages.Clear( + editContext.Field(nameof(Starship.Identifier))); ValidateIdentifier(); - _editContext.NotifyValidationStateChanged(); + editContext.NotifyValidationStateChanged(); } private void ValidateIdentifier() @@ -308,8 +308,8 @@ The following interactive-form pattern adds a form-level business rule alongside if (Model.MaximumAccommodation == 1 && string.IsNullOrWhiteSpace(Model.Identifier)) { - _messages.Add( - _editContext.Field(nameof(Starship.Identifier)), + messages.Add( + editContext.Field(nameof(Starship.Identifier)), "An identifier is required for a single-occupant ship."); } } @@ -321,8 +321,8 @@ The following interactive-form pattern adds a form-level business rule alongside public void Dispose() { - _editContext.OnValidationRequested -= ValidateBusinessRules; - _editContext.OnFieldChanged -= ValidateChangedField; + editContext.OnValidationRequested -= ValidateBusinessRules; + editContext.OnFieldChanged -= ValidateChangedField; } } ``` @@ -370,9 +370,9 @@ Assign the summary's `Model` parameter to restrict it to messages associated wit To inspect current messages in code, call : ```csharp -var allMessages = _editContext.GetValidationMessages(); -var fieldMessages = _editContext.GetValidationMessages( - _editContext.Field(nameof(Starship.Identifier))); +var allMessages = editContext.GetValidationMessages(); +var fieldMessages = editContext.GetValidationMessages( + editContext.Field(nameof(Starship.Identifier))); ``` These methods read the current validation state. They don't initiate validation. @@ -463,7 +463,7 @@ A custom `FieldCssClassProvider` determines the complete class value for each fi Assign the provider to the form's `EditContext`: ```csharp -_editContext.SetFieldCssClassProvider( +editContext.SetFieldCssClassProvider( new BootstrapFieldCssClassProvider()); ``` @@ -498,11 +498,11 @@ The following example displays custom UI only after a field is modified and inva ```razor @{ - var identifier = _editContext.Field(nameof(Starship.Identifier)); + var identifier = editContext.Field(nameof(Starship.Identifier)); } -@if (_editContext.IsModified(identifier) && - !_editContext.IsValid(identifier)) +@if (editContext.IsModified(identifier) && + !editContext.IsValid(identifier)) {

        Correct the identifier before continuing.

        } @@ -514,11 +514,11 @@ The following example displays custom UI only after a field is modified and inva ```razor @{ - var identifier = _editContext.Field(nameof(Starship.Identifier)); + var identifier = editContext.Field(nameof(Starship.Identifier)); } -@if (_editContext.IsModified(identifier) && - _editContext.GetValidationMessages(identifier).Any()) +@if (editContext.IsModified(identifier) && + editContext.GetValidationMessages(identifier).Any()) {

        Correct the identifier before continuing.

        } @@ -563,7 +563,7 @@ Live pending indicators require an interactive render mode. During a static SSR `EditForm` uses before invoking `OnValidSubmit` or `OnInvalidSubmit`, so it awaits synchronous and asynchronous validators. When handling `OnSubmit`, call `ValidateAsync` before processing the form: ```razor - + ... @@ -583,7 +583,7 @@ The synchronous + ``` @@ -595,7 +595,7 @@ For interactive forms, the form-level pending state can be used to disable submi When handling `OnSubmit`, call before processing the form: ```razor - + ... diff --git a/aspnetcore/fundamentals/localization/make-content-localizable.md b/aspnetcore/fundamentals/localization/make-content-localizable.md index 8d7ff67242b2..bab10667bf7c 100644 --- a/aspnetcore/fundamentals/localization/make-content-localizable.md +++ b/aspnetcore/fundamentals/localization/make-content-localizable.md @@ -1,172 +1,172 @@ ---- -title: Make an ASP.NET Core app's content localizable -author: wadepickett -description: Learn how to make an ASP.NET Core app's content localizable to prepare the app for localizing content into different languages and cultures. -ms.author: wpickett -monikerRange: '>= aspnetcore-5.0' -ms.date: 06/20/2025 -uid: fundamentals/localization/make-content-localizable ---- -# Make an ASP.NET Core app's content localizable - -[!INCLUDE[](~/includes/not-latest-version.md)] - -:::moniker range="> aspnetcore-5.0" - -By [Hisham Bin Ateya](https://twitter.com/hishambinateya), [Damien Bowden](https://github.com/damienbod), [Bart Calixto](https://twitter.com/bartmax) and [Nadeem Afana](https://afana.me/) - -One task for localizing an app is to wrap localizable content with code that facilitates replacing that content for different cultures. - -## `IStringLocalizer` - - and were architected to improve productivity when developing localized apps. `IStringLocalizer` uses the and to provide culture-specific resources at run time. The interface has an indexer and an `IEnumerable` for returning localized strings. `IStringLocalizer` doesn't require storing the default language strings in a resource file. You can develop an app targeted for localization and not need to create resource files early in development. - -The following code example shows how to wrap the string "About Title" for localization. - -[!code-csharp[](~/fundamentals/localization/sample/8.x/Localization/Controllers/AboutController.cs)] - -In the preceding code, the `IStringLocalizer` implementation comes from [Dependency Injection](~/fundamentals/dependency-injection.md). If the localized value of "About Title" isn't found, then the indexer key is returned, that is, the string "About Title". - -You can leave the default language literal strings in the app and wrap them in the localizer, so that you can focus on developing the app. You develop an app with your default language and prepare it for the localization step without first creating a default resource file. - -Alternatively, you can use the traditional approach and provide a key to retrieve the default language string. For many developers, the new workflow of not having a default language *.resx* file and simply wrapping the string literals can reduce the overhead of localizing an app. Other developers prefer the traditional work flow as it can be easier to work with long string literals and easier to update localized strings. - -## `IHtmlLocalizer` - -Use the implementation for resources that contain HTML. HTML-encodes arguments that are formatted in the resource string, but doesn't HTML-encode the resource string itself. In the following highlighted code, only the value of the `name` parameter is HTML-encoded. - -[!code-csharp[](~/fundamentals/localization/sample/8.x/Localization/Controllers/BookController.cs?highlight=3,5,20&start=1&end=24)] - -***NOTE:*** Generally, only localize text, not HTML. - -## `IStringLocalizerFactory` - -At the lowest level, can be retrieved from of [Dependency Injection](~/fundamentals/dependency-injection.md): - -[!code-csharp[](~/fundamentals/localization/sample/8.x/Localization/Controllers/TestController.cs?highlight=6-12&name=snippet_1)] - -The preceding code demonstrates each of the two factory create methods. - -## Shared resources - -You can partition your localized strings by controller or area, or have just one container. In the sample app, a marker class named `SharedResource` is used for shared resources. The marker class is never called: - -[!code-csharp[](~/fundamentals/localization/sample/8.x/Localization/SharedResource.cs)] - -In the following sample, the `InfoController` and the `SharedResource` localizers are used: - -[!code-csharp[](~/fundamentals/localization/sample/8.x/Localization/Controllers/InfoController.cs?name=snippet_1)] - -## View localization - -The service provides localized strings for a [view](xref:mvc/views/overview). The `ViewLocalizer` class implements this interface and finds the resource location from the view file path. The following code shows how to use the default implementation of `IViewLocalizer`: - -[!code-cshtml[](~/fundamentals/localization/sample/8.x/Localization/Views/Home/About.cshtml)] - -The default implementation of `IViewLocalizer` finds the resource file based on the view's file name. There's no option to use a global shared resource file. `ViewLocalizer` implements the localizer using `IHtmlLocalizer`, so Razor doesn't HTML-encode the localized string. You can parameterize resource strings, and `IViewLocalizer` HTML-encodes the parameters but not the resource string. Consider the following Razor markup: - -```cshtml -@Localizer["Hello {0}!", UserManager.GetUserName(User)] -``` - -A French resource file could contain the following values: - -| Key | Value | -| -------------------------- | ----------------------------- | -| `Hello {0}!` | `Bonjour {0} !` | - -The rendered view would contain the HTML markup from the resource file. - -Generally, ***only localize text***, not HTML. - -To use a shared resource file in a view, inject `IHtmlLocalizer`: - -[!code-cshtml[](~/fundamentals/localization/sample/8.x/Localization/Views/Test/About.cshtml?highlight=5,12)] - -## DataAnnotations localization - -DataAnnotations error messages are localized with `IStringLocalizer`. Using the option `ResourcesPath = "Resources"`, the error messages in `RegisterViewModel` can be stored in either of the following paths: - -* *Resources/ViewModels.Account.RegisterViewModel.fr.resx* -* *Resources/ViewModels/Account/RegisterViewModel.fr.resx* - -[!code-csharp[](~/fundamentals/localization/sample/8.x/Localization/ViewModels/Account/RegisterViewModel.cs)] - -Non-validation attributes are localized. - - - -### How to use one resource string for multiple classes - -The following code shows how to use one resource string for validation attributes with multiple classes: - -```csharp - services.AddMvc() - .AddDataAnnotationsLocalization(options => { - options.DataAnnotationLocalizerProvider = (type, factory) => - factory.Create(typeof(SharedResource)); - }); -``` - -In the preceding code, `SharedResource` is the class corresponding to the *.resx* file where the validation messages are stored. With this approach, DataAnnotations only uses `SharedResource`, rather than the resource for each class. - -:::moniker-end - -:::moniker range=">= aspnetcore-11.0" - -## DataAnnotations localization in Minimal APIs and Blazor - -Validation error messages and the display names of validated members are localized by , which is the validation pipeline used by Minimal APIs and Blazor forms. - -Localization activates automatically when an is registered. Call together with : - -```csharp -builder.Services.AddLocalization(); -builder.Services.AddValidation(); -``` - -For the message lookup key conventions, shared resource files, custom message formatting, and the full set of options, see . - -The integration doesn't apply to MVC and Razor Pages apps. For those frameworks, see . - -:::moniker-end - -:::moniker range="> aspnetcore-5.0" - -## Configure localization services - -Localization services are configured in `Program.cs`: - -[!code-csharp[](~/fundamentals/localization/sample/6.x/Localization/program.cs?name=snippet_LocalizationConfigurationServices)] - -* adds the localization services to the services container, including implementations for `IStringLocalizer` and `IStringLocalizerFactory`. The preceding code also sets the resources path to "Resources". - -* adds support for localized view files. In this sample, view localization is based on the view file suffix. For example "fr" in the `Index.fr.cshtml` file. - -* adds support for localized `DataAnnotations` validation messages through `IStringLocalizer` abstractions. - -[!INCLUDE[](~/includes/localization/currency.md)] - -## Next steps - -Localizing an app also involves the following tasks: - -* [Provide localized resources for the languages and cultures the app supports](xref:fundamentals/localization/provide-resources) -* [Implement a strategy to select the language/culture for each request](xref:fundamentals/localization/select-language-culture) - -## Additional resources - -* [Url culture provider using middleware as filters in ASP.NET Core](https://andrewlock.net/url-culture-provider-using-middleware-as-mvc-filter-in-asp-net-core-1-1-0/) -* [Applying the RouteDataRequest CultureProvider globally with middleware as filters](https://andrewlock.net/applying-the-routedatarequest-cultureprovider-globally-with-middleware-as-filters/) -* -* -* -* -* [Globalizing and localizing .NET applications](/dotnet/standard/globalization-localization/index) -* [Localization.StarterWeb project](https://github.com/aspnet/Entropy/tree/master/samples/Localization.StarterWeb) used in the article. -* [Resources in .resx Files](/dotnet/framework/resources/working-with-resx-files-programmatically) -* [Localization & Generics](http://hishambinateya.com/localization-and-generics) - -:::moniker-end - -[!INCLUDE [make-content-localizable5](~/fundamentals/localization/includes/make-content-localizable5.md)] +--- +title: Make an ASP.NET Core app's content localizable +author: wadepickett +description: Learn how to make an ASP.NET Core app's content localizable to prepare the app for localizing content into different languages and cultures. +ms.author: wpickett +monikerRange: '>= aspnetcore-5.0' +ms.date: 09/22/2026 +uid: fundamentals/localization/make-content-localizable +--- +# Make an ASP.NET Core app's content localizable + +[!INCLUDE[](~/includes/not-latest-version.md)] + +:::moniker range="> aspnetcore-5.0" + +By [Hisham Bin Ateya](https://twitter.com/hishambinateya), [Damien Bowden](https://github.com/damienbod), [Bart Calixto](https://twitter.com/bartmax) and [Nadeem Afana](https://afana.me/) + +One task for localizing an app is to wrap localizable content with code that facilitates replacing that content for different cultures. + +## `IStringLocalizer` + + and were architected to improve productivity when developing localized apps. `IStringLocalizer` uses the and to provide culture-specific resources at run time. The interface has an indexer and an `IEnumerable` for returning localized strings. `IStringLocalizer` doesn't require storing the default language strings in a resource file. You can develop an app targeted for localization and not need to create resource files early in development. + +The following code example shows how to wrap the string "About Title" for localization. + +[!code-csharp[](~/fundamentals/localization/sample/8.x/Localization/Controllers/AboutController.cs)] + +In the preceding code, the `IStringLocalizer` implementation comes from [Dependency Injection](~/fundamentals/dependency-injection.md). If the localized value of "About Title" isn't found, then the indexer key is returned, that is, the string "About Title". + +You can leave the default language literal strings in the app and wrap them in the localizer, so that you can focus on developing the app. You develop an app with your default language and prepare it for the localization step without first creating a default resource file. + +Alternatively, you can use the traditional approach and provide a key to retrieve the default language string. For many developers, the new workflow of not having a default language *.resx* file and simply wrapping the string literals can reduce the overhead of localizing an app. Other developers prefer the traditional work flow as it can be easier to work with long string literals and easier to update localized strings. + +## `IHtmlLocalizer` + +Use the implementation for resources that contain HTML. HTML-encodes arguments that are formatted in the resource string, but doesn't HTML-encode the resource string itself. In the following highlighted code, only the value of the `name` parameter is HTML-encoded. + +[!code-csharp[](~/fundamentals/localization/sample/8.x/Localization/Controllers/BookController.cs?highlight=3,5,20&start=1&end=24)] + +***NOTE:*** Generally, only localize text, not HTML. + +## `IStringLocalizerFactory` + +At the lowest level, can be retrieved from of [Dependency Injection](~/fundamentals/dependency-injection.md): + +[!code-csharp[](~/fundamentals/localization/sample/8.x/Localization/Controllers/TestController.cs?highlight=6-12&name=snippet_1)] + +The preceding code demonstrates each of the two factory create methods. + +## Shared resources + +You can partition your localized strings by controller or area, or have just one container. In the sample app, a marker class named `SharedResource` is used for shared resources. The marker class is never called: + +[!code-csharp[](~/fundamentals/localization/sample/8.x/Localization/SharedResource.cs)] + +In the following sample, the `InfoController` and the `SharedResource` localizers are used: + +[!code-csharp[](~/fundamentals/localization/sample/8.x/Localization/Controllers/InfoController.cs?name=snippet_1)] + +## View localization + +The service provides localized strings for a [view](xref:mvc/views/overview). The `ViewLocalizer` class implements this interface and finds the resource location from the view file path. The following code shows how to use the default implementation of `IViewLocalizer`: + +[!code-cshtml[](~/fundamentals/localization/sample/8.x/Localization/Views/Home/About.cshtml)] + +The default implementation of `IViewLocalizer` finds the resource file based on the view's file name. There's no option to use a global shared resource file. `ViewLocalizer` implements the localizer using `IHtmlLocalizer`, so Razor doesn't HTML-encode the localized string. You can parameterize resource strings, and `IViewLocalizer` HTML-encodes the parameters but not the resource string. Consider the following Razor markup: + +```cshtml +@Localizer["Hello {0}!", UserManager.GetUserName(User)] +``` + +A French resource file could contain the following values: + +| Key | Value | +| -------------------------- | ----------------------------- | +| `Hello {0}!` | `Bonjour {0} !` | + +The rendered view would contain the HTML markup from the resource file. + +Generally, ***only localize text***, not HTML. + +To use a shared resource file in a view, inject `IHtmlLocalizer`: + +[!code-cshtml[](~/fundamentals/localization/sample/8.x/Localization/Views/Test/About.cshtml?highlight=5,12)] + +## DataAnnotations localization + +DataAnnotations error messages are localized with `IStringLocalizer`. Using the option `ResourcesPath = "Resources"`, the error messages in `RegisterViewModel` can be stored in either of the following paths: + +* *Resources/ViewModels.Account.RegisterViewModel.fr.resx* +* *Resources/ViewModels/Account/RegisterViewModel.fr.resx* + +[!code-csharp[](~/fundamentals/localization/sample/8.x/Localization/ViewModels/Account/RegisterViewModel.cs)] + +Non-validation attributes are localized. + + + +### How to use one resource string for multiple classes + +The following code shows how to use one resource string for validation attributes with multiple classes: + +```csharp + services.AddMvc() + .AddDataAnnotationsLocalization(options => { + options.DataAnnotationLocalizerProvider = (type, factory) => + factory.Create(typeof(SharedResource)); + }); +``` + +In the preceding code, `SharedResource` is the class corresponding to the *.resx* file where the validation messages are stored. With this approach, DataAnnotations only uses `SharedResource`, rather than the resource for each class. + +:::moniker-end + +:::moniker range=">= aspnetcore-11.0" + +## DataAnnotations localization in Minimal APIs and Blazor + +Validation error messages and the display names of validated members are localized by , which is the validation pipeline used by Minimal APIs and Blazor forms. + +Localization activates automatically when an is registered. Call together with : + +```csharp +builder.Services.AddLocalization(); +builder.Services.AddValidation(); +``` + +For the message lookup key conventions, shared resource files, custom message formatting, and the full set of options, see . + +The integration doesn't apply to MVC and Razor Pages apps. For those frameworks, see . + +:::moniker-end + +:::moniker range="> aspnetcore-5.0" + +## Configure localization services + +Localization services are configured in `Program.cs`: + +[!code-csharp[](~/fundamentals/localization/sample/6.x/Localization/program.cs?name=snippet_LocalizationConfigurationServices)] + +* adds the localization services to the services container, including implementations for `IStringLocalizer` and `IStringLocalizerFactory`. The preceding code also sets the resources path to "Resources". + +* adds support for localized view files. In this sample, view localization is based on the view file suffix. For example "fr" in the `Index.fr.cshtml` file. + +* adds support for localized `DataAnnotations` validation messages through `IStringLocalizer` abstractions. + +[!INCLUDE[](~/includes/localization/currency.md)] + +## Next steps + +Localizing an app also involves the following tasks: + +* [Provide localized resources for the languages and cultures the app supports](xref:fundamentals/localization/provide-resources) +* [Implement a strategy to select the language/culture for each request](xref:fundamentals/localization/select-language-culture) + +## Additional resources + +* [Url culture provider using middleware as filters in ASP.NET Core](https://andrewlock.net/url-culture-provider-using-middleware-as-mvc-filter-in-asp-net-core-1-1-0/) +* [Applying the RouteDataRequest CultureProvider globally with middleware as filters](https://andrewlock.net/applying-the-routedatarequest-cultureprovider-globally-with-middleware-as-filters/) +* +* +* +* +* [Globalizing and localizing .NET applications](/dotnet/standard/globalization-localization/index) +* [Localization.StarterWeb project](https://github.com/aspnet/Entropy/tree/master/samples/Localization.StarterWeb) used in the article. +* [Resources in .resx Files](/dotnet/framework/resources/working-with-resx-files-programmatically) +* [Localization & Generics](http://hishambinateya.com/localization-and-generics) + +:::moniker-end + +[!INCLUDE [make-content-localizable5](~/fundamentals/localization/includes/make-content-localizable5.md)] diff --git a/aspnetcore/fundamentals/minimal-apis.md b/aspnetcore/fundamentals/minimal-apis.md index e3417a5dbc5b..89e5b65aac0b 100644 --- a/aspnetcore/fundamentals/minimal-apis.md +++ b/aspnetcore/fundamentals/minimal-apis.md @@ -6,7 +6,7 @@ content_well_notification: AI-contribution description: Provides an overview of Minimal APIs in ASP.NET Core monikerRange: '>= aspnetcore-6.0' ms.author: wpickett -ms.date: 08/14/2026 +ms.date: 09/22/2026 uid: fundamentals/minimal-apis --- diff --git a/aspnetcore/fundamentals/validation.md b/aspnetcore/fundamentals/validation.md index 87892a72aa40..fc6082ed5fcd 100644 --- a/aspnetcore/fundamentals/validation.md +++ b/aspnetcore/fundamentals/validation.md @@ -5,7 +5,7 @@ author: Youssef1313 description: Use Microsoft.Extensions.Validation in ASP.NET Core to validate models. monikerRange: '>= aspnetcore-10.0' ms.author: ygerges -ms.date: 08/17/2026 +ms.date: 09/22/2026 uid: fundamentals/validation --- # Validation in ASP.NET Core @@ -20,8 +20,8 @@ Asynchronous validation attributes and . -* Blazor uses the service through the component. For how validation is surfaced in a form, see . +* Minimal APIs use the service to validate a request before the endpoint handler runs. For guidance on how validation is surfaced in an endpoint, see . +* Blazor uses the service through the component. For guidance on how validation is surfaced in a form, see . While the API in the [`Microsoft.Extensions.Validation` NuGet package](https://www.nuget.org/packages/Microsoft.Extensions.Validation) can be used in scenarios outside ASP.NET Core, this article focuses on ASP.NET Core. The API isn't supported for MVC or Razor Pages. For validation guidance that applies to MVC and Razor Pages, see . @@ -47,23 +47,23 @@ Validation uses a source generator that creates metadata for validatable types i ### Behavior without generated validation metadata -The consequence of omitting , or of calling it without the required type being discovered by the source generator, differs by framework: +The consequence of omitting , or of calling it without the required type being discovered by the source generator, differs by framework, as shown in the following table. :::moniker range=">= aspnetcore-11.0" -| Framework | Behavior without generated validation metadata | -|---|---| -| Minimal APIs | No automatic validation runs. Invalid input reaches the endpoint handler instead of being rejected before the handler executes. | -| Blazor | The component falls back to , which validates top-level properties only. Nested objects, collection items, and the [`Microsoft.Extensions.Validation` message-localization pipeline](#localize-validation-messages) aren't supported on the fallback path. | +Framework | Behavior without generated validation metadata +--- | --- +Minimal APIs | No automatic validation runs. Invalid input reaches the endpoint handler instead of being rejected before the handler executes. +Blazor | The component falls back to , which validates top-level properties only. Nested objects, collection items, and the [`Microsoft.Extensions.Validation` message-localization pipeline](#localize-validation-messages) aren't supported on the fallback path. :::moniker-end :::moniker range="< aspnetcore-11.0" -| Framework | Behavior without generated validation metadata | -|---|---| -| Minimal APIs | No automatic validation runs. Invalid input reaches the endpoint handler instead of being rejected before the handler executes. | -| Blazor | The component falls back to , which validates top-level properties only. Nested objects and collection items aren't validated on the fallback path. | +Framework | Behavior without generated validation metadata +--- | --- +Minimal APIs | No automatic validation runs. Invalid input reaches the endpoint handler instead of being rejected before the handler executes. +Blazor | The component falls back to , which validates top-level properties only. Nested objects and collection items aren't validated on the fallback path. :::moniker-end @@ -159,7 +159,7 @@ For model types defined in another assembly or in a Blazor Web App's `.Client` p ### Skip validation -Apply to a parameter, type, or property that shouldn't be validated. +Apply to a property or parameter to skip validation for that property or parameter. Apply it to a type to skip validation for all properties and parameters of that type, including nested properties when the type is complex. ## Write custom validation rules @@ -211,7 +211,7 @@ protected override ValidationResult? IsValid(object? value, { var catalog = validationContext.GetService(); - ... + // ... } ``` @@ -219,7 +219,7 @@ For a service that must be resolved, use for a rule that spans several properties, because an attribute applied to one property can't reliably observe the others. Class-level validation runs after property validation and only if property validation succeeds: +Implement for a rule that spans several properties because an attribute applied to one property can't reliably observe the others. Class-level validation runs after property validation and only if property validation succeeds: ```csharp using System.ComponentModel.DataAnnotations; @@ -266,7 +266,7 @@ Asynchronous validation operations can run in parallel, and their execution and For Minimal API validation, always calls the asynchronous path and never the synchronous path. -Blazor form validation calls the asynchronous path for per-field validation and when the form is validated with , which is what uses on submit. The synchronous path is only reached through the method, which is obsolete as of .NET 11. Asynchronous rules therefore work in Blazor forms without additional configuration. +Blazor form validation calls the asynchronous path for per-field validation and when the form is validated with , which is what uses on submit. The synchronous path is only reached through the method, which is obsolete in .NET 11 or later. Asynchronous rules therefore work in Blazor forms without additional configuration. If your implementation can't support the synchronous path, throw . @@ -360,7 +360,7 @@ When `ErrorMessage` isn't set, conventional keys are tried in order from most to 1. `{DeclaringType}_{AttributeType}_Error` 1. `{AttributeType}_Error` -For example, a on the `Name` property of `CustomerModel` is looked up as `CustomerModel_Name_RequiredAttribute_Error`, then `CustomerModel_RequiredAttribute_Error`, then `RequiredAttribute_Error`. If none resolve, the attribute's built-in message is used. +For example, a on the `Name` property of `CustomerModel` is looked up in this order: `CustomerModel_Name_RequiredAttribute_Error`, `CustomerModel_RequiredAttribute_Error`, and `RequiredAttribute_Error`. If none resolve, the attribute's built-in message is used. This makes it possible to translate or override the default message of an attribute across an entire app without setting `ErrorMessage` on every attribute instance: @@ -381,14 +381,14 @@ Two details affect key construction: ### Where resource files are located -Keys are resolved from *.resx* files through ASP.NET Core's standard infrastructure, so the usual naming and placement conventions apply. Per-type resolution is the default and needs no configuration beyond the resources path: +Keys are resolved from `.resx` files through ASP.NET Core's standard infrastructure, so the usual naming and placement conventions apply. Per-type resolution is the default and needs no configuration beyond the resources path: ```csharp builder.Services.AddLocalization(options => options.ResourcesPath = "Resources"); builder.Services.AddValidation(); ``` -Under the configured `ResourcesPath`, the type's full name minus the project's root namespace is used as a dotted path. For example, in a project whose root namespace is `Contoso`, French messages for `Contoso.Models.Customer` are read from *Resources/Models/Customer.fr.resx* (equivalently *Resources/Models.Customer.fr.resx*). For a full description of the conventions, see . +Under the configured `ResourcesPath`, the type's full name minus the project's root namespace is used as a dotted path. For a project root namespace of `Contoso`, French messages for `Contoso.Models.Customer` are read from `Resources/Models/Customer.fr.resx` (equivalently `Resources/Models.Customer.fr.resx`). For a full description of the conventions, see . For per-type lookup, place the resource files in the project that declares the validated type. For example, if a Blazor Web App's form models are declared in its `.Client` project, place their per-type resources in that project. The model assembly's root namespace and the configured `ResourcesPath` determine the resource name. @@ -518,10 +518,10 @@ Missing generated validation metadata doesn't produce a runtime exception or log Build analyzers report common unsupported configurations: -* ASP0033 and ASP0034 report inaccessible validatable types and endpoint parameter types. -* ASP0035 and ASP0036 report inaccessible validated properties or property types. -* ASP0037 reports `[ValidatableType]` applied to generated code. -* ASP0038 reports `[ValidatableType]` used without a matching `AddValidation` call. +* and report inaccessible validatable types and endpoint parameter types. +* and report inaccessible validated properties or property types. +* reports `[ValidatableType]` applied to generated code. +* reports `[ValidatableType]` used without a matching `AddValidation` call. Other missing-metadata cases might not produce a diagnostic. @@ -529,7 +529,7 @@ Other missing-metadata cases might not produce a diagnostic. For the runtime behavior when metadata isn't available, see [Behavior without generated validation metadata](#behavior-without-generated-validation-metadata). -:::moniker range="= aspnetcore-10.0" +:::moniker range=">= aspnetcore-10.0 < aspnetcore-11.0" ## Experimental API in apps that target .NET 10 diff --git a/aspnetcore/release-notes/aspnetcore-11.md b/aspnetcore/release-notes/aspnetcore-11.md index cc5cfcefd437..7c52e2e2e504 100644 --- a/aspnetcore/release-notes/aspnetcore-11.md +++ b/aspnetcore/release-notes/aspnetcore-11.md @@ -4,7 +4,7 @@ ai-usage: ai-assisted author: wadepickett description: Learn about the new features in ASP.NET Core in .NET 11. ms.author: wpickett -ms.date: 09/14/2026 +ms.date: 09/22/2026 uid: aspnetcore-11 --- # What's new in ASP.NET Core in .NET 11 diff --git a/aspnetcore/release-notes/aspnetcore-5.0.md b/aspnetcore/release-notes/aspnetcore-5.0.md index 8afa4d8f2659..7cdba6c962bc 100644 --- a/aspnetcore/release-notes/aspnetcore-5.0.md +++ b/aspnetcore/release-notes/aspnetcore-5.0.md @@ -141,7 +141,7 @@ Use the `FocusAsync` convenience method on element references to set the UI focu ### Custom validation CSS class attributes -Custom validation CSS class attributes are useful when integrating with CSS frameworks, such as Bootstrap. For more information, see . +Custom validation CSS class attributes are useful when integrating with CSS frameworks, such as Bootstrap. For more information, see . ### IAsyncDisposable support