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 b3ea042099f3..41934ccc64c4 100644 --- a/aspnetcore/blazor/forms/binding.md +++ b/aspnetcore/blazor/forms/binding.md @@ -398,6 +398,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 8480bf336b2a..9fe68eb1a1c4 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..46d8a040e4ec --- /dev/null +++ b/aspnetcore/blazor/forms/validation-advanced.md @@ -0,0 +1,1596 @@ +--- +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 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 +private void OnFieldChanged(object? sender, FieldChangedEventArgs e) +{ + if (e.FieldIdentifier.FieldName != nameof(RegistrationModel.Username)) + { + return; + } + + CurrentEditContext!.RegisterAsyncFieldValidator( + e.FieldIdentifier, + token => CheckAsync(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. + +For a complete validator 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 . + +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 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. + +> [!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)] + +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. + +`Starship/StarshipModel.cs` (Blazor Web App) or `StarshipModel.cs` (Minimal API project): + +:::code language="csharp" source="~/../blazor-samples/10.0/BlazorWebAppRemoteValidation/BlazorSample.Client/Starship/StarshipModel.cs"::: + +### 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. + +`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`: + +:::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. + +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`: + +:::code language="csharp" source="~/../blazor-samples/10.0/BlazorWebAppRemoteValidation/BlazorSample/Starship/ServerFormValidation.cs"::: + +### Register the server form validator + +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(); +``` + +### 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); +}); +``` + +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: + +```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. + +### 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`: + +:::code language="csharp" source="~/../blazor-samples/10.0/BlazorWebAppRemoteValidation/BlazorSample.Client/CustomValidation.cs"::: + +> [!NOTE] +> This is the same `CustomValidation` component described in the [Validator components](#validator-components) section. + +### 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`. + +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). + +:::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 + +``` + +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-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..fc8b77b0ecef --- /dev/null +++ b/aspnetcore/blazor/forms/validation-client-side.md @@ -0,0 +1,232 @@ +--- +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: + +:::code language="csharp" source="~/../blazor-samples/11.0/BlazorSample_BlazorWebApp/Validation/StartsWithAttribute.cs"::: + +Apply the attribute to the model in the usual way: + +:::code language="csharp" source="~/../blazor-samples/11.0/BlazorSample_BlazorWebApp/Validation/ShipModel.cs"::: + +### Register the 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: + +:::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. + +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..a8799e9ed843 100644 --- a/aspnetcore/blazor/forms/validation.md +++ b/aspnetcore/blazor/forms/validation.md @@ -5,2569 +5,431 @@ 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 -## 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: * [`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: - -* The form's hosting component uses static SSR (no `@rendermode` directive applied to the component). -* The form contains a component. - -### 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 timing - -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. - -### Validation messages and accessibility - -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. - -### Localized validation messages - -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 . - -### CSS framework integration - -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. - -### Enhanced navigation - -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. - -### Opting out - -To keep server-side data annotations validation but disable client-side enforcement for a single form, set the component's `DisableClientValidation` parameter to `true`: - -```razor - -``` - -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: - -```razor - -``` - -:::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 - -``` +Two general approaches are available for validation logic that isn't declared on the model, both described in : -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: +* 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). -```diff -- OnSubmit="Submit" -+ OnValidSubmit="Submit" -``` +## Validation Summary and Validation Message components -In the ``, add a field for the `Id` property: +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 -
- - -
+ ``` -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. - -:::moniker range=">= aspnetcore-11.0" - -## Asynchronous validation - - 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: - - - -* `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. - - awaits any registered async work before invoking . Sync-only forms continue to work without changes. - -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. - -> [!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. - -### Form-level async validation - -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. - -In the following example, a custom validator component checks a username against a remote endpoint when the form is submitted: - +Output validation messages for a specific model with the `Model` parameter: + ```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 async handlers before invoking , the submit handler runs only after the remote check completes successfully: +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 - - - - - - + ``` -### 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. - -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); +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. - if (!available) - { - _messages.Add(field, "The username is already taken."); - } - - CurrentEditContext!.NotifyValidationStateChanged(); -} +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: -public void Dispose() -{ - if (CurrentEditContext is not null) - { - CurrentEditContext.OnValidationRequested -= OnValidationRequested; - CurrentEditContext.OnFieldChanged -= OnFieldChanged; - } +```css +.validation-message { + color: red; } ``` -A canceled task is discarded silently and does not change the field's faulted state. A task that throws an exception other than places the field in the faulted state described in the next section. - -### Pending and faulted state - -While an async task is in flight, the field is *pending*. If an async task throws an exception other than , the field is *faulted*. Each state has both a per-field and a form-level query: - -| State | Per-field | Form-level (any field) | -|----------|----------------------------------------------------|----------------------------------| -| Pending | `EditContext.IsValidationPending(fieldIdentifier)` | `EditContext.IsValidationPending()` | -| Faulted | `EditContext.IsValidationFaulted(fieldIdentifier)` | `EditContext.IsValidationFaulted()` | - -The per-field overloads accept either a or a `() => model.Property` lambda for convenient use in Razor markup: - -```razor - - - -@if (EditContext.IsValidationPending(() => Model.Username)) -{ - Checking… -} -else if (EditContext.IsValidationFaulted(() => Model.Username)) -{ - - Validation could not be completed. - -} -``` +### Validation state CSS classes -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: +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#: -```razor - -``` +| 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` | - 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: +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 -.pending { - background-image: url('spinner.gif'); - background-repeat: no-repeat; - background-position: right center; +.valid.modified:not([type=checkbox]) { + outline: 1px solid #26b050; } +``` -.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`: - -:::code language="razor" source="~/../blazor-samples/8.0/BlazorSample_BlazorWebApp/Components/CustomInputText.razor"::: - -The `CustomInputText` component can be used anywhere is used. The following component uses the shared `CustomInputText` component. - -`Starship11.razor`: - -:::moniker range=">= aspnetcore-9.0" - -:::code language="razor" source="~/../blazor-samples/9.0/BlazorSample_BlazorWebApp/Components/Pages/Starship11.razor"::: - -:::moniker-end - -:::moniker range=">= aspnetcore-8.0 < aspnetcore-9.0" - -:::code language="razor" source="~/../blazor-samples/8.0/BlazorSample_BlazorWebApp/Components/Pages/Starship11.razor"::: - -:::moniker-end - -:::moniker range="< aspnetcore-8.0" - -```razor -@page "/starship-11" -@using System.ComponentModel.DataAnnotations -@inject ILogger Logger - - - - - - - - -
    - CurrentValue: @Model?.Id -
    - -@code { - public Starship? Model { get; set; } - - protected override void OnInitialized() => Model ??= new(); - - private void Submit() - { - Logger.LogInformation("Submit called: Processing the form"); - } - - public class Starship - { - [Required] - [StringLength(10, ErrorMessage = "Id is too long.")] - public string? Id { get; set; } - } -} -``` - - - -:::moniker-end - -## Validation Summary and Validation Message components - -The component summarizes all validation messages, which is similar to the [Validation Summary Tag Helper](xref:mvc/views/working-with-forms#the-validation-summary-tag-helper): - -```razor - -``` - -Output validation messages for a specific model with the `Model` parameter: - -```razor - -``` - -The component displays validation messages for a specific field, which is similar to the [Validation Message Tag Helper](xref:mvc/views/working-with-forms#the-validation-message-tag-helper). Specify the field for validation with the attribute and a lambda expression naming the model property: - -```razor - -``` - -The and components support arbitrary attributes. Any attribute that doesn't match a component parameter is added to the generated `
    ` or `
      ` element. If a class attribute is supplied, its value replaces the component's default CSS class. - -Control the style of validation messages in the app's stylesheet (`wwwroot/css/app.css` or `wwwroot/css/site.css`). The default `validation-message` class sets the text color of validation messages to red: - -```css -.validation-message { - color: red; -} -``` - -:::moniker range=">= aspnetcore-8.0" - -## Determine if a form field is valid - -Use to determine if a field is valid without obtaining validation messages. - - Supported, but not recommended: - -```csharp -var isValid = !editContext.GetValidationMessages(fieldIdentifier).Any(); -``` - - Recommended: - -```csharp -var isValid = editContext.IsValid(fieldIdentifier); -``` - -:::moniker-end - -## Custom validation attributes - -To ensure that a validation result is correctly associated with a field when using a [custom validation attribute](xref:mvc/models/validation#custom-attributes), pass the validation context's when creating the . - -`CustomValidator.cs`: - -:::moniker range=">= aspnetcore-8.0" - -```csharp -using System; -using System.ComponentModel.DataAnnotations; - -public class CustomValidator : ValidationAttribute -{ - protected override ValidationResult IsValid(object? value, - ValidationContext validationContext) - { - ... - - return new ValidationResult("Validation message to user.", - [ validationContext.MemberName! ]); - } -} -``` - -:::moniker-end - -:::moniker range=">= aspnetcore-6.0 < aspnetcore-8.0" - -```csharp -using System; -using System.ComponentModel.DataAnnotations; - -public class CustomValidator : ValidationAttribute -{ - protected override ValidationResult IsValid(object? value, - ValidationContext validationContext) - { - ... - - return new ValidationResult("Validation message to user.", - new[] { validationContext.MemberName! }); - } -} -``` - -:::moniker-end - -:::moniker range="< aspnetcore-6.0" - -```csharp -using System; -using System.ComponentModel.DataAnnotations; - -public class CustomValidator : ValidationAttribute -{ - protected override ValidationResult IsValid(object value, - ValidationContext validationContext) - { - ... - - return new ValidationResult("Validation message to user.", - new[] { validationContext.MemberName }); - } -} -``` - -:::moniker-end - -Inject services into custom validation attributes through the . The following example demonstrates a salad chef form that validates user input with dependency injection (DI). - -The `SaladChef` class indicates the approved starship ingredient list for a Ten Forward salad. - -`SaladChef.cs`: - -:::code language="csharp" source="~/../blazor-samples/8.0/BlazorSample_BlazorWebApp/SaladChef.cs"::: - -Register `SaladChef` in the app's DI container in the `Program` file: - -```csharp -builder.Services.AddTransient(); -``` - -The `IsValid` method of the following `SaladChefValidatorAttribute` class obtains the `SaladChef` service from DI to check the user's input. - -`SaladChefValidatorAttribute.cs`: - -:::code language="csharp" source="~/../blazor-samples/8.0/BlazorSample_BlazorWebApp/SaladChefValidatorAttribute.cs"::: - -The following component validates user input by applying the `SaladChefValidatorAttribute` (`[SaladChefValidator]`) to the salad ingredient string (`SaladIngredient`). - -`Starship12.razor`: - -:::moniker range=">= aspnetcore-9.0" - -:::code language="razor" source="~/../blazor-samples/9.0/BlazorSample_BlazorWebApp/Components/Pages/Starship12.razor"::: - -:::moniker-end - -:::moniker range=">= aspnetcore-8.0 < aspnetcore-9.0" - -:::code language="razor" source="~/../blazor-samples/8.0/BlazorSample_BlazorWebApp/Components/Pages/Starship12.razor"::: - -:::moniker-end - -:::moniker range="< aspnetcore-8.0" - -```razor -@page "/starship-12" -@inject SaladChef SaladChef - - - -

      - -

      - -
        - @foreach (var message in context.GetValidationMessages()) - { -
      • @message
      • - } -
      -
      - -@code { - private string? saladToppers; - - [SaladChefValidator] - public string? SaladIngredient { get; set; } - - protected override void OnInitialized() => - saladToppers ??= string.Join(", ", SaladChef.SaladToppers); -} -``` - -:::moniker-end - -:::moniker range=">= aspnetcore-7.0" - -## Custom validation CSS class attributes - -Custom validation CSS class attributes are useful when integrating with CSS frameworks, such as [Bootstrap](https://getbootstrap.com/). - -To specify custom validation CSS class attributes, start by providing CSS styles for custom validation. In the following example, valid (`validField`) and invalid (`invalidField`) styles are specified. - -Add the following CSS classes to the app's stylesheet: - -```css -.validField { - border-color: lawngreen; -} - -.invalidField { - background-color: tomato; -} -``` - -Create a class derived from that checks for field validation messages and applies the appropriate valid or invalid style. - -`CustomFieldClassProvider.cs`: - -:::moniker-end - -:::moniker range=">= aspnetcore-8.0" - -```csharp -using Microsoft.AspNetCore.Components.Forms; - -public class CustomFieldClassProvider : FieldCssClassProvider -{ - public override string GetFieldCssClass(EditContext editContext, - in FieldIdentifier fieldIdentifier) - { - var isValid = editContext.IsValid(fieldIdentifier); - - return isValid ? "validField" : "invalidField"; - } -} -``` - -:::moniker-end - -:::moniker range=">= aspnetcore-7.0 < aspnetcore-8.0" - -```csharp -using Microsoft.AspNetCore.Components.Forms; - -public class CustomFieldClassProvider : FieldCssClassProvider -{ - public override string GetFieldCssClass(EditContext editContext, - in FieldIdentifier fieldIdentifier) - { - var isValid = !editContext.GetValidationMessages(fieldIdentifier).Any(); - - return isValid ? "validField" : "invalidField"; - } -} -``` - -:::moniker-end - -:::moniker range=">= aspnetcore-7.0" - - - -Set the `CustomFieldClassProvider` class as the Field CSS Class Provider on the form's instance with . - -`Starship13.razor`: - -:::moniker-end - -:::moniker range=">= aspnetcore-9.0" - -:::code language="razor" source="~/../blazor-samples/9.0/BlazorSample_BlazorWebApp/Components/Pages/Starship13.razor"::: - -:::moniker-end - -:::moniker range=">= aspnetcore-8.0 < aspnetcore-9.0" - -:::code language="razor" source="~/../blazor-samples/8.0/BlazorSample_BlazorWebApp/Components/Pages/Starship13.razor"::: - -:::moniker-end - -:::moniker range=">= aspnetcore-7.0 < aspnetcore-8.0" - -```razor -@page "/starship-13" -@using System.ComponentModel.DataAnnotations -@inject ILogger Logger - - - - - - - - -@code { - private EditContext? editContext; - - public Starship? Model { get; set; } - - protected override void OnInitialized() - { - Model ??= new(); - editContext = new(Model); - editContext.SetFieldCssClassProvider(new CustomFieldClassProvider()); - } - - private void Submit() - { - Logger.LogInformation("Submit called: Processing the form"); - } - - public class Starship - { - [Required] - [StringLength(10, ErrorMessage = "Id is too long.")] - public string? Id { get; set; } - } -} -``` - - - -:::moniker-end - -:::moniker range=">= aspnetcore-7.0" - -The preceding example checks the validity of all form fields and applies a style to each field. If the form should only apply custom styles to a subset of the fields, make `CustomFieldClassProvider` apply styles conditionally. The following `CustomFieldClassProvider2` example only applies a style to the `Name` field. For any fields with names not matching `Name`, `string.Empty` is returned, and no style is applied. Using [reflection](/dotnet/csharp/advanced-topics/reflection-and-attributes/), the field is matched to the model member's property or field name, not an `id` assigned to the HTML entity. - -`CustomFieldClassProvider2.cs`: - -:::moniker-end - -:::moniker range=">= aspnetcore-8.0" - -```csharp -using Microsoft.AspNetCore.Components.Forms; - -public class CustomFieldClassProvider2 : FieldCssClassProvider -{ - public override string GetFieldCssClass(EditContext editContext, - in FieldIdentifier fieldIdentifier) - { - if (fieldIdentifier.FieldName == "Name") - { - var isValid = editContext.IsValid(fieldIdentifier); - - return isValid ? "validField" : "invalidField"; - } - - return string.Empty; - } -} -``` - -:::moniker-end - -:::moniker range=">= aspnetcore-7.0 < aspnetcore-8.0" - -```csharp -using Microsoft.AspNetCore.Components.Forms; - -public class CustomFieldClassProvider2 : FieldCssClassProvider -{ - public override string GetFieldCssClass(EditContext editContext, - in FieldIdentifier fieldIdentifier) - { - if (fieldIdentifier.FieldName == "Name") - { - var isValid = !editContext.GetValidationMessages(fieldIdentifier).Any(); - - return isValid ? "validField" : "invalidField"; - } - - return string.Empty; - } -} -``` - -:::moniker-end - -:::moniker range=">= aspnetcore-7.0" - - - -> [!NOTE] -> Matching the field name in the preceding example is case sensitive, so a model property member designated "`Name`" must match a conditional check on "`Name`": -> -> * Correctly matches: `fieldId.FieldName == "Name"` -> * Fails to match: `fieldId.FieldName == "name"` -> * Fails to match: `fieldId.FieldName == "NAME"` -> * Fails to match: `fieldId.FieldName == "nAmE"` - -Add an additional property to `Model`, for example: - -```csharp -[StringLength(10, ErrorMessage = "Description is too long.")] -public string? Description { get; set; } -``` - -Add the `Description` to the `CustomValidationForm` component's form: - -```razor - -``` - -Update the instance in the component's `OnInitialized` method to use the new Field CSS Class Provider: - -```csharp -editContext?.SetFieldCssClassProvider(new CustomFieldClassProvider2()); -``` - -Because a CSS validation class isn't applied to the `Description` field, it isn't styled. However, field validation runs normally. If more than 10 characters are provided, the validation summary indicates the error: - -> Description is too long. - -In the following example: - -* The custom CSS style is applied to the `Name` field. -* Any other fields apply logic similar to Blazor's default logic and using Blazor's default field CSS validation styles, `modified` with `valid` or `invalid`. Note that for the default styles, you don't need to add them to the app's stylesheet if the app is based on a Blazor project template. For apps not based on a Blazor project template, the default styles can be added to the app's stylesheet: - - ```css - .valid.modified:not([type=checkbox]) { - outline: 1px solid #26b050; - } - - .invalid { - outline: 1px solid red; - } - ``` - -`CustomFieldClassProvider3.cs`: - -:::moniker-end - -:::moniker range=">= aspnetcore-8.0" - -```csharp -using Microsoft.AspNetCore.Components.Forms; - -public class CustomFieldClassProvider3 : FieldCssClassProvider -{ - public override string GetFieldCssClass(EditContext editContext, - in FieldIdentifier fieldIdentifier) - { - var isValid = editContext.IsValid(fieldIdentifier); - - if (fieldIdentifier.FieldName == "Name") - { - return isValid ? "validField" : "invalidField"; - } - else - { - if (editContext.IsModified(fieldIdentifier)) - { - return isValid ? "modified valid" : "modified invalid"; - } - else - { - return isValid ? "valid" : "invalid"; - } - } - } -} -``` +To supply different class names, for example to integrate with a CSS framework such as [Bootstrap](https://getbootstrap.com/), see . -:::moniker-end +:::moniker range=">= aspnetcore-8.0" + +### Determine if a form field is valid + +Use to determine if a field is valid without obtaining validation messages. -:::moniker range=">= aspnetcore-7.0 < aspnetcore-8.0" + Supported, but not recommended: ```csharp -using Microsoft.AspNetCore.Components.Forms; +var isValid = !editContext.GetValidationMessages(fieldIdentifier).Any(); +``` -public class CustomFieldClassProvider3 : FieldCssClassProvider -{ - public override string GetFieldCssClass(EditContext editContext, - in FieldIdentifier fieldIdentifier) - { - var isValid = !editContext.GetValidationMessages(fieldIdentifier).Any(); + Recommended: - if (fieldIdentifier.FieldName == "Name") - { - return isValid ? "validField" : "invalidField"; - } - else - { - if (editContext.IsModified(fieldIdentifier)) - { - return isValid ? "modified valid" : "modified invalid"; - } - else - { - return isValid ? "valid" : "invalid"; - } - } - } -} +```csharp +var isValid = editContext.IsValid(fieldIdentifier); ``` :::moniker-end -:::moniker range=">= aspnetcore-7.0" +## Choose the validation your form needs - +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: + +:::moniker range=">= aspnetcore-11.0" -Update the instance in the component's `OnInitialized` method to use the preceding Field CSS Class Provider: +| 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 . | -```csharp -editContext.SetFieldCssClassProvider(new CustomFieldClassProvider3()); -``` +:::moniker-end -Using `CustomFieldClassProvider3`: +:::moniker range="= aspnetcore-10.0" -* 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. +| 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 -## Class-level validation with `IValidatableObject` +:::moniker range="< aspnetcore-10.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. +| 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). | -:::moniker range="< aspnetcore-10.0" +:::moniker-end -## Blazor data annotations validation package +:::moniker range=">= aspnetcore-10.0" -> [!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. +### Nested objects and collections require additional configuration -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*. +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. + +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`). + +For the full guidance and an example, see . > [!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. +> 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). :::moniker-end -:::moniker range="< aspnetcore-6.0" +## Custom validation rules -## `[CompareProperty]` attribute +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. -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 range=">= aspnetcore-10.0" + +For guidance on writing these rules, which is shared with Minimal APIs, see . :::moniker-end -:::moniker range=">= aspnetcore-10.0" +:::moniker range="< aspnetcore-10.0" + +For guidance on writing these rules, see [Custom attributes](xref:mvc/models/validation#custom-attributes) and [`IValidatableObject`](xref:mvc/models/validation#ivalidatableobject). -## Nested objects and collection types +:::moniker-end -Blazor form validation includes support for validating properties of nested objects and collection items with the built-in . +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 . -To create a validated form, use a component inside an component. +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. -To opt into the nested objects and collection types validation feature: +### Associate a validation result with a field -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. +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. -The following example demonstrates customer orders with nested collection form validation. +`CustomValidator.cs`: -In `Program.cs`, call on the service collection: +:::moniker range=">= aspnetcore-8.0" ```csharp -builder.Services.AddValidation(); +using System; +using System.ComponentModel.DataAnnotations; + +public class CustomValidator : ValidationAttribute +{ + protected override ValidationResult IsValid(object? value, + ValidationContext validationContext) + { + ... + + return new ValidationResult("Validation message to user.", + [ validationContext.MemberName! ]); + } +} ``` -In the following `Order` class, the `[ValidatableType]` attribute is required on the top-level model type. The other types are discovered automatically. +:::moniker-end -`Order.cs`: +:::moniker range=">= aspnetcore-6.0 < aspnetcore-8.0" ```csharp +using System; using System.ComponentModel.DataAnnotations; -[ValidatableType] -public class Order -{ - public Customer Customer { get; set; } = new(); - public List OrderItems { get; set; } = []; -} - -public class Customer +public class CustomValidator : ValidationAttribute { - [Required(ErrorMessage = "Name is required.")] - public string? FullName { get; set; } - - [Required(ErrorMessage = "Email is required.")] - public string? Email { get; set; } + protected override ValidationResult IsValid(object? value, + ValidationContext validationContext) + { + ... - public ShippingAddress ShippingAddress { get; set; } = new(); + return new ValidationResult("Validation message to user.", + new[] { validationContext.MemberName! }); + } } ``` -`OrderItem.cs`: +:::moniker-end + +:::moniker range="< aspnetcore-6.0" ```csharp -public class OrderItem -{ - [Required(ErrorMessage = "Id is required.")] - public int Id { get; set; } +using System; +using System.ComponentModel.DataAnnotations; - [Required(ErrorMessage = "Description is required.")] - public string? Description { get; set; } +public class CustomValidator : ValidationAttribute +{ + protected override ValidationResult IsValid(object value, + ValidationContext validationContext) + { + ... - [Required(ErrorMessage = "Price is required.")] - public decimal Price { get; set; } + return new ValidationResult("Validation message to user.", + new[] { validationContext.MemberName }); + } } ``` -`ShippingAddress.cs`: +:::moniker-end + +### Inject services into a custom validation attribute -```csharp -public class ShippingAddress -{ - [Required(ErrorMessage = "Street is required.")] - public string? Street { get; set; } +Inject services into custom validation attributes through the . The following example demonstrates a salad chef form that validates user input with dependency injection (DI). - [Required(ErrorMessage = "City is required.")] - public string? City { get; set; } +The `SaladChef` class indicates the approved starship ingredient list for a Ten Forward salad. - [Required(ErrorMessage = "State/Province is required.")] - public string? StateProvince { get; set; } +`SaladChef.cs`: - [Required(ErrorMessage = "PostalCode is required.")] - public string? PostalCode { get; set; } -} +:::code language="csharp" source="~/../blazor-samples/8.0/BlazorSample_BlazorWebApp/SaladChef.cs"::: + +Register `SaladChef` in the app's DI container in the `Program` file: + +```csharp +builder.Services.AddTransient(); ``` -In the following `OrderPage` component, the component is present in the component. +The `IsValid` method of the following `SaladChefValidatorAttribute` class obtains the `SaladChef` service from DI to check the user's input. + +`SaladChefValidatorAttribute.cs`: + +:::code language="csharp" source="~/../blazor-samples/8.0/BlazorSample_BlazorWebApp/SaladChefValidatorAttribute.cs"::: + +The following component validates user input by applying the `SaladChefValidatorAttribute` (`[SaladChefValidator]`) to the salad ingredient string (`SaladIngredient`). + +`Starship12.razor`: + +:::moniker range=">= aspnetcore-9.0" + +:::code language="razor" source="~/../blazor-samples/9.0/BlazorSample_BlazorWebApp/Components/Pages/Starship12.razor"::: + +:::moniker-end + +:::moniker range=">= aspnetcore-8.0 < aspnetcore-9.0" + +:::code language="razor" source="~/../blazor-samples/8.0/BlazorSample_BlazorWebApp/Components/Pages/Starship12.razor"::: + +:::moniker-end -`OrderPage.razor`: +:::moniker range="< aspnetcore-8.0" ```razor - - +@page "/starship-12" +@inject SaladChef SaladChef -

      Customer Details

      -
      + + +

      - -

      - - // ... form continues ... +

      + +
        + @foreach (var message in context.GetValidationMessages()) + { +
      • @message
      • + } +
      @code { - public Order? Model { get; set; } + private string? saladToppers; - protected override void OnInitialized() => Model ??= new(); + [SaladChefValidator] + public string? SaladIngredient { get; set; } + + protected override void OnInitialized() => + saladToppers ??= string.Join(", ", SaladChef.SaladToppers); } ``` -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. +:::moniker-end -For guidance on using validation models from a different assembly, such as a library or the `.Client` project of a Blazor Web App, see . +### Class-level validation with `IValidatableObject` -:::moniker-end +[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" @@ -2626,6 +488,89 @@ public class ShipDescription :::moniker-end +:::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-11.0" + +## Display pending and faulted validation state + +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. + +To author asynchronous validation rules, see for attribute-based rules, or for validator components. + +While an async task is in flight, the field is *pending*. If an async task throws an exception other than , the field is *faulted*. Each state has both a per-field and a form-level query: + +| State | Per-field | Form-level (any field) | +|----------|----------------------------------------------------|----------------------------------| +| Pending | `EditContext.IsValidationPending(fieldIdentifier)` | `EditContext.IsValidationPending()` | +| Faulted | `EditContext.IsValidationFaulted(fieldIdentifier)` | `EditContext.IsValidationFaulted()` | + +The per-field overloads accept either a or a `() => model.Property` lambda for convenient use in Razor markup: + +```razor + + + +@if (EditContext.IsValidationPending(() => Model.Username)) +{ + Checking… +} +else if (EditContext.IsValidationFaulted(() => Model.Username)) +{ + + Validation could not be completed. + +} +``` + +The form-level parameterless overloads return `true` when any field is currently pending or faulted. A common use is disabling the submit button while validation is in flight: + +```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; +} +``` + +:::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 +583,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 +715,16 @@ 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 6af830485c27..9c988a5aaa7b 100644 --- a/aspnetcore/blazor/globalization-localization.md +++ b/aspnetcore/blazor/globalization-localization.md @@ -34,7 +34,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..79ee5d0d5069 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,199 +31,159 @@ 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. -## Register validation in multi-assembly apps +### Validation when `AddValidation` isn't called -To validate types from separate assemblies: +The consequence of omitting , or of calling it but not having a type discovered by the source generator, differs by framework: -* 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. +:::moniker range=">= aspnetcore-11.0" -### Minimal API example +| 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. | -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. +:::moniker-end -Create a service collection extension method in an assembly that defines Minimal API endpoints and call it from the host app. +:::moniker range="< aspnetcore-11.0" -`ServiceCollectionExtensions.cs` in the assembly that defines the endpoints, which uses the example namespace `MinimalApisAssembly.Extensions`: +| 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. | -```csharp -namespace MinimalApisAssembly.Extensions; +:::moniker-end -public static class ServiceCollectionExtensions -{ - public static IServiceCollection AddApiValidation( - this IServiceCollection services) - { - return services.AddValidation(); - } -} -``` +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: -In the host app's `Program` file, call the extension method instead of calling `AddValidation` directly: +* 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). -```csharp -using MinimalApisAssembly.Extensions; +## 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 -builder.Services.AddApiValidation(); +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. -var app = builder.Build(); +:::moniker range="< aspnetcore-11.0" -app.MapApi(); -``` +> [!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 -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. +### Type validation -### Blazor Web App example +Type validation is the next step after parameter validation (and is the first step in Blazor). It involves the following steps: -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. +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. -Create a service collection extension method in the assembly that defines the validatable types and call it from the host app. +### Property validation + +Property validation happens as part of the type validation as explained in the previous section. It involves the following steps: -For model validation defined in the `.Client` project of a Blazor Web App: +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. -* 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 . +## Write custom validation rules -The preceding approach results in validation of the types from both assemblies. +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. -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. +### Custom validation attributes -`ServiceCollectionExtensions.cs` in the `.Client` project that defines validatable types, which uses the example namespace `BlazorSample.Client.Extensions`: +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 -namespace BlazorSample.Client.Extensions; +using System.ComponentModel.DataAnnotations; -public static class ServiceCollectionExtensions +public class EvenNumberAttribute : ValidationAttribute { - public static IServiceCollection AddValidationForClientTypes( - this IServiceCollection services) + protected override ValidationResult? IsValid(object? value, + ValidationContext validationContext) { - return services.AddValidation(); + if (value is int number && number % 2 != 0) + { + return new ValidationResult( + "The value must be an even number.", + [ validationContext.MemberName! ]); + } + + return ValidationResult.Success; } } ``` -In the server project's `Program` file: - -* Call the `.Client` project's service collection extension method to validate types in the `.Client` project. -* Call to validate types in the server project. +Apply the attribute to a property in the same way as a built-in attribute: ```csharp -using BlazorSample.Client.Extensions; - -... - -builder.Services.AddValidationForClientTypes(); -builder.Services.AddValidation(); +public class Order +{ + [EvenNumber] + public int Quantity { get; set; } +} ``` -:::moniker range="= aspnetcore-10.0" - -## Experimental API in apps that target .NET 10 +### Resolve services in a validation attribute -Attributes from the [`Microsoft.Extensions.Validation` NuGet package](https://www.nuget.org/packages/Microsoft.Extensions.Validation) ( and ) are published as *experimental* in .NET 10. The package is intended to provide a new shared infrastructure for validation features across frameworks, and publishing experimental types provides greater flexibility for the final design of the public API for better support in consuming frameworks. As of .NET 11, the attributes are no longer experimental, so the guidance in this section doesn't apply to apps that target .NET 11 or later. - -In Blazor apps, types are made available via a generated embedded attribute. If a web app project that uses the `Microsoft.NET.Sdk.Web` SDK (``) or an RCL that uses the `Microsoft.NET.Sdk.Razor` SDK (``) contains Razor components (`.razor`), the framework automatically generates an internal attribute inside the project (`Microsoft.Extensions.Validation.Embedded.ValidatableType`, `Microsoft.Extensions.Validation.Embedded.SkipValidation`). These types are interchangeable with the actual attributes and not marked experimental. In the majority of cases, developers use the `[ValidatableType]`/`[SkipValidation]` attributes on their classes without concern over their source. +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: -However, the preceding approach isn't viable in plain class libraries that use the `Microsoft.NET.Sdk` SDK (``). Using the types in a plain class library results in a code analysis warning: - -> :::no-loc text="ASP0029: 'Microsoft.Extensions.Validation.ValidatableTypeAttribute' is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed."::: - -The warning can be suppressed using any of the following approaches: +```csharp +protected override ValidationResult? IsValid(object? value, + ValidationContext validationContext) +{ + var catalog = validationContext.GetService(); -* A `` property in the project file: + ... +} +``` - ```xml - - $(NoWarn);ASP0029 - - ``` +For a service that must be resolved, use . Services resolved this way must be registered in the app's service container. -* A [`pragma` directive](/cpp/preprocessor/pragma-directives-and-the-pragma-keyword) where the attribute is used: +### Class-level validation with `IValidatableObject` - ```csharp - #pragma warning disable ASP0029 - [Microsoft.Extensions.Validation.ValidatableType] - #pragma warning restore ASP0029 - ``` - -* An [EditorConfig file (`.editorconfig`)](/visualstudio/ide/create-portable-custom-editor-options) rule: - - ``` - dotnet_diagnostic.ASP0029.severity = none - ``` - -If suppressing the warning isn't acceptable, manually create the embedded attribute in the library that the Web and Razor SDKs generate automatically. - -`ValidatableTypeAttribute.cs`: +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 -namespace Microsoft.Extensions.Validation.Embedded +using System.ComponentModel.DataAnnotations; + +public class DateRange : IValidatableObject { - [AttributeUsage(AttributeTargets.Class)] - internal sealed class ValidatableTypeAttribute : Attribute + 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) ]); + } } } ``` -Use the exact namespace (`Microsoft.Extensions.Validation.Embedded`) and class name () in order for the validation source generator to detect and use the type. You can declare a global `using` statement for the namespace, either with a `global using Microsoft.Extensions.Validation.Embedded;` statement or with a `` item in the library's project file. +:::moniker range=">= aspnetcore-11.0" -Whichever approach is adopted, denote the presence of the workaround for a future update to your code when the app can target .NET 11 or later. At that time, you can remove your workarounds from the app. +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 -## 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. +> 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" @@ -237,7 +204,7 @@ When validating properties on a type, all validation tasks are started concurren 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. +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 . @@ -283,17 +250,334 @@ 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 + +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 + +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. + +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. + +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 assembly that declares validatable types. The method calls so that the source generator runs in that assembly: + + ```csharp + namespace ValidatableTypesAssembly.Extensions; + + public static class ServiceCollectionExtensions + { + public static IServiceCollection AddValidationForLibraryTypes( + this IServiceCollection services) + { + return services.AddValidation(); + } + } + ``` + +* Call each of those extension methods from the host app, along with `AddValidation` for the host app's own types: + + ```csharp + using ValidatableTypesAssembly.Extensions; + + ... + + builder.Services.AddValidationForLibraryTypes(); + builder.Services.AddValidation(); + ``` + +The preceding approach validates the types from both assemblies. + +Two framework-specific notes: + +* **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: + + ```csharp + builder.Services.AddApiValidation(); + + ... + + var app = builder.Build(); + + app.MapApi(); + ``` + +* **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" + +## Experimental API in apps that target .NET 10 + +Attributes from the [`Microsoft.Extensions.Validation` NuGet package](https://www.nuget.org/packages/Microsoft.Extensions.Validation) ( and ) are published as *experimental* in .NET 10. The package is intended to provide a new shared infrastructure for validation features across frameworks, and publishing experimental types provides greater flexibility for the final design of the public API for better support in consuming frameworks. As of .NET 11, the attributes are no longer experimental, so the guidance in this section doesn't apply to apps that target .NET 11 or later. + +In Blazor apps, types are made available via a generated embedded attribute. If a web app project that uses the `Microsoft.NET.Sdk.Web` SDK (``) or an RCL that uses the `Microsoft.NET.Sdk.Razor` SDK (``) contains Razor components (`.razor`), the framework automatically generates an internal attribute inside the project (`Microsoft.Extensions.Validation.Embedded.ValidatableType`, `Microsoft.Extensions.Validation.Embedded.SkipValidation`). These types are interchangeable with the actual attributes and not marked experimental. In the majority of cases, developers use the `[ValidatableType]`/`[SkipValidation]` attributes on their classes without concern over their source. + +However, the preceding approach isn't viable in plain class libraries that use the `Microsoft.NET.Sdk` SDK (``). Using the types in a plain class library results in a code analysis warning: + +> :::no-loc text="ASP0029: 'Microsoft.Extensions.Validation.ValidatableTypeAttribute' is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed."::: + +The warning can be suppressed using any of the following approaches: + +* A `` property in the project file: + + ```xml + + $(NoWarn);ASP0029 + + ``` + +* A [`pragma` directive](/cpp/preprocessor/pragma-directives-and-the-pragma-keyword) where the attribute is used: + + ```csharp + #pragma warning disable ASP0029 + [Microsoft.Extensions.Validation.ValidatableType] + #pragma warning restore ASP0029 + ``` + +* An [EditorConfig file (`.editorconfig`)](/visualstudio/ide/create-portable-custom-editor-options) rule: + + ``` + dotnet_diagnostic.ASP0029.severity = none + ``` + +If suppressing the warning isn't acceptable, manually create the embedded attribute in the library that the Web and Razor SDKs generate automatically. + +`ValidatableTypeAttribute.cs`: + +```csharp +namespace Microsoft.Extensions.Validation.Embedded +{ + [AttributeUsage(AttributeTargets.Class)] + internal sealed class ValidatableTypeAttribute : Attribute + { + } +} +``` + +Use the exact namespace (`Microsoft.Extensions.Validation.Embedded`) and class name () in order for the validation source generator to detect and use the type. You can declare a global `using` statement for the namespace, either with a `global using Microsoft.Extensions.Validation.Embedded;` statement or with a `` item in the library's project file. + +Whichever approach is adopted, denote the presence of the workaround for a future update to your code when the app can target .NET 11 or later. At that time, you can remove your workarounds from the app. + +:::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 +585,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 77d5b347d07c..2dc33cc2590c 100644 --- a/aspnetcore/release-notes/aspnetcore-11/includes/blazor.md +++ b/aspnetcore/release-notes/aspnetcore-11/includes/blazor.md @@ -651,7 +651,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: @@ -662,12 +662,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)) { @@ -708,75 +710,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. @@ -866,15 +809,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-preview-7.md b/aspnetcore/release-notes/aspnetcore-11/includes/validation-attributes-no-longer-experimental-preview-7.md index 733eb1fb93f4..c1f4d257806a 100644 --- a/aspnetcore/release-notes/aspnetcore-11/includes/validation-attributes-no-longer-experimental-preview-7.md +++ b/aspnetcore/release-notes/aspnetcore-11/includes/validation-attributes-no-longer-experimental-preview-7.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-11/includes/validation-localization-preview-7.md b/aspnetcore/release-notes/aspnetcore-11/includes/validation-localization-preview-7.md index 97acd31ef834..d800d403ab5e 100644 --- a/aspnetcore/release-notes/aspnetcore-11/includes/validation-localization-preview-7.md +++ b/aspnetcore/release-notes/aspnetcore-11/includes/validation-localization-preview-7.md @@ -19,7 +19,20 @@ public class CustomerModel } ``` -Keys resolve against the model's own resources, and a miss falls back to the attribute's built-in message. Use `ValidationOptions.LocalizerProvider` to resolve keys from a shared resource file instead: +Keys resolve against the model's own resources, and a miss falls back to the attribute's built-in message. When an attribute doesn't set `ErrorMessage`, conventional keys are tried from most to least specific, so the default message of an attribute can be translated once for the whole app: + +```csharp +[ValidatableType] +public class ContactModel +{ + // Looks up 'ContactModel_Username_RequiredAttribute_Error', then + // 'ContactModel_RequiredAttribute_Error', then 'RequiredAttribute_Error'. + [Required] + public string? Username { get; set; } +} +``` + +Use `ValidationOptions.LocalizerProvider` to resolve keys from a shared resource file instead: ```csharp builder.Services.AddValidation(options => @@ -28,6 +41,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 @@ -41,3 +61,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/release-notes/aspnetcore-5.0.md b/aspnetcore/release-notes/aspnetcore-5.0.md index 78afe7ffdf4c..95a120f2f5c7 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 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` diff --git a/aspnetcore/toc.yml b/aspnetcore/toc.yml index 97ce3f765920..daf87ed35364 100644 --- a/aspnetcore/toc.yml +++ b/aspnetcore/toc.yml @@ -873,6 +873,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