diff --git a/aspnetcore/blazor/components/component-disposal.md b/aspnetcore/blazor/components/component-disposal.md index a54ad0967ed3..4ca51cfce557 100644 --- a/aspnetcore/blazor/components/component-disposal.md +++ b/aspnetcore/blazor/components/component-disposal.md @@ -331,7 +331,7 @@ protected override void OnInitialized() } ``` -The full example of the preceding code with anonymous lambda expressions appears in the article. +The full example of the preceding code with anonymous lambda expressions appears in the article. For more information, see [Cleaning up unmanaged resources](/dotnet/standard/garbage-collection/unmanaged) and the topics that follow it on implementing the `Dispose` and `DisposeAsync` methods. diff --git a/aspnetcore/blazor/forms/binding.md b/aspnetcore/blazor/forms/binding.md index 9e10329da8a7..3c0bab64f5c2 100644 --- a/aspnetcore/blazor/forms/binding.md +++ b/aspnetcore/blazor/forms/binding.md @@ -5,7 +5,7 @@ author: guardrex description: Learn how to use binding in Blazor forms. monikerRange: '>= aspnetcore-3.1' ms.author: wpickett -ms.date: 08/24/2026 +ms.date: 09/22/2026 uid: blazor/forms/binding --- # ASP.NET Core Blazor forms binding @@ -403,6 +403,71 @@ Developers aren't expected to interact with component to create a custom component that uses the `oninput` event ([`input`](https://developer.mozilla.org/docs/Web/API/HTMLElement/input_event)) instead of the `onchange` event ([`change`](https://developer.mozilla.org/docs/Web/API/HTMLElement/change_event)). Use of the `input` event triggers field validation on each keystroke. + +The following `CustomInputText` component inherits the framework's `InputText` component and sets event binding to the `oninput` event ([`input`](https://developer.mozilla.org/docs/Web/API/HTMLElement/input_event)). + +`CustomInputText.razor`: + +:::code language="razor" source="~/../blazor-samples/8.0/BlazorSample_BlazorWebApp/Components/CustomInputText.razor"::: + +The `CustomInputText` component can be used anywhere is used. The following component uses the shared `CustomInputText` component. + +`Starship11.razor`: + +:::moniker range=">= aspnetcore-9.0" + +:::code language="razor" source="~/../blazor-samples/9.0/BlazorSample_BlazorWebApp/Components/Pages/Starship11.razor"::: + +:::moniker-end + +:::moniker range=">= aspnetcore-8.0 < aspnetcore-9.0" + +:::code language="razor" source="~/../blazor-samples/8.0/BlazorSample_BlazorWebApp/Components/Pages/Starship11.razor"::: + +:::moniker-end + +:::moniker range="< aspnetcore-8.0" + +```razor +@page "/starship-11" +@using System.ComponentModel.DataAnnotations +@inject ILogger Logger + + + + + + + + +
+ CurrentValue: @Model?.Id +
+ +@code { + public Starship? Model { get; set; } + + protected override void OnInitialized() => Model ??= new(); + + private void Submit() + { + Logger.LogInformation("Submit called: Processing the form"); + } + + public class Starship + { + [Required] + [StringLength(10, ErrorMessage = "Id is too long.")] + public string? Id { get; set; } + } +} +``` + +:::moniker-end + ## Custom input components For custom input processing scenarios, the following subsections demonstrate custom input components: diff --git a/aspnetcore/blazor/forms/index.md b/aspnetcore/blazor/forms/index.md index 0859df0e93e9..ce9960105126 100644 --- a/aspnetcore/blazor/forms/index.md +++ b/aspnetcore/blazor/forms/index.md @@ -4,7 +4,7 @@ author: guardrex description: Learn how to use forms in Blazor. monikerRange: '>= aspnetcore-3.1' ms.author: wpickett -ms.date: 11/11/2025 +ms.date: 09/22/2026 uid: blazor/forms/index --- # ASP.NET Core Blazor forms overview @@ -35,7 +35,7 @@ The Blazor framework supports forms and provides built-in input components: :::moniker range=">= aspnetcore-11.0" -In Blazor Web Apps that use static server-side rendering (static SSR), input components automatically participate in client-side validation when the form contains a component. For details, see . +In Blazor Web Apps that use static server-side rendering (static SSR), input components automatically participate in client-side validation when the form contains a component. For details, see . :::moniker-end @@ -187,7 +187,7 @@ In the next example, the preceding component is modified to create the form in t * If the `` form field contains more than ten characters when the **`Submit`** button is selected, an error appears in the validation summary ("`Id is too long.`"). `Submit` is **not** called. * If the `` form field contains a valid value when the **`Submit`** button is selected, `Submit` is called. -†The component is covered in the [Validator component](xref:blazor/forms/validation#validator-components) section. ‡The component is covered in the [Validation Summary and Validation Message components](xref:blazor/forms/validation#validation-summary-and-validation-message-components) section. +†The component is covered in the [Data Annotations Validator component and custom validation](xref:blazor/forms/validation#data-annotations-validator-component-and-custom-validation) section. ‡The component is covered in the [Validation Summary and Validation Message components](xref:blazor/forms/validation#validation-summary-and-validation-message-components) section. `Starship2.razor`: @@ -488,7 +488,7 @@ In Blazor Web Apps, client-side validation requires an active Blazor SignalR cir ## Client-side validation in static SSR forms -In Blazor Web Apps, forms in components that adopt static server-side rendering (static SSR) gain client-side validation automatically when a component is present in the form. For details, see . +In Blazor Web Apps, forms in components that adopt static server-side rendering (static SSR) gain client-side validation automatically when a component is present in the form. For details, see . :::moniker-end @@ -502,7 +502,7 @@ jQuery validation isn't supported in Razor components. We recommend any of the f * Follow the guidance in for any of the following scenarios: * Server-side validation in a Blazor Web App that adopts an interactive render mode. - * Client-side validation in [static SSR forms](xref:blazor/forms/validation#client-side-validation-in-static-ssr-forms). + * Client-side validation in [static SSR forms](xref:blazor/forms/validation-client-side). * Client-side validation in a standalone Blazor WebAssembly app. * Use native HTML validation attributes (see [Client-side form validation](https://developer.mozilla.org/docs/Learn/Forms/Form_validation)). * Adopt a third-party validation JavaScript library. diff --git a/aspnetcore/blazor/forms/validation-advanced.md b/aspnetcore/blazor/forms/validation-advanced.md new file mode 100644 index 000000000000..049a667e650b --- /dev/null +++ b/aspnetcore/blazor/forms/validation-advanced.md @@ -0,0 +1,447 @@ +--- +title: ASP.NET Core Blazor advanced form validation +ai-usage: ai-assisted +author: guardrex +description: Learn how to implement validator components and remote validation for Blazor forms. +monikerRange: '>= aspnetcore-3.1' +ms.author: wpickett +ms.date: 09/22/2026 +uid: blazor/forms/validation-advanced +--- +# ASP.NET Core Blazor advanced form validation + +[!INCLUDE[](~/includes/not-latest-version.md)] + +This article demonstrates reusable validator components and remote validation. For common form validation APIs, including data annotations, direct validation, message display, styling, state, and submit behavior, see . + +:::moniker range=">= aspnetcore-10.0" + +For model-based validation rules shared by Blazor and Minimal APIs, see . + +:::moniker-end + +:::moniker range=">= aspnetcore-11.0" + +For browser validation in static server-side rendering (static SSR), see . + +:::moniker-end + + + + + + +## Build a validator component + +A validator component encapsulates validation that uses a form's `EditContext` and . This is useful when the same validation behavior is used by several forms or when errors arrive from a service rather than from validation attributes on the model. + +The component: + +* Receives the form's `EditContext` as a cascading parameter. +* Creates a message store for its errors. +* Clears stale form errors when validation is requested. +* Clears a field's stale errors when the field changes. +* Exposes methods for displaying and clearing errors. +* Unsubscribes its event handlers when disposed. + +`CustomValidation.razor`: + +```razor +@implements IDisposable + +@code { + [CascadingParameter] + private EditContext? CurrentEditContext { get; set; } + + private ValidationMessageStore messages = default!; + + protected override void OnInitialized() + { + if (CurrentEditContext is null) + { + throw new InvalidOperationException( + "CustomValidation requires a cascading EditContext."); + } + + messages = new ValidationMessageStore(CurrentEditContext); + CurrentEditContext.OnValidationRequested += + HandleValidationRequested; + CurrentEditContext.OnFieldChanged += HandleFieldChanged; + } + + public void DisplayErrors(IDictionary errors) + { + foreach (var error in errors) + { + messages.Add( + CurrentEditContext!.Field(error.Key), + error.Value); + } + + CurrentEditContext!.NotifyValidationStateChanged(); + } + + public void ClearErrors() + { + messages.Clear(); + CurrentEditContext!.NotifyValidationStateChanged(); + } + + private void HandleValidationRequested( + object? sender, ValidationRequestedEventArgs e) => + ClearErrors(); + + private void HandleFieldChanged( + object? sender, FieldChangedEventArgs e) + { + messages.Clear(e.FieldIdentifier); + CurrentEditContext!.NotifyValidationStateChanged(); + } + + public void Dispose() + { + if (CurrentEditContext is not null) + { + CurrentEditContext.OnValidationRequested -= + HandleValidationRequested; + CurrentEditContext.OnFieldChanged -= HandleFieldChanged; + } + } +} +``` + +Place the component inside an `EditForm` and capture a component reference when the form or a service should display errors: + +```razor + + + + + + ... + + +@code { + private CustomValidation? customValidation; +} +``` + +The component can be used alongside `DataAnnotationsValidator`. Each validator has its own message store associated with the same `EditContext`, and `ValidationMessage` and `ValidationSummary` display messages from both validators. + +To implement a business rule inside the validator component instead of accepting external errors, run the rule from `HandleValidationRequested` or `HandleFieldChanged` and add its messages to `messages`. For a smaller example that performs this directly in a form component, see . + +:::moniker range=">= aspnetcore-11.0" + + + +### Add asynchronous validation + +The same component pattern supports asynchronous work: + +* In an `OnValidationRequested` handler, call `e.AddAsyncValidator` to register form-level work. `EditForm` awaits it before invoking `OnValidSubmit` or `OnInvalidSubmit`. +* In an `OnFieldChanged` handler, call to start validation for that field. Starting another validation for the same field supersedes and cancels the previous operation. + +For form-level asynchronous validation: + +```csharp +private void HandleValidationRequested( + object? sender, ValidationRequestedEventArgs e) => + e.AddAsyncValidator(ValidateAsync); + +private async Task ValidateAsync(CancellationToken cancellationToken) +{ + var field = CurrentEditContext!.Field(nameof(Model.Username)); + messages.Clear(field); + + var available = await Http.GetFromJsonAsync( + $"api/usernames/available?value={Uri.EscapeDataString(Model.Username)}", + cancellationToken); + + if (!available) + { + messages.Add(field, "The username is already taken."); + } + + CurrentEditContext.NotifyValidationStateChanged(); +} +``` + +For field-level asynchronous validation: + +```csharp +private void HandleFieldChanged( + object? sender, FieldChangedEventArgs e) +{ + CurrentEditContext!.RegisterAsyncFieldValidator( + e.FieldIdentifier, + token => ValidateFieldAsync(e.FieldIdentifier, token)); +} +``` + +Pass the supplied cancellation token to I/O. Clear prior messages before starting the operation, avoid publishing partial results after an exception, and call `NotifyValidationStateChanged` after updating messages. + +An operation canceled because it was superseded or because the validation pass was canceled is discarded. Other exceptions place the field or form in the faulted state. For displaying pending and faulted state, see . + +For a complete component that combines form-level and per-field asynchronous validation, see the following sample: + +:::code language="razor" source="~/../blazor-samples/11.0/BlazorSample_BlazorWebApp/Components/UsernameUniquenessValidator.razor"::: + +For asynchronous validation attributes on the model, see . + +:::moniker-end + +Validator component code runs where the component runs. In Interactive WebAssembly, it runs in the browser, while it runs on the server over the circuit in Interactive Server. + +:::moniker range=">= aspnetcore-8.0" + +In static SSR, validator component code runs on the server during the form post and doesn't provide live .NET field validation between requests. + +:::moniker-end + + + + +## Remote validation from Interactive WebAssembly + +Remote validation sends form data from an Interactive WebAssembly component to a server endpoint and adds returned field errors to the form's `EditContext`. It's useful when a rule requires private server data, an external service, or other logic that shouldn't run in the browser. + +The form: + +1. Runs data annotations validation locally. +1. Sends locally valid input to the endpoint from `OnValidSubmit`. +1. Receives field-keyed validation errors from the server. +1. Adds remote errors to the form through the validator component. + +`OnValidSubmit` only means that local validation succeeded. Process or save the model only after remote validation also succeeds. + +> [!IMPORTANT] +> Don't send private validation data or business rules to the browser. The server must validate every request independently because client-side validation can be bypassed. + +:::moniker range=">= aspnetcore-11.0" + +This example validates remotely when the form is submitted. For live per-field remote checks, use the asynchronous field-validation pattern from [Add asynchronous validation](#add-asynchronous-validation). + +:::moniker-end + +:::moniker range=">= aspnetcore-8.0" + +If the WebAssembly form is prerendered, its client-side services must also be available during prerendering. For the available approaches, see . + +:::moniker-end + +:::moniker range=">= aspnetcore-10.0" + +### Validate with a Minimal API + +Call in the server project to validate supported endpoint parameters before the handler runs. + +:::moniker-end + +:::moniker range="= aspnetcore-10.0" + +The APIs used for generated validation metadata are experimental in .NET 10. For details, see . + +:::moniker-end + +:::moniker range=">= aspnetcore-10.0" + +If the model is declared in the `.Client` project, register its generated validation metadata in both projects as described in . + +The endpoint adds a private business rule and returns errors keyed by model member name: + +:::moniker-end + +:::moniker range=">= aspnetcore-11.0" + +:::code language="csharp" source="~/../blazor-samples/11.0/BlazorWebAppRemoteValidation/BlazorWebAppRemoteValidation/Program.cs" id="snippet_ValidationEndpoint"::: + +:::moniker-end + +:::moniker range="= aspnetcore-10.0" + +```csharp +app.MapPost("/api/starships/validate", (StarshipModel model) => +{ + Dictionary errors = []; + + if (model.Classification == "Defense" && + string.IsNullOrWhiteSpace(model.Description)) + { + errors[nameof(model.Description)] = + ["A defense ship requires a description."]; + } + + if (errors.Count > 0) + { + return Results.ValidationProblem(errors); + } + + return Results.NoContent(); +}); +``` + +:::moniker-end + +:::moniker range=">= aspnetcore-10.0" + +Automatic validation rejects invalid data annotations before the handler runs. returns `400 Bad Request` with an `errors` property containing field-keyed messages. Successful validation returns `204 No Content`. + +:::moniker-end + +:::moniker range="< aspnetcore-10.0" + +### Validate with an API controller + +In a hosted Blazor WebAssembly solution, place the shared model in the `Shared` project and validate it with an API controller in the `Server` project. The `[ApiController]` attribute automatically rejects invalid data annotations before the action runs. + +```csharp +[ApiController] +[Route("api/starships/validate")] +public class StarshipValidationController : ControllerBase +{ + [HttpPost] + public IActionResult Validate(StarshipModel model) + { + if (model.Classification == "Defense" && + string.IsNullOrWhiteSpace(model.Description)) + { + ModelState.AddModelError( + nameof(model.Description), + "A defense ship requires a description."); + } + + if (!ModelState.IsValid) + { + return ValidationProblem(ModelState); + } + + return NoContent(); + } +} +``` + +Register and map controllers in the server project. The controller returns `400 Bad Request` with a `ValidationProblemDetails` response when validation fails and `204 No Content` when it succeeds. + +:::moniker-end + +### Call the endpoint and display errors + +Register an `HttpClient` in the WebAssembly project with the app's base address: + +:::moniker range=">= aspnetcore-11.0" + +:::code language="csharp" source="~/../blazor-samples/11.0/BlazorWebAppRemoteValidation/BlazorWebAppRemoteValidation.Client/Program.cs" id="snippet_HttpClient"::: + +:::moniker-end + +:::moniker range="< aspnetcore-11.0" + +```csharp +builder.Services.AddScoped(sp => + new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) }); +``` + +:::moniker-end + +Place the `CustomValidation` component from [Build a validator component](#build-a-validator-component) in the form: + +:::moniker range=">= aspnetcore-11.0" + +:::code language="razor" source="~/../blazor-samples/11.0/BlazorWebAppRemoteValidation/BlazorWebAppRemoteValidation.Client/Pages/Home.razor"::: + +:::moniker-end + +:::moniker range="< aspnetcore-11.0" + +```razor +@using System.Net +@using System.Net.Http.Json +@inject HttpClient Http + + + + + + + ... + + +@code { + private StarshipModel Model { get; } = new StarshipModel(); + private CustomValidation? remoteErrors; + + private async Task Submit() + { + using var response = await Http.PostAsJsonAsync( + "api/starships/validate", Model); + + if (response.IsSuccessStatusCode) + { + // Process or save the model. + return; + } + + if (response.StatusCode == HttpStatusCode.BadRequest) + { + var problem = await response.Content + .ReadFromJsonAsync(); + + if (problem is not null) + { + remoteErrors!.DisplayErrors(problem.Errors); + } + + return; + } + + response.EnsureSuccessStatusCode(); + } + + private sealed class ValidationProblemResponse + { + public Dictionary Errors { get; set; } = + new Dictionary(); + } +} +``` + +:::moniker-end + +The validator component clears a remote field error when that field changes, so the user can correct the value and submit again. Protect the endpoint according to the application's security requirements; authentication and authorization are outside the scope of this validation example. + +:::moniker range=">= aspnetcore-11.0" + +The [complete remote-validation sample](https://github.com/dotnet/blazor-samples/tree/main/11.0/BlazorWebAppRemoteValidation) includes the host endpoint, shared model, cross-assembly validation registration, validator component, and Interactive WebAssembly form. + +:::moniker-end + +:::moniker range="= aspnetcore-10.0" + +The [.NET 10 remote-validation sample](https://github.com/dotnet/blazor-samples/tree/main/10.0/BlazorWebAppRemoteValidation) demonstrates the same validation flow in an Interactive Auto app with authentication and a server-side proxy. + +:::moniker-end + +## Additional resources + +:::moniker range=">= aspnetcore-11.0" + +* +* +* +* + +:::moniker-end + +:::moniker range=">= aspnetcore-10.0 < aspnetcore-11.0" + +* +* +* + +:::moniker-end + +:::moniker range="< aspnetcore-10.0" + + + +:::moniker-end diff --git a/aspnetcore/blazor/forms/validation-client-side.md b/aspnetcore/blazor/forms/validation-client-side.md new file mode 100644 index 000000000000..3ae51b5726cc --- /dev/null +++ b/aspnetcore/blazor/forms/validation-client-side.md @@ -0,0 +1,208 @@ +--- +title: ASP.NET Core Blazor client-side form validation in static SSR +ai-usage: ai-assisted +author: guardrex +description: Learn how Blazor validates static server-side rendered forms in the browser before they're submitted. +monikerRange: '>= aspnetcore-11.0' +ms.author: wpickett +ms.date: 09/22/2026 +uid: blazor/forms/validation-client-side +--- +# ASP.NET Core Blazor client-side form validation in static SSR + + + +This article explains how Blazor adds live client-side validation to forms that use [static server-side rendering (static SSR)](xref:blazor/components/render-modes#static-server-side-rendering-static-ssr). The browser validates individual fields as the user edits them and validates the full form before it's submitted. If the client-side check passes, the form is submitted and validated again on the server. + +Forms that use an interactive render mode don't use the static SSR client-side validation feature described in this article. Both their per-field validation and full-form submit validation run in .NET through the form's . + +## How client-side validation works + +When a static SSR form contains a component, Blazor renders the form's validation rules into the page and enforces them using JavaScript before the form is submitted. The user sees validation errors without a round trip to the server. + +Client-side validation activates automatically when the following conditions are met: + +* The form's hosting component uses static SSR (no `@rendermode` directive applied to the component). +* The form contains a component. +* The form's model uses validation attributes. + +For the built-in set of validation attributes, no JavaScript configuration, additional package, or service registration is required. The section [Custom client-side validation rules](#custom-client-side-validation-rules) describes how to add support for custom validation attributes. + +> [!IMPORTANT] +> Client-side validation is a user experience improvement, not an authoritative validation pass. It can be bypassed by disabling or modifying the browser's JavaScript execution. Server-side validation runs after the form is posted and remains authoritative. Never rely on client-side validation to protect data integrity. + +## Supported validation attributes + +The following attributes are enforced client-side, matching the server-side data annotations behavior: + +* +* +* +* +* (only when the operand type is numeric) +* +* +* +* +* +* +* + +Validation attributes that don't appear in this list, including custom -derived attributes, aren't enforced client-side by default. They continue to run server-side after the form is submitted. To supply a client-side rule for a custom attribute, see the [Custom client-side validation rules](#custom-client-side-validation-rules) section. + +## Validation timing + +A field is validated when its value is committed. For text inputs (`` elements), this occurs when the field loses focus. Checkboxes and dropdown lists are validated immediately after selection. + +After a field has shown a validation error or after the form has been submitted at least once, the field is validated again on every keystroke so that corrections are reflected immediately. + +Submitting the form validates every tracked field. If any field is invalid, the submission is blocked and focus moves to the first invalid field. + +## Validation messages, localization, and accessibility + +Client-side validation uses to display messages for individual fields and to display messages for the whole form, which is the same way that interactive validation reports messages to users. + +When validation localization is configured, error messages are localized on the server as the page is rendered, so client-side validation displays the same localized strings as the server-side experience. Localization requires . For more information, see . + +ARIA attributes on input elements and validation message containers are managed by Blazor automatically, so assistive technologies announce validation errors without additional configuration. + +## Validation state CSS classes + +The client-side validation engine applies the same CSS classes as Blazor's interactive validation, which are shown in the following table. + +Element | Classes +--- | --- +Input | `valid` or `invalid`, plus `modified` once the user edits the field +Validation message | `validation-message` +Validation summary | `validation-summary-errors` or `validation-summary-valid` + +Client-side validation also calls the browser's [Constraint Validation API](https://developer.mozilla.org/docs/Web/API/Constraint_validation), so the standard CSS pseudo-classes `:valid` and `:invalid` reflect each input's current validation state. + +## Opt out of client-side validation + +The feature can be disabled at multiple levels. Server-side validation is unaffected by any of the options in this section. + +### Opt out for a single form + +Set the component's `DisableClientValidation` parameter to `true`: + +```razor + +``` + +### Opt out for the entire app + +Set `DisableClientValidation` on when Razor components services are registered in the `Program` file: + +```csharp +builder.Services.AddRazorComponents(options => +{ + options.DisableClientValidation = true; +}); +``` + +The global option takes precedence. When it's set to `true`, forms don't emit client-side validation rules. + +### Opt out for a single submit button + +Use the standard HTML [`formnovalidate` attribute](https://developer.mozilla.org/docs/Web/API/HTMLInputElement/formNoValidate) on the button. The form is posted without a client-side check, and server-side validation still runs after the post: + +```razor + +``` + +This can be used to implement a "save draft" or "back" button that don't require a completely valid form for the submit to succeed. + +## Custom client-side validation rules + +Custom validation attributes continue to run on the server but don't have a client-side implementation by default. Enforcing a custom rule in the browser involves two steps: emitting the rule from the .NET attribute and registering a matching JavaScript validator. The server-side implementation remains authoritative. + +### Emit the rule from .NET + +Implement `IClientValidationRuleProvider` on the validation attribute and return one or more `ClientValidationRule` instances. The rule's `Name` identifies the client-side validator, and `Parameters` supplies values the validator needs. + +The following `StartsWithAttribute` validates server-side in `IsValid` and contributes a `startswith` client-side rule with a `prefix` parameter: + +:::code language="csharp" source="~/../blazor-samples/11.0/BlazorSample_BlazorWebApp/Validation/StartsWithAttribute.cs"::: + +Apply the attribute to the model in the usual way: + +:::code language="csharp" source="~/../blazor-samples/11.0/BlazorSample_BlazorWebApp/Validation/ShipModel.cs"::: + +### Register the JavaScript validator + +In JavaScript, call `Blazor.formValidation.addValidator(name, validator)` to associate a rule name with a validator function. The `name` must exactly match `ClientValidationRule.Name`, including casing. Registrations are app-wide, and registering the same name again replaces the previous validator. + +> [!WARNING] +> If no JavaScript validator is registered for an emitted rule name, the rule is skipped in the browser. Server-side validation still runs when the form is posted. + +Register custom validators once from app startup code. Choose the registration location based on how Blazor starts, as described in the following table. + +Blazor startup | Registration location +--- | --- +Automatic startup (default) | A script immediately after `blazor.web.js` +Manual `Blazor.start()` | The continuation returned by `Blazor.start()` +Either startup style | A JavaScript initializer's `afterWebStarted` callback + +A [JavaScript initializer](xref:blazor/fundamentals/startup#javascript-initializers) works with either startup style. In a file named `{ASSEMBLY NAME}.lib.module.js` in the app's `wwwroot` folder: + +:::code language="javascript" source="~/../blazor-samples/11.0/BlazorSample_BlazorWebApp/wwwroot/BlazorSample.lib.module.js"::: + +With automatic startup, an app-specific validator script can instead be loaded immediately after `blazor.web.js`: + +```razor + + +``` + +The second script can call `Blazor.formValidation.addValidator` directly. + +With manual startup, define the registration in an app script: + +:::code language="javascript" source="~/../blazor-samples/11.0/BlazorSample_BlazorWebApp/wwwroot/js/custom-validation.js"::: + +Load the scripts with automatic startup disabled, and register the validators after `Blazor.start()` completes: + +:::code language="razor" source="~/../blazor-samples/11.0/BlazorSample_BlazorWebApp/Components/AppManualStartup.razor"::: + +> [!IMPORTANT] +> Register validators from app startup code, not from a page or form component. A component script can run before Blazor starts, and scripts added by enhanced navigation aren't executed. Static SSR components also can't use `IJSRuntime` because they don't have an interactive .NET runtime. + +### Write JavaScript validator functions + +The validator receives a context object with the following members, as shown in the following table. + +Member | Description +--- | --- +`value` | The field's current value as a string, or `null`/`undefined` when there's no value. +`element` | The validated `input`, `select`, or `textarea` element. +`params` | The rule's `Parameters` as a string dictionary. + +The validator is expected to return `{ success: true }` when the value is valid. Return `{ success: false }` to use the rule's server-supplied message, or you can return `{ success: false, message: '...' }` to override the message for that call. + +Empty values should normally be treated as valid by rules other than `required`, allowing an optional field to remain empty while still validating values that are supplied. + +Implement the same rule semantics in .NET and JavaScript, including case sensitivity, normalization, and empty-value handling. If the implementations differ, the browser and the authoritative server-side validation can produce different results. + +## Validate form on demand + +The `Blazor.formValidation` API also exposes JavaScript methods for validating on demand, as the following table shows. + +Method | Description +--- | --- +`validateField(element)` | Validates a single field element and updates its error display. Returns `true` when valid. +`validateForm(form)` | Validates every tracked field in a form. Returns `true` when all fields are valid. + +## Limitations + +* Client-side rules are emitted only for fields included in server-side validation as well. Without , only top-level model properties are validated. Validating nested objects and collections requires the app to call and the model to be discovered. For more information, see . Note that this limitation is an intentional feature to help prevent bugs where the authoritative server validation would be missing due to misconfiguration. +* Inputs added to an existing form by a later [streaming rendering](xref:blazor/components/rendering#streaming-rendering) update don't receive client-side validation. A form delivered in a single streamed batch is covered normally. +* Only the attributes listed in [Supported validation attributes](#supported-validation-attributes) have built-in client-side implementations. For example, a with a non-numeric operand type is only enforced on the server. Other attributes require a [custom client-side validation rule](#custom-client-side-validation-rules). +* Custom JavaScript validators are synchronous. Rules that require a network call or other asynchronous work must run on the server or use asynchronous validation with an interactive render mode. For more information, see . + +## Additional resources + +* +* +* +* diff --git a/aspnetcore/blazor/forms/validation.md b/aspnetcore/blazor/forms/validation.md index f1c8bc3fdc6c..9fcb3b289c72 100644 --- a/aspnetcore/blazor/forms/validation.md +++ b/aspnetcore/blazor/forms/validation.md @@ -5,2764 +5,628 @@ author: guardrex description: Learn how to use validation in Blazor forms. monikerRange: '>= aspnetcore-3.1' ms.author: wpickett -ms.date: 08/14/2026 +ms.date: 09/22/2026 uid: blazor/forms/validation --- # ASP.NET Core Blazor forms validation [!INCLUDE[](~/includes/not-latest-version.md)] -This article explains how to use validation in Blazor forms. +This article explains how to validate user input in Blazor forms. -:::moniker range=">= aspnetcore-10.0" - -For an overview of validation, including how to register services for Minimal API projects, see . +For most forms, the simplest and recommended approach is to add [data annotations validation attributes](xref:System.ComponentModel.DataAnnotations) to the model and place a component in the . Blazor also supports custom validation through the form's , either directly in the form component or in a reusable validator component. -:::moniker-end +Related articles provide more detail: -## Form validation +:::moniker range=">= aspnetcore-11.0" -In basic form validation scenarios, an instance can use declared and instances to validate form fields. A handler for the event of the executes custom validation logic. The handler's result updates the instance. +* For writing and configuring model-based validation rules, including custom and asynchronous rules, nested object validation, and localization, see . +* For live browser validation in static server-side rendering (static SSR), see . +* For complete validator-component and remote-validation implementations, see . -Basic form validation is useful in cases where the form's model is defined within the component hosting the form, either as members directly on the component or in a subclass. Use of a [validator component](#validator-components) is recommended where an independent model class is used across several components. +:::moniker-end -:::moniker range=">= aspnetcore-8.0 < aspnetcore-11.0" +:::moniker range=">= aspnetcore-10.0 < aspnetcore-11.0" -In Blazor Web Apps, client-side validation requires an active Blazor SignalR circuit. Client-side validation isn't available to forms in components that have adopted static server-side rendering (static SSR). Forms that adopt static SSR are validated on the server after the form is submitted. +* For writing and configuring model-based validation rules and nested object validation, see . +* For complete validator-component and remote-validation implementations, see . :::moniker-end -:::moniker range=">= aspnetcore-11.0" +:::moniker range="< aspnetcore-10.0" -In Blazor Web Apps that use interactive render modes (Server, WebAssembly, or Auto), client-side validation runs through the live pipeline as in earlier releases. Forms that adopt static server-side rendering (static SSR) gain client-side validation automatically when a component is present in the form. For details, see . +* For complete validator-component and remote-validation implementations, see . :::moniker-end -In the following component, the `HandleValidationRequested` handler method clears any existing validation messages by calling before validating the form. + -`Starship8.razor`: +## Validate with data annotations -:::moniker range=">= aspnetcore-9.0" +The following model uses and : -:::code language="razor" source="~/../blazor-samples/9.0/BlazorSample_BlazorWebApp/Components/Pages/Starship8.razor"::: - -:::moniker-end +`Starship.cs`: -:::moniker range=">= aspnetcore-8.0 < aspnetcore-9.0" +```csharp +using System.ComponentModel.DataAnnotations; -:::code language="razor" source="~/../blazor-samples/8.0/BlazorSample_BlazorWebApp/Components/Pages/Starship8.razor"::: +public class Starship +{ + [Required] + public string? Identifier { get; set; } -:::moniker-end + [Range(1, 10, ErrorMessage = "Accommodation must be between 1 and 10.")] + public int MaximumAccommodation { get; set; } +} +``` -:::moniker range="< aspnetcore-8.0" +Add the model to an `EditForm`, include `DataAnnotationsValidator`, and display errors with or . The callback is invoked only when validation succeeds: ```razor -@page "/starship-8" -@implements IDisposable -@inject ILogger Logger - -

Holodeck Configuration

+ + + - -
+

-

-
+ +

+ +

-

-
- -
-
- -
+ +

+ +
@code { - private EditContext? editContext; - - public Holodeck? Model { get; set; } - - private ValidationMessageStore? messageStore; - - protected override void OnInitialized() - { - Model ??= new(); - editContext = new(Model); - editContext.OnValidationRequested += HandleValidationRequested; - messageStore = new(editContext); - } - - private void HandleValidationRequested(object? sender, - ValidationRequestedEventArgs args) - { - messageStore?.Clear(); - - // Custom validation logic - if (!Model!.Options) - { - messageStore?.Add(() => Model.Options, "Select at least one."); - } - } + private Starship Model { get; } = new Starship(); private void Submit() { - Logger.LogInformation("Submit called: Processing the form"); - } - - public class Holodeck - { - public bool Subsystem1 { get; set; } - public bool Subsystem2 { get; set; } - public bool Options => Subsystem1 || Subsystem2; - } - - public void Dispose() - { - if (editContext is not null) - { - editContext.OnValidationRequested -= HandleValidationRequested; - } + // Process the valid form. } } ``` - - -:::moniker-end +:::moniker range=">= aspnetcore-8.0" -## Data Annotations Validator component and custom validation +For a static SSR form post, assign a unique `FormName` and receive the posted model with `[SupplyParameterFromForm]`: -The component attaches data annotations validation to a cascaded . Enabling data annotations validation requires the component. To use a different validation system than data annotations, use a custom implementation instead of the component. The framework implementations for are available for inspection in the reference source: +```razor + + ... + -* [`DataAnnotationsValidator`](https://github.com/dotnet/AspNetCore/blob/main/src/Components/Forms/src/DataAnnotationsValidator.cs) -* [`EnableDataAnnotationsValidation`](https://github.com/dotnet/AspNetCore/blob/main/src/Components/Forms/src/EditContextDataAnnotationsExtensions.cs) +@code { + [SupplyParameterFromForm] + private Starship? Model { get; set; } -:::moniker range=">= aspnetcore-10.0" + protected override void OnInitialized() => Model ??= new(); +} +``` -For details on validation behavior, see the [`DataAnnotationsValidator` validation behavior](#dataannotationsvalidator-validation-behavior) section. +For more information about form submission and model binding across render modes, see and . :::moniker-end -If you need to enable data annotations validation support for an in code, call with an injected (`@inject IServiceProvider ServiceProvider`) on the . For an advanced example, see the [`NotifyPropertyChangedValidationComponent` component in the ASP.NET Core Blazor framework's `BasicTestApp` (`dotnet/aspnetcore` GitHub repository)](https://github.com/dotnet/aspnetcore/blob/main/src/Components/test/testassets/BasicTestApp/FormsTest/NotifyPropertyChangedValidationComponent.razor). In a production version of the example, replace the `new TestServiceProvider()` argument for the service provider with an injected . - -[!INCLUDE[](~/includes/aspnetcore-repo-ref-source-links.md)] - -Blazor performs two types of validation: - -* *Field validation* is performed when the user tabs out of a field. During field validation, the component associates all reported validation results with the field. -* *Model validation* is performed when the user submits the form. During model validation, the component attempts to determine the field based on the member name that the validation result reports. Validation results that aren't associated with an individual member are associated with the model rather than a field. - -In custom validation scenarios: +Without a `DataAnnotationsValidator` component, validation attributes on the model don't participate in the form's validation. -* Validation manages a for a form's . -* The component is used to attach validation support to forms based on [validation attributes (data annotations)](xref:mvc/models/validation#validation-attributes). +### When validation runs -There are two general approaches for achieving custom validation, which are described in the next two sections of this article: +Blazor performs field validation and full-form validation: -* [Manual validation using the `OnValidationRequested` event](#manual-validation-using-the-onvalidationrequested-event): Manually validate a form's fields with data annotations validation and custom code for field checks when validation is requested via an event handler assigned to the event. -* [Validator components](#validator-components): One or more custom validator components can be used to process validation for different forms on the same page or the same form at different steps of form processing (for example, client validation followed by server-side validation in a Blazor Web App). +* Field validation runs after a field changes. In an interactive form, this occurs in .NET while the user edits the form. +* Full-form validation normally runs when `EditForm` handles submission through `OnValidSubmit` or `OnInvalidSubmit`. An `OnSubmit` handler takes control of validation, as described in [Control form submission](#control-form-submission). :::moniker range=">= aspnetcore-11.0" -## Client-side validation in static SSR forms +A static SSR form can provide live browser feedback with . The form is validated again authoritatively on the server when posted. -When a Blazor form that uses [static server-side rendering (static SSR)](xref:blazor/components/render-modes#static-server-side-rendering-static-ssr) contains a component, Blazor automatically validates the form in the browser before the form is submitted. Server-side data annotations validation continues to run after the form is posted, so the client-side check supplements but never replaces the server-side check. - -Client-side validation activates automatically when both conditions are met: +:::moniker-end -* The form's hosting component uses static SSR (no `@rendermode` directive applied to the component). -* The form contains a component. +:::moniker range=">= aspnetcore-8.0 < aspnetcore-11.0" -### Supported validation attributes +A static SSR form is validated on the server when posted and doesn't provide live field validation between requests. -The following attributes are enforced client-side, matching the server-side data annotations behavior: +:::moniker-end -* -* -* -* -* -* -* -* -* -* -* -* +Validation results that identify a member are associated with that field. Results without a member name are associated with the model and appear in a validation summary rather than a field's `ValidationMessage` component. -Validation attributes that don't appear in this list, including custom -derived attributes, aren't enforced client-side. They continue to run server-side after the form is submitted. +:::moniker range=">= aspnetcore-10.0" -### Validation timing +### Configure data annotations validation -A field validates when it loses focus (blur) for the first time. After a field has shown a validation error or after the form has been submitted at least once, the field re-validates on every change so corrections appear immediately. Submitting the form validates every field. +`DataAnnotationsValidator` always enables DataAnnotations validation for the form. To use the extended validation capabilities provided by the package, call the `AddValidation` extension method in the `Program` file: -### Validation messages and accessibility +```csharp +builder.Services.AddValidation(); +``` -The existing and components display client-side validation errors without any changes. ARIA attributes on input elements and on the validation message containers are managed by Blazor automatically so that assistive technologies announce validation errors without additional configuration. +The `AddValidation` call registers the package's validation services and activates a source generator that creates validation metadata for discovered model types. The available behavior depends on whether that metadata includes the form's model: -### Localized validation messages +:::moniker-end -When validation localization is configured through `Microsoft.Extensions.Validation`, error messages are localized at server-render time before being included in the page, so the client-side validation shows the same localized strings as the server-side experience. For more information, see . +:::moniker range=">= aspnetcore-11.0" -### CSS framework integration +| Configuration | Behavior | +|---|---| +| Generated metadata is available | Validates nested objects and collections and supports message localization. | +| Generated metadata isn't available | Validates top-level properties, but doesn't validate nested objects or collections and doesn't use the `Microsoft.Extensions.Validation` message-localization pipeline. | -Client-side validation integrates with the browser's [Constraint Validation API](https://developer.mozilla.org/docs/Web/API/Constraint_validation), so the standard CSS pseudo-classes `:valid` and `:invalid` reflect each input's current validation state. +:::moniker-end -### Enhanced navigation +:::moniker range=">= aspnetcore-10.0 < aspnetcore-11.0" -Client-side validation is preserved across [enhanced navigation](xref:blazor/fundamentals/navigation#enhanced-navigation-and-form-handling). When the user navigates to a page that contains an SSR form, the form is wired up automatically. Multiple forms on the same page validate independently of each other. +| Configuration | Behavior | +|---|---| +| Generated metadata is available | Validates nested objects and collections. | +| Generated metadata isn't available | Validates top-level properties only. | -### Opting out +The `ValidatableTypeAttribute` and `SkipValidationAttribute` APIs are experimental in .NET 10. For details and available workarounds, see . -To keep server-side data annotations validation but disable client-side enforcement for a single form, set the component's `DisableClientValidation` parameter to `true`: +:::moniker-end -```razor - -``` +:::moniker range=">= aspnetcore-10.0" -To bypass client-side validation for a single submit button, use the standard HTML `formnovalidate` attribute on the button. The form is then posted without a client-side check, and server-side validation still runs after the post: +When using `Microsoft.Extensions.Validation`, declare model types in C# files (`.cs`) rather than Razor component files (`.razor`). The source generator creates validation metadata from C# source and can't include model types declared in Razor components. -```razor - -``` +For configuration requirements, validation order, custom rules, nested object graphs, and generated metadata, see . :::moniker-end -## Manual validation using the `OnValidationRequested` event - -You can manually validate a form with a custom event handler assigned to the event to manage a . - -The Blazor framework provides the component to attach additional validation support to forms based on [validation attributes (data annotations)](xref:mvc/models/validation#validation-attributes). +:::moniker range="< aspnetcore-10.0" -Recalling the earlier `Starship8` component example, the `HandleValidationRequested` method is assigned to , where you can perform manual validation in C# code. A few changes demonstrate combining the existing manual validation with data annotations validation via a and a validation attribute applied to the `Holodeck` model. +### Validate nested object graphs -Reference the namespace in the component's Razor directives at the top of the component definition file: +In .NET 9 or earlier, `DataAnnotationsValidator` validates top-level model properties but doesn't recursively validate collection or complex-type properties. For recursive validation, use `ObjectGraphDataAnnotationsValidator` and `[ValidateComplexType]` from the experimental [`Microsoft.AspNetCore.Components.DataAnnotations.Validation` package](https://www.nuget.org/packages/Microsoft.AspNetCore.Components.DataAnnotations.Validation): ```razor -@using System.ComponentModel.DataAnnotations + + + ... + ``` -Add an `Id` property to the `Holodeck` model with a validation attribute to limit the string's length to six characters: - ```csharp -[StringLength(6)] -public string? Id { get; set; } +public class Starship +{ + [ValidateComplexType] + public ShipDescription Description { get; set; } = + new ShipDescription(); +} ``` -Add a component (``) to the form. Typically, the component is placed immediately under the `` tag, but you can place it anywhere in the form: +The package remains experimental in these framework versions. -```razor - -``` +:::moniker-end -Change the form's submit behavior in the `` tag from to , which ensures that the form is valid before executing the assigned event handler method: +:::moniker range="< aspnetcore-6.0" -```diff -- OnSubmit="Submit" -+ OnValidSubmit="Submit" -``` +### `[CompareProperty]` attribute -In the ``, add a field for the `Id` property: +For .NET 5 or earlier, use the experimental package's `ComparePropertyAttribute` instead of . `ComparePropertyAttribute` associates the validation result with the field consistently during field and full-form validation. -```razor -
- - -
-``` +:::moniker-end + + -After making the preceding changes, the form's behavior matches the following specification: +:::moniker range=">= aspnetcore-10.0" -* The data annotations validation on the `Id` property doesn't trigger a validation failure when the `Id` field merely loses focus. The validation executes when the user selects the **`Update`** button. -* Any manual validation that you want to perform in the `HandleValidationRequested` method assigned to the form's event executes when the user selects the form's **`Update`** button. In the existing code of the `Starship8` component example, the user must select either or both of the checkboxes to validate the form. -* The form doesn't process the `Submit` method until both the data annotations and manual validation pass. +### Write model-based custom rules -:::moniker range=">= aspnetcore-11.0" +When built-in attributes can't express a rule, use a custom or . For detailed guidance, see . -## Asynchronous validation +:::moniker-end + +:::moniker range="< aspnetcore-10.0" + +### Write model-based custom rules - exposes an asynchronous validation pipeline that custom validator components and custom submit handlers can use to run validation work that performs I/O, such as calling a server endpoint to check a value's uniqueness. The pipeline is built around the following API: +When built-in attributes can't express a rule, use a [custom validation attribute](xref:mvc/models/validation#custom-attributes) or implement [`IValidatableObject`](xref:mvc/models/validation#ivalidatableobject). Both run through `DataAnnotationsValidator`. - + -* `Microsoft.AspNetCore.Components.Forms.EditContext.ValidateAsync`: an asynchronous counterpart to that awaits any registered async work and accepts a . -* `ValidationRequestedEventArgs.AddAsyncValidator`: registers asynchronous work to run as part of the current validation pass. It's called from an handler, typically to validate the form as a whole on submit. -* `EditContext.RegisterAsyncFieldValidator`: registers asynchronous work for a single field. Registering a new validation for a field cancels and replaces the field's current pending validation. +Custom attributes can resolve registered services through . - awaits any registered async work before invoking . Sync-only forms continue to work without changes. +:::moniker-end -The built-in component runs the asynchronous `DataAnnotations` APIs (`AsyncValidationAttribute` and `IAsyncValidatableObject`), so asynchronous rules declared on the model work without adopting the patterns in this section. +## Add validation through `EditContext` -> [!IMPORTANT] -> Asynchronous work can only be registered during an asynchronous validation pass. If a form is validated with the synchronous method, `AddAsyncValidator` throws an that directs the caller to `ValidateAsync`. This guarantees that an asynchronous validator is never silently skipped. +`EditForm` creates an `EditContext` automatically when its `Model` parameter is assigned. To use validation APIs directly, create the `EditContext` yourself and assign it to . Don't assign both `Model` and `EditContext` to the same form. -### Form-level async validation +Custom validation commonly uses: -Subscribe to and call `AddAsyncValidator` from the handler to run async work whenever the form is validated as a whole. The framework invokes the registered validator with the validation pass's cancellation token, which should be passed to any I/O that the validator performs, so the work is cancelled when the framework supersedes the current validation pass. +* for full-form validation. +* for field validation. +* to add and clear messages. +* to notify the UI after messages change. -In the following example, a custom validator component checks a username against a remote endpoint when the form is submitted: +The following interactive-form pattern adds a form-level business rule alongside data annotations validation and rechecks the rule when either relevant field changes: ```razor @implements IDisposable -@inject HttpClient Http -@code { - [CascadingParameter] - private EditContext? CurrentEditContext { get; set; } + + + - [Parameter, EditorRequired] - public RegistrationModel Model { get; set; } = default!; + ... + - private ValidationMessageStore? _messages; +@code { + private Starship Model { get; } = new Starship(); + private EditContext editContext = default!; + private ValidationMessageStore messages = default!; protected override void OnInitialized() { - ArgumentNullException.ThrowIfNull(CurrentEditContext); - _messages = new ValidationMessageStore(CurrentEditContext); - CurrentEditContext.OnValidationRequested += OnValidationRequested; + editContext = new EditContext(Model); + messages = new ValidationMessageStore(editContext); + editContext.OnValidationRequested += ValidateBusinessRules; + editContext.OnFieldChanged += ValidateChangedField; } - private void OnValidationRequested( - object? sender, ValidationRequestedEventArgs e) => - e.AddAsyncValidator(ValidateUsernameAsync); + private void ValidateBusinessRules( + object? sender, ValidationRequestedEventArgs e) + { + messages.Clear(); + ValidateIdentifier(); + editContext.NotifyValidationStateChanged(); + } - private async Task ValidateUsernameAsync(CancellationToken token) + private void ValidateChangedField( + object? sender, FieldChangedEventArgs e) { - var field = CurrentEditContext!.Field(nameof(Model.Username)); - _messages!.Clear(field); + if (e.FieldIdentifier.FieldName != nameof(Starship.Identifier) && + e.FieldIdentifier.FieldName != nameof(Starship.MaximumAccommodation)) + { + return; + } - var available = await Http.GetFromJsonAsync( - $"api/usernames/available?value={Uri.EscapeDataString(Model.Username)}", - token); + messages.Clear( + editContext.Field(nameof(Starship.Identifier))); + ValidateIdentifier(); + editContext.NotifyValidationStateChanged(); + } - if (!available) + private void ValidateIdentifier() + { + if (Model.MaximumAccommodation == 1 && + string.IsNullOrWhiteSpace(Model.Identifier)) { - _messages.Add(field, "The username is already taken."); + messages.Add( + editContext.Field(nameof(Starship.Identifier)), + "An identifier is required for a single-occupant ship."); } - - CurrentEditContext!.NotifyValidationStateChanged(); + } + + private void Submit() + { + // Process the valid form. } public void Dispose() { - if (CurrentEditContext is not null) - { - CurrentEditContext.OnValidationRequested -= OnValidationRequested; - } + editContext.OnValidationRequested -= ValidateBusinessRules; + editContext.OnFieldChanged -= ValidateChangedField; } } ``` -Place the component inside an alongside the form's inputs. Because awaits the async handlers before invoking , the submit handler runs only after the remote check completes successfully: +An `OnFieldChanged` handler receives the changed field in `e.FieldIdentifier`. Clear or replace the affected messages and call `NotifyValidationStateChanged`, as the preceding example demonstrates. -```razor - - - - - - -``` +:::moniker range=">= aspnetcore-8.0" -### Per-field async validation +Static SSR doesn't provide live .NET field validation between requests. -For async work that should run when the user edits a single field, call `RegisterAsyncFieldValidator` with the field's and a validator that starts the work. The framework tracks each validation so the field's pending and faulted state can be queried and visualized independently of other fields. +:::moniker-end -The owns the cancellation token source. If the user edits the same field again while a check is in flight, the prior validation is canceled and superseded automatically, so there's no token source for the component to create, cancel, or dispose. +:::moniker range=">= aspnetcore-11.0" -Add the following members to the validator component shown in the previous section to re-run the uniqueness check whenever the `Username` field changes: +For asynchronous full-form validation, call `e.AddAsyncValidator` from an `OnValidationRequested` handler. For asynchronous field validation in an interactive form, call from an `OnFieldChanged` handler. A new asynchronous validation for the same field supersedes and cancels the previous one. -```csharp -protected override void OnInitialized() -{ - ArgumentNullException.ThrowIfNull(CurrentEditContext); - _messages = new ValidationMessageStore(CurrentEditContext); - CurrentEditContext.OnValidationRequested += OnValidationRequested; - CurrentEditContext.OnFieldChanged += OnFieldChanged; -} +For model-based asynchronous validation attributes, see . For a complete reusable validator component, see . -private void OnFieldChanged(object? sender, FieldChangedEventArgs e) -{ - if (e.FieldIdentifier.FieldName != nameof(RegistrationModel.Username)) - { - return; - } +:::moniker-end - CurrentEditContext!.RegisterAsyncFieldValidator( - e.FieldIdentifier, - token => CheckAsync(e.FieldIdentifier, token)); -} +For a reusable implementation that encapsulates event subscriptions and its message store, see . -private async Task CheckAsync(FieldIdentifier field, CancellationToken token) -{ - _messages!.Clear(field); + - var available = await Http.GetFromJsonAsync( - $"api/usernames/available?value={Uri.EscapeDataString(Model.Username)}", - token); +## Display validation messages - if (!available) - { - _messages.Add(field, "The username is already taken."); - } - - CurrentEditContext!.NotifyValidationStateChanged(); -} +Use to display messages associated with one field: -public void Dispose() -{ - if (CurrentEditContext is not null) - { - CurrentEditContext.OnValidationRequested -= OnValidationRequested; - CurrentEditContext.OnFieldChanged -= OnFieldChanged; - } -} +```razor + ``` -A canceled task is discarded silently and does not change the field's faulted state. A task that throws an exception other than places the field in the faulted state described in the next section. - -### Pending and faulted state +Use to display messages for the form: -While an async task is in flight, the field is *pending*. If an async task throws an exception other than , the field is *faulted*. Each state has both a per-field and a form-level query: - -| State | Per-field | Form-level (any field) | -|----------|----------------------------------------------------|----------------------------------| -| Pending | `EditContext.IsValidationPending(fieldIdentifier)` | `EditContext.IsValidationPending()` | -| Faulted | `EditContext.IsValidationFaulted(fieldIdentifier)` | `EditContext.IsValidationFaulted()` | +```razor + +``` -The per-field overloads accept either a or a `() => model.Property` lambda for convenient use in Razor markup: +Assign the summary's `Model` parameter to restrict it to messages associated with a particular model: ```razor - - - -@if (EditContext.IsValidationPending(() => Model.Username)) -{ - Checking… -} -else if (EditContext.IsValidationFaulted(() => Model.Username)) -{ - - Validation could not be completed. - -} + ``` -The form-level parameterless overloads return `true` when any field is currently pending or faulted. A common use is disabling the submit button while validation is in flight: +To inspect current messages in code, call : -```razor - +```csharp +var allMessages = editContext.GetValidationMessages(); +var fieldMessages = editContext.GetValidationMessages( + editContext.Field(nameof(Starship.Identifier))); ``` - automatically adds the `pending` and `faulted` CSS classes to its rendered element while the bound field is in the corresponding state, in addition to the existing `modified` / `valid` / `invalid` classes. The classes compose, so unmodified pending styling and modified pending styling can be targeted independently: +These methods read the current validation state. They don't initiate validation. -```css -.pending { - background-image: url('spinner.gif'); - background-repeat: no-repeat; - background-position: right center; -} +## Customize validation appearance -.modified.pending { - border-color: lightblue; -} +Blazor applies CSS classes that represent field and message state: -.modified.faulted { - border-color: orange; -} -``` +| Element | Classes | +|---|---| +| Input | `valid` or `invalid`, plus `modified` after the user edits the field | +| Validation message | `validation-message` | +| Validation summary | `validation-summary-errors` or `validation-summary-valid` | -### Calling `ValidateAsync` from a custom submit handler +:::moniker range=">= aspnetcore-11.0" - +The Blazor project templates include styles for the common valid and invalid classes. Add styles for other classes as needed. `ValidationMessage` and `ValidationSummary` also accept arbitrary HTML attributes. Supplying a `class` attribute replaces the component's default class. -When a form uses instead of , call `Microsoft.AspNetCore.Components.Forms.EditContext.ValidateAsync` from the handler to await any registered async work before deciding whether to proceed: +:::moniker range=">= aspnetcore-5.0" -```razor - - - - - +To change the classes applied to input components, derive from . -@code { - private EditContext _editContext = default!; +:::moniker-end + +:::moniker range=">= aspnetcore-8.0" - protected override void OnInitialized() => - _editContext = new EditContext(Model); +```csharp +using Microsoft.AspNetCore.Components.Forms; - private async Task HandleSubmitAsync() +public sealed class BootstrapFieldCssClassProvider : FieldCssClassProvider +{ + public override string GetFieldCssClass( + EditContext editContext, + in FieldIdentifier fieldIdentifier) { - if (await _editContext.ValidateAsync(CancellationToken.None)) + if (!editContext.IsModified(fieldIdentifier)) { - await RegisterAsync(); + return string.Empty; } + + return editContext.IsValid(fieldIdentifier) + ? "is-valid" + : "is-invalid"; } } ``` - +:::moniker range=">= aspnetcore-5.0 < aspnetcore-8.0" -The synchronous method continues to work for forms that only have synchronous validators, but it's obsolete as of .NET 11. Call `Microsoft.AspNetCore.Components.Forms.EditContext.ValidateAsync` instead. If a handler attempts to register asynchronous work during a synchronous pass, `AddAsyncValidator` throws an directing the caller to use `ValidateAsync`. +```csharp +using System.Linq; +using Microsoft.AspNetCore.Components.Forms; -### Async validation across rendering modes +public sealed class BootstrapFieldCssClassProvider : FieldCssClassProvider +{ + public override string GetFieldCssClass( + EditContext editContext, + in FieldIdentifier fieldIdentifier) + { + if (!editContext.IsModified(fieldIdentifier)) + { + return string.Empty; + } -The async validation API is the same in every Blazor rendering mode. Validator code runs wherever the component runs: in the browser for Interactive WebAssembly, on the server for Interactive Server, and on the server during the form POST for static SSR. Static SSR renders the full response after async validation completes. + return editContext.GetValidationMessages(fieldIdentifier).Any() + ? "is-invalid" + : "is-valid"; + } +} +``` :::moniker-end -## Validator components +:::moniker range=">= aspnetcore-11.0" -Validator components support form validation by managing a for a form's . +A custom `FieldCssClassProvider` determines the complete class value for each field. If the form uses asynchronous field validation, handle `IsValidationPending(fieldIdentifier)` and `IsValidationFaulted(fieldIdentifier)` in the provider when pending or faulted classes are required. -The Blazor framework provides the component to attach validation support to forms based on [validation attributes (data annotations)](xref:mvc/models/validation#validation-attributes). You can create custom validator components to process validation messages for different forms on the same page or the same form at different steps of form processing (for example, client validation followed by server-side validation in a Blazor Web App). The validator component example shown in this section, `CustomValidation`, is used in the following sections of this article: +:::moniker-end -* [Business logic validation with a validator component](#business-logic-validation-with-a-validator-component) -* [Remote validation with a validator component](#remote-validation-with-a-validator-component) +:::moniker range=">= aspnetcore-5.0" -Of the [data annotation built-in validators](xref:mvc/models/validation#built-in-attributes), only the [`[Remote]` validation attribute](xref:mvc/models/validation#remote-attribute) isn't supported in Blazor. +Assign the provider to the form's `EditContext`: -> [!NOTE] -> Custom data annotation validation attributes can be used instead of custom validator components in many cases. Custom attributes applied to the form's model activate with the use of the component. When used with server-side validation in a Blazor Web App, any custom attributes applied to the model must be executable on the server. For more information, see the [Custom validation attributes](#custom-validation-attributes) section. +```csharp +editContext.SetFieldCssClassProvider( + new BootstrapFieldCssClassProvider()); +``` -Create a validator component from : +For custom input markup, call to obtain the class selected by the current provider. -* The form's is a [cascading parameter](xref:blazor/components/cascading-values-and-parameters) of the component. -* When the validator component is initialized, a new is created to maintain a current list of form errors. -* The message store receives errors when developer code in the form's component calls the `DisplayErrors` method. The errors are passed to the `DisplayErrors` method in a [`Dictionary>`](xref:System.Collections.Generic.Dictionary%602). In the dictionary, the key is the name of the form field that has one or more errors. The value is the error list. -* Messages are cleared when any of the following have occurred: - * Validation is requested on the when the event is raised. All of the errors are cleared. - * A field changes in the form when the event is raised. Only the errors for the field are cleared. - * The `ClearErrors` method is called by developer code. All of the errors are cleared. +:::moniker-end -Update the namespace in the following class to match your app's namespace. + -`CustomValidation.cs`: +## Respond to validation state -:::code language="csharp" source="~/../blazor-samples/8.0/BlazorSample_BlazorWebApp/CustomValidation.cs"::: +`EditContext` exposes the current validation state without initiating validation. -> [!IMPORTANT] -> Specifying a namespace is **required** when deriving from . Failing to specify a namespace results in a build error: -> -> > :::no-loc text="Tag helpers cannot target tag name '\.{CLASS NAME}' because it contains a ' ' character."::: -> -> The `{CLASS NAME}` placeholder is the name of the component class. The custom validator example in this section specifies the example namespace `BlazorSample`. +* Use `IsModified(field)` or `IsModified()` to determine whether a field or any field in the form has changed. +* Use `GetValidationMessages(field)` or `GetValidationMessages()` to inspect current field or form messages. -> [!NOTE] -> Anonymous lambda expressions are registered event handlers for and in the preceding example. It isn't necessary to implement and unsubscribe the event delegates in this scenario. For more information, see . +:::moniker range=">= aspnetcore-8.0" -## Business logic validation with a validator component +Use `IsValid(field)` to determine whether a field currently has validation messages. -For general business logic validation, use a [validator component](#validator-components) that receives form errors in a dictionary. +:::moniker-end -Basic validation is useful in cases where the form's model is defined within the component hosting the form, either as members directly on the component or in a subclass. Use of a validator component is recommended where an independent model class is used across several components. +:::moniker range="< aspnetcore-8.0" -In the following example: +For a field, the absence of messages can be checked with `!editContext.GetValidationMessages(field).Any()`. -* A shortened version of the `Starfleet Starship Database` form (`Starship3` component) of the [Example form](xref:blazor/forms/input-components#example-form) section of the *Input components* article is used that only accepts the starship's classification and description. Data annotation validation isn't triggered on form submission because the component isn't included in the form. -* The `CustomValidation` component from the [Validator components](#validator-components) section of this article is used. -* The validation requires a value for the ship's description (`Description`) if the user selects the "`Defense`" ship classification (`Classification`). +:::moniker-end -When validation messages are set in the component, they're added to the validator's and shown in the 's validation summary. +The following example displays custom UI only after a field is modified and invalid: -`Starship9.razor`: +:::moniker range=">= aspnetcore-8.0" -:::moniker range=">= aspnetcore-9.0" +```razor +@{ + var identifier = editContext.Field(nameof(Starship.Identifier)); +} -:::code language="razor" source="~/../blazor-samples/9.0/BlazorSample_BlazorWebApp/Components/Pages/Starship9.razor"::: +@if (editContext.IsModified(identifier) && + !editContext.IsValid(identifier)) +{ +

Correct the identifier before continuing.

+} +``` :::moniker-end -:::moniker range=">= aspnetcore-8.0 < aspnetcore-9.0" +:::moniker range="< aspnetcore-8.0" + +```razor +@{ + var identifier = editContext.Field(nameof(Starship.Identifier)); +} -:::code language="razor" source="~/../blazor-samples/8.0/BlazorSample_BlazorWebApp/Components/Pages/Starship9.razor"::: +@if (editContext.IsModified(identifier) && + editContext.GetValidationMessages(identifier).Any()) +{ +

Correct the identifier before continuing.

+} +``` :::moniker-end -:::moniker range="< aspnetcore-8.0" +Input components, `ValidationMessage`, and `ValidationSummary` update themselves when validation state changes. A component that renders other conditional validation UI should subscribe to and call `StateHasChanged`: -```razor -@page "/starship-9" -@inject ILogger Logger +```csharp +private void HandleValidationStateChanged( + object? sender, ValidationStateChangedEventArgs e) => + _ = InvokeAsync(StateHasChanged); +``` -

Starfleet Starship Database

+Unsubscribe from `OnValidationStateChanged` when the component is disposed. -

New Ship Entry Form

+:::moniker range=">= aspnetcore-11.0" - - - -
- -
-
- -
-
- -
-
+Use `IsValidationPending(field)` and `IsValidationFaulted(field)` for asynchronous field validation. The parameterless methods describe form-level `ValidateAsync` passes and don't aggregate the state of every field. -@code { - private CustomValidation? customValidation; +These states also include asynchronous work performed by . An `AsyncValidationAttribute` applied to a property uses field state during field validation, including the default `pending` and `faulted` CSS classes. During `ValidateAsync`, asynchronous attributes and contribute to the form-level state reported by the parameterless methods. + +Live pending indicators require an interactive render mode. During a static SSR form post, server-side validation completes before the response is rendered. - public Starship? Model { get; set; } +:::moniker-end - protected override void OnInitialized() => - Model ??= new() { ProductionDate = DateTime.UtcNow }; +## Control form submission - private void Submit() - { - customValidation?.ClearErrors(); +`EditForm` provides three submission callbacks: - var errors = new Dictionary>(); +| Callback | Behavior | +|---|---| +| | Runs after automatic validation succeeds. | +| | Runs after automatic validation fails. | +| | Gives the handler control of validation and submission. | - if (Model!.Classification == "Defense" && - string.IsNullOrEmpty(Model.Description)) - { - errors.Add(nameof(Model.Description), - new() { "For a 'Defense' ship classification, " + - "'Description' is required." }); - } +`OnValidSubmit` and `OnInvalidSubmit` can be used together. Don't combine `OnSubmit` with either of them. - if (errors.Any()) - { - customValidation?.DisplayErrors(errors); - } - else +:::moniker range=">= aspnetcore-11.0" + +`EditForm` uses before invoking `OnValidSubmit` or `OnInvalidSubmit`, so it awaits synchronous and asynchronous validators. When handling `OnSubmit`, call `ValidateAsync` before processing the form: + +```razor + + ... + + +@code { + private async Task HandleSubmit(EditContext editContext) + { + if (await editContext.ValidateAsync()) { - Logger.LogInformation("Submit called: Processing the form"); + await SaveAsync(); } } } ``` - +The synchronous method is obsolete in .NET 11. It doesn't await asynchronous validation and throws if a handler attempts to register asynchronous work. -:::moniker-end - -> [!NOTE] -> As an alternative to using [validation components](#validator-components), data annotation validation attributes can be used. Custom attributes applied to the form's model activate with the use of the component. When used with server-side validation in a Blazor Web App, the attributes must be executable on the server. For more information, see the [Custom validation attributes](#custom-validation-attributes) section. - -:::moniker range=">= aspnetcore-10.0" - -## Remote validation in a Minimal API - -In a [Minimal API](xref:fundamentals/minimal-apis), call the extension method for [data annotation validation of model types](xref:mvc/models/validation#validation-attributes) for all web API endpoints: - -```csharp -builder.Services.AddValidation(); -``` - -The implementation automatically discovers types that are defined in Minimal API handlers or as base types of types defined in Minimal API handlers. An endpoint filter performs validation on these types and is added for each endpoint. - -Built-in validation also supports [custom validation attributes](xref:mvc/models/validation#custom-attributes). - -For more information, see . - -:::moniker-end - -## Remote validation with a validator component - -:::moniker range=">= aspnetcore-10.0" - -*This section demonstrates remote validation using a Blazor Web App (global Interactive Auto render mode) and a Minimal API.* - -Remote validation is supported in addition to Blazor Web App client/server-side validation: - -* Process client validation in the form with the component. -* When the form passes client validation ( is called), send the to a backend Minimal API for remote validation. -* Process remote model validation: - * Data annotations validation with built-in support for Minimal APIs. - * Custom validation logic. -* Send validation errors, if any, back to the client. -* Either disable the form on success or display the errors so that the user can correct any problems with the form's field values. - -Basic validation is useful in cases where the form's model is defined within the component hosting the form, either as members directly on the component or in a subclass. Use of a *validator component* is recommended where an independent model class is used across several components. The approach demonstrated by the following guidance uses a validator component. - -The following example is based on: - -* A Blazor Web App with global Interactive Auto components created from the [Blazor Web App project template](xref:blazor/project-structure). -* A `CustomValidation` component to handle adding model errors to the form's validation message store for display in the UI. -* A [Minimal API](xref:fundamentals/minimal-apis) project that validates: - * Data annotations validation attributes on the model class (), including for [custom validation attributes](xref:mvc/models/validation#custom-attributes). - * Custom validation logic that determines if a description form field (`Description`) has a value if the user selects a particular classification in another form field (`Defense` classification). - -The validation for the `Defense` ship classification only occurs on the server because the upcoming form doesn't perform the same validation client-side when the form is submitted to the server. Remote validation without client validation is common in apps that require private business logic validation of user input on the server. For example, private information from data stored for a user might be required to validate user input. Private data is never sent to the client for client validation. - -> [!NOTE] -> For more information on security pertaining to the following example, see the following resources: -> -> * -> * (and the other articles in the Blazor *Security and Identity* node) -> * [Microsoft identity platform documentation](/entra/identity-platform/) - -Create a `Starship` folder in the `.Client` project of the Blazor Web App. - -Place the following `StarshipModel` model (`StarshipModel.cs`) into the `Starship` folder ***and*** into the Minimal API project of the solution. - -> [!NOTE] -> If you choose to place one copy of the `StarshipModel` into a shared class library project for use by both the Blazor Web App and the Minimal API project, confirm that the shared class library uses the shared framework or add the [`System.ComponentModel.Annotations` package](https://www.nuget.org/packages/System.ComponentModel.Annotations) to the shared project. This ensures that the model has access to data annotations. -> -> [!INCLUDE[](~/includes/package-reference.md)] - -In the two `StarshipModel` classes, set the namespace (`{NAMESPACE}`) appropriately for each project (for example, `BlazorSample.Client.Starship` in the Blazor Web App and `MinimalApiJwt.Models` in the Minimal API project). Some developers prefer to use a different folder scheme. If you position the classes in the projects in different locations, set the namespaces appropriately. - -`Starship/StarshipModel.cs` (Blazor Web App) or `Models/StarshipModel.cs` (Minimal API project): - -```csharp -using System.ComponentModel.DataAnnotations; - -namespace {NAMESPACE}; - -public class StarshipModel -{ - [Required] - [StringLength(16, ErrorMessage = "Identifier too long (16 character limit).")] - public string? Id { get; set; } - - public string? Description { get; set; } - - [Required] - public string? Classification { get; set; } - - [Range(1, 100000, ErrorMessage = "Accommodation invalid (1-100000).")] - public int MaximumAccommodation { get; set; } - - [Required] - [Range(typeof(bool), "true", "true", ErrorMessage = "Approval required.")] - public bool IsValidatedDesign { get; set; } - - [Required] - public DateTime ProductionDate { get; set; } -} -``` - -Add an interface for a form validation service to the `.Client` project in the `Starship` folder. The interface is used to register validation services in the Blazor Web App. - -`Starship/IFormValidation.cs`: - -```csharp -namespace BlazorSample.Client.Starship; - -public interface IFormValidation -{ - Task> ValidateStarshipFormAsync( - StarshipModel starship); -} -``` - -Add a client form validator class to the `.Client` project's `Starship` folder. The client form validator is used when the app is running on the client. The validator class posts to the Blazor Web App endpoint, which then proxies to the Minimal API. - -`Starship/ClientFormValidation.cs`: - -```csharp -using System.Net.Http.Json; - -namespace BlazorSample.Client.Starship; - -internal sealed class ClientFormValidation(HttpClient httpClient) : IFormValidation -{ - public async Task> ValidateStarshipFormAsync( - StarshipModel starship) - { - Dictionary genericError = new() - { - { - "Validation Error", - ["An unexpected client error occurred during validation."] - } - }; - - try - { - using var response = await httpClient.PostAsJsonAsync( - "/starship-validation", starship); - - if (response.IsSuccessStatusCode) - { - var deserializedResponseContent = - await response.Content.ReadFromJsonAsync - >(); - - return deserializedResponseContent ?? genericError; - } - } - catch (Exception ex) - { - // Log exception - } - - return genericError; - } -} -``` - -Confirm or update the namespace of the preceding class. - -Create a `Starship` folder in the server project of the Blazor Web App. - -In the Blazor Web App, create a server form validator that implements the `IFormValidation` interface. Place the server form validator class in the server-side `Starship` folder. The server form validator is used when the Blazor Web App is running on the server. The validator class posts the form's model to the backend Minimal API for processing. - -`Starship/ServerFormValidation.cs`: - -```csharp -using System.Net; -using System.Net.Http.Headers; -using System.Text.Json; -using Microsoft.AspNetCore.Authentication; -using Microsoft.AspNetCore.Mvc; -using BlazorSample.Client.Starship; - -namespace BlazorSample.Starship; - -internal sealed class ServerFormValidation( - IHttpContextAccessor httpContextAccessor, IHttpClientFactory httpClientFactory) - : IFormValidation -{ - public async Task> ValidateStarshipFormAsync( - StarshipModel starship) - { - Dictionary genericError = new() - { - { - "Validation Error", - ["An unexpected server error occurred during validation."] - } - }; - - try - { - if (httpContextAccessor.HttpContext is null) - { - throw new Exception("HttpContext not available"); - } - - var request = new HttpRequestMessage(HttpMethod.Post, - "https://localhost:7277/api-starship-validation") - { - Content = new StringContent(JsonSerializer.Serialize(starship), - System.Text.Encoding.UTF8, "application/json") - }; - - var accessToken = - await httpContextAccessor.HttpContext.GetTokenAsync("access_token"); - - request.Headers.Authorization = - new AuthenticationHeaderValue("Bearer", accessToken); - - using var httpClient = httpClientFactory.CreateClient(); - - var response = await httpClient.SendAsync(request); - - if (response?.StatusCode == HttpStatusCode.NoContent) - { - return new Dictionary(); - } - - if (response?.StatusCode == HttpStatusCode.BadRequest) - { - var content = await response.Content.ReadAsStringAsync(); - - var deserialized = - JsonSerializer.Deserialize( - content, - new JsonSerializerOptions(JsonSerializerDefaults.Web)); - - return deserialized?.Errors ?? genericError; - } - - return genericError; - } - catch (Exception ex) - { - // Log exception - } - - return genericError; - } -} -``` - -In the `Program` file of the Blazor Web App: - -* Register the server form validator (`ServerFormValidation`) for the `IFormValidation` interface in the DI container. -* The server form validator is used on the server to call `ValidateStarshipFormAsync` for form validation. - -```csharp -builder.Services.AddScoped(); - -... - -app.MapPost("/starship-validation", (IFormValidation formValidator, - StarshipModel model) => -{ - return formValidator.ValidateStarshipFormAsync(model); -}).RequireAuthorization(); -``` - -The `.Client` project of a Blazor Web App must register an for HTTP POST requests to the Minimal API. Add the following to the `.Client` project's `Program` file: - -```csharp -builder.Services.AddHttpClient(httpClient => -{ - httpClient.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress); -}); -``` - -The preceding example sets the base address with `builder.HostEnvironment.BaseAddress` (), which gets the base address for the app and is typically derived from the `` tag's `href` value in the host page. - -In the `Program` file of the `MinimalApiJwt` project, add the following starship form validation endpoint. The endpoint validates that the model's `Description` property has a value when the model's `Classification` property is `Defense`. If validation fails, a `ValidationProblem` returns a dictionary with the failed field and a description of the error. If validation passes, a *204 - No Content* response is issued. In a typical production app, any number of custom form model checks are made, and the validation errors dictionary can include multiple failures (`string[]` value) for each model property. - -In the `Program` file of the Minimal API project: - -```csharp -app.MapPost("/api-starship-validation", ( - StarshipModel model, ILogger logger) => -{ - Dictionary errors = []; - - if (model.Classification == "Defense" && string.IsNullOrEmpty(model.Description)) - { - errors.Add(nameof(model.Description), - ["For a 'Defense' ship, 'Description' is required."]); - } - - if (errors.Count > 0) - { - return Results.ValidationProblem( - errors: errors, - detail: "One or more validation errors occurred.", - instance: typeof(Program).Assembly.GetName().Name, - title: "Validation Errors", - type: "https://tools.ietf.org/html/rfc9110#section-15.5.1"); - } - - return Results.NoContent(); - -}).RequireAuthorization(); -``` - -Also in the `Program` file of the Minimal API, register [built-in validation services](xref:fundamentals/minimal-apis#validation-support-in-minimal-apis): - -```csharp -builder.Services.AddValidation(); -``` - -Built-in validation automatically intercepts the endpoint request and validates the types that the endpoint receives. If the model fails validation, the framework returns a *400 - Bad Request* response with error details without executing the endpoint's code. If you don't want to implement built-in model validation, don't use the preceding line of code in the Minimal API's `Program` file. - -In the `.Client` project, add the following `CustomValidation` component. When the component's `DisplayErrors` method is called with a set of validation errors, the errors are added to the parent component's edit context validation message store. Errors are cleared from the edit context by calling the `ClearErrors` method. - -`CustomValidation.cs`: - -```csharp -using Microsoft.AspNetCore.Components; -using Microsoft.AspNetCore.Components.Forms; -using Microsoft.AspNetCore.Mvc; - -namespace BlazorSample.Client; - -public class CustomValidation : ComponentBase -{ - private ValidationMessageStore? messageStore; - - [CascadingParameter] - private EditContext? CurrentEditContext { get; set; } - - protected override void OnInitialized() - { - if (CurrentEditContext is null) - { - throw new InvalidOperationException( - $"{nameof(CustomValidation)} requires a cascading " + - $"parameter of type {nameof(EditContext)}. " + - $"For example, you can use {nameof(CustomValidation)} " + - $"inside an {nameof(EditForm)}."); - } - - messageStore = new(CurrentEditContext); - - CurrentEditContext.OnValidationRequested += (s, e) => - messageStore?.Clear(); - CurrentEditContext.OnFieldChanged += (s, e) => - messageStore?.Clear(e.FieldIdentifier); - } - - public void DisplayErrors(IDictionary errors) - { - if (CurrentEditContext is not null) - { - foreach (var err in errors) - { - messageStore?.Add(CurrentEditContext.Field(err.Key), err.Value); - } - - CurrentEditContext.NotifyValidationStateChanged(); - } - } - - public void ClearErrors() - { - messageStore?.Clear(); - CurrentEditContext?.NotifyValidationStateChanged(); - } -} -``` - -In the `.Client` project, the `Starfleet Starship Database` form is updated to show validation errors with help of the `CustomValidation` component. When validation messages are returned, they're added to the `CustomValidation` component's . The errors are available in the form's for display by the form's validation summary. Confirm or update the namespace for `BlazorSample.Client.Starship`. - -Note that the form requires authorization, so the user must be signed into the app to navigate to the form. - -> [!NOTE] -> Forms based on automatically enable [antiforgery support](xref:blazor/forms/index#antiforgery-support). - -`Pages/Starship10.razor` in the `.Client` project: - -```razor -@page "/starship-10" -@using Microsoft.AspNetCore.Authorization -@using Microsoft.AspNetCore.Components.WebAssembly.Authentication -@using BlazorSample.Client.Starship -@attribute [Authorize] -@inject IFormValidation FormValidation -@inject ILogger Logger - -

Starfleet Starship Database

- -

New Ship Entry Form

- - - - - -
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- @message -
-
- -@code { - private CustomValidation? customValidation; - private bool disabled; - private string? message; - private string messageStyles = "visibility:hidden"; - - [SupplyParameterFromForm] - private StarshipModel? Model { get; set; } - - protected override void OnInitialized() => - Model ??= new() { ProductionDate = DateTime.UtcNow }; - - private async Task Submit(EditContext editContext) - { - customValidation?.ClearErrors(); - - try - { - var validationProblemDetails = - await FormValidation.ValidateStarshipFormAsync( - (StarshipModel)editContext.Model); - - if (validationProblemDetails?.Count > 0) - { - customValidation?.DisplayErrors(validationProblemDetails); - } - else - { - disabled = true; - messageStyles = "color:green"; - message = "The form has been processed."; - } - } - catch (AccessTokenNotAvailableException ex) - { - ex.Redirect(); - } - catch (Exception ex) - { - Logger.LogError(ex, "Form processing error."); - disabled = true; - messageStyles = "color:red"; - message = "There was an error processing the form."; - } - } -} -``` - -> [!NOTE] -> As an alternative to the use of a [validation component](#validator-components), custom data annotation validation attributes can be used. Custom attributes applied to the form's model activate with the use of the component. For more information, see the [Custom validation attributes](#custom-validation-attributes) section. - -To reach the form easily, add the following entry to the `NavMenu` component (`Layout/NavMenu.razor`) in the `.Client` project: - -```razor - -``` - -When automatic model binding validation fails on the server, the framework returns a [default bad request response](xref:web-api/index#default-badrequest-response) with a . The response contains more data than just the validation errors, as shown in the following example when all of the fields of the `Starfleet Starship Database` form aren't submitted and the form fails validation: - -```json -{ - "type": "https://tools.ietf.org/html/rfc9110#section-15.5.1", - "title": "One or more validation errors occurred.", - "status": 400, - "errors": { - "Id": ["The Id field is required."], - "Classification": ["The Classification field is required."], - "IsValidatedDesign": ["This form disallows unapproved ships."], - "MaximumAccommodation": ["Accommodation invalid (1-100000)."] - } -} -``` - -> [!NOTE] -> To demonstrate the preceding JSON responses, you must either disable the form's client validation to permit empty field form submission or use a tool to send a request directly to the Minimal API, such as [Firefox Browser Developer](https://www.mozilla.org/firefox/developer/). - -If automatic type validation passes but the custom validation fails, the following JSON response is received from the Minimal API: - -```json -{ - "type": "https://tools.ietf.org/html/rfc9110#section-15.5.1", - "title": "One or more validation errors occurred.", - "instance": "MinimalApiJwt", - "status": 400, - "errors": { - "Description": ["For a 'Defense' ship, 'Description' is required."] - } -} -``` - -:::moniker-end - -:::moniker range=">= aspnetcore-8.0 < aspnetcore-10.0" - -*This section is focused on Blazor Web App scenarios, but the approach for any type of app that uses server-side validation with web API adopts the same general approach.* - -Remote validation is supported in addition to Blazor Web App client-side and server-side validation: - -* Process client validation in the form with the component. -* When the form passes client validation ( is called), send the to a backend server API for form processing. -* Process model validation on the server. -* The server API includes both the built-in framework data annotations validation and custom validation logic supplied by the developer. If validation passes on the server, process the form and send back a success status code ([`200 - OK`](https://developer.mozilla.org/docs/Web/HTTP/Status/200)). If validation fails, return a failure status code ([`400 - Bad Request`](https://developer.mozilla.org/docs/Web/HTTP/Status/400)) and the field validation errors. -* Either disable the form on success or display the errors. - -Basic validation is useful in cases where the form's model is defined within the component hosting the form, either as members directly on the component or in a subclass. Use of a validator component is recommended where an independent model class is used across several components. - -The following example is based on: - -* A Blazor Web App with Interactive WebAssembly components created from the [Blazor Web App project template](xref:blazor/project-structure). -* The `Starship` model (`Starship.cs`) of the [Example form](xref:blazor/forms/input-components#example-form) section of the *Input components* article. -* The `CustomValidation` component shown in the [Validator components](#validator-components) section. - -Place the `Starship` model (`Starship.cs`) into a shared class library project so that both the client and server projects can use the model. Add or update the namespace to match the namespace of the shared app (for example, `namespace BlazorSample.Shared`). Since the model requires data annotations, confirm that the shared class library uses the shared framework or add the [`System.ComponentModel.Annotations` package](https://www.nuget.org/packages/System.ComponentModel.Annotations) to the shared project. - -[!INCLUDE[](~/includes/package-reference.md)] - -In the main project of the Blazor Web App, add a controller to process starship validation requests and return failed validation messages. Update the namespaces in the last `using` statement for the shared class library project and the `namespace` for the controller class. In addition to client and server data annotations validation, the controller validates that a value is provided for the ship's description (`Description`) if the user selects the `Defense` ship classification (`Classification`). - -The validation for the `Defense` ship classification only occurs on the server in the controller because the upcoming form doesn't perform the same validation client-side when the form is submitted to the server. Remote validation is common in apps that require private business logic validation of user input. For example, private information from data stored for a user might be required to validate user input. Private data obviously can't be sent to the client for client validation. - -> [!NOTE] -> The `StarshipValidation` controller in this section uses Microsoft Identity 2.0. The Web API only accepts tokens for users that have the "`API.Access`" scope for this API. Additional customization is required if the API's scope name is different from `API.Access`. -> -> For more information on security, see: -> -> * (and the other articles in the Blazor *Security and Identity* node) -> * [Microsoft identity platform documentation](/entra/identity-platform/) - -`Controllers/StarshipValidation.cs`: - -```csharp -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; -using BlazorSample.Shared; - -namespace BlazorSample.Server.Controllers; - -[Authorize] -[ApiController] -[Route("[controller]")] -public class StarshipValidationController( - ILogger logger) - : ControllerBase -{ - static readonly string[] scopeRequiredByApi = [ "API.Access" ]; - - [HttpPost] - public async Task Post(Starship model) - { - HttpContext.VerifyUserHasAnyAcceptedScope(scopeRequiredByApi); - - try - { - if (model.Classification == "Defense" && - string.IsNullOrEmpty(model.Description)) - { - ModelState.AddModelError(nameof(model.Description), - "For a 'Defense' ship " + - "classification, 'Description' is required."); - } - else - { - logger.LogInformation("Processing the form asynchronously"); - - // async ... - - return Ok(ModelState); - } - } - catch (Exception ex) - { - logger.LogError("Validation Error: {Message}", ex.Message); - } - - return BadRequest(ModelState); - } -} -``` - -Confirm or update the namespace of the preceding controller (`BlazorSample.Server.Controllers`) to match the app's controllers' namespace. - -When a model binding validation error occurs on the server, an [`ApiController`](xref:web-api/index) () normally returns a [default bad request response](xref:web-api/index#default-badrequest-response) with a . The response contains more data than just the validation errors, as shown in the following example when all of the fields of the `Starfleet Starship Database` form aren't submitted and the form fails validation: - -```json -{ - "title": "One or more validation errors occurred.", - "status": 400, - "errors": { - "Id": [ "The Id field is required." ], - "Classification": [ "The Classification field is required." ], - "IsValidatedDesign": [ "This form disallows unapproved ships." ], - "MaximumAccommodation": [ "Accommodation invalid (1-100000)." ] - } -} -``` - -> [!NOTE] -> To demonstrate the preceding JSON response, you must either disable the form's client validation to permit empty field form submission or use a tool to send a request directly to the server API, such as [Firefox Browser Developer](https://www.mozilla.org/firefox/developer/). - -If the server API returns the preceding default JSON response, it's possible for the client to parse the response in developer code to obtain the children of the `errors` node for forms validation error processing. It's inconvenient to write developer code to parse the file. Parsing the JSON manually requires producing a [`Dictionary>`](xref:System.Collections.Generic.Dictionary%602) of errors after calling . Ideally, the server API should only return the validation errors, as the following example shows: - -```json -{ - "Id": [ "The Id field is required." ], - "Classification": [ "The Classification field is required." ], - "IsValidatedDesign": [ "This form disallows unapproved ships." ], - "MaximumAccommodation": [ "Accommodation invalid (1-100000)." ] -} -``` - -To modify the server API's response to make it only return the validation errors, change the delegate that's invoked on actions that are annotated with in the `Program` file. For the API endpoint (`/StarshipValidation`), return a with the . For any other API endpoints, preserve the default behavior by returning the object result with a new . - -Add the namespace to the top of the `Program` file in the main project of the Blazor Web App: - -```csharp -using Microsoft.AspNetCore.Mvc; -``` - -In the `Program` file, add or update the following extension method and add the following call to : - -```csharp -builder.Services.AddControllersWithViews() - .ConfigureApiBehaviorOptions(options => - { - options.InvalidModelStateResponseFactory = context => - { - if (context.HttpContext.Request.Path == "/StarshipValidation") - { - return new BadRequestObjectResult(context.ModelState); - } - else - { - return new BadRequestObjectResult( - new ValidationProblemDetails(context.ModelState)); - } - }; - }); -``` - -If you're adding controllers to the main project of the Blazor Web App for the first time, map controller endpoints when you place the preceding code that registers services for controllers. The following example uses default controller routes: - -```csharp -app.MapDefaultControllerRoute(); -``` - -> [!NOTE] -> The preceding example explicitly registers controller services by calling to automatically [mitigate Cross-Site Request Forgery (XSRF/CSRF) attacks](xref:security/anti-request-forgery). If you merely use , antiforgery isn't enabled automatically. - -For more information on controller routing and validation failure error responses, see the following resources: - -* -* - -In the `.Client` project, add the `CustomValidation` component shown in the [Validator components](#validator-components) section. Update the namespace to match the app (for example, `namespace BlazorSample.Client`). - -In the `.Client` project, the `Starfleet Starship Database` form is updated to show validation errors with help of the `CustomValidation` component. When validation messages are returned, they're added to the `CustomValidation` component's . The errors are available in the form's for display by the form's validation summary. - -In the following component, update the namespace of the shared project (`@using BlazorSample.Shared`) to the shared project's namespace. Note that the form requires authorization, so the user must be signed into the app to navigate to the form. - -`Starship10.razor`: - -> [!NOTE] -> Forms based on automatically enable [antiforgery support](xref:blazor/forms/index#antiforgery-support). The controller should use to register controller services and automatically enable antiforgery support for the web API. - -```razor -@page "/starship-10" -@rendermode InteractiveWebAssembly -@using System.Net -@using System.Net.Http.Json -@using Microsoft.AspNetCore.Authorization -@using Microsoft.AspNetCore.Components.WebAssembly.Authentication -@using BlazorSample.Shared -@attribute [Authorize] -@inject HttpClient Http -@inject ILogger Logger - -

Starfleet Starship Database

- -

New Ship Entry Form

- - - - - -
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- @message -
-
- -@code { - private CustomValidation? customValidation; - private bool disabled; - private string? message; - private string messageStyles = "visibility:hidden"; - - [SupplyParameterFromForm] - private Starship? Model { get; set; } - - protected override void OnInitialized() => - Model ??= new() { ProductionDate = DateTime.UtcNow }; - - private async Task Submit(EditContext editContext) - { - customValidation?.ClearErrors(); - - try - { - using var response = await Http.PostAsJsonAsync( - "StarshipValidation", (Starship)editContext.Model); - - var errors = await response.Content - .ReadFromJsonAsync>>() ?? - new Dictionary>(); - - if (response.StatusCode == HttpStatusCode.BadRequest && - errors.Any()) - { - customValidation?.DisplayErrors(errors); - } - else if (!response.IsSuccessStatusCode) - { - throw new HttpRequestException( - $"Validation failed. Status Code: {response.StatusCode}"); - } - else - { - disabled = true; - messageStyles = "color:green"; - message = "The form has been processed."; - } - } - catch (AccessTokenNotAvailableException ex) - { - ex.Redirect(); - } - catch (Exception ex) - { - Logger.LogError("Form processing error: {Message}", ex.Message); - disabled = true; - messageStyles = "color:red"; - message = "There was an error processing the form."; - } - } -} -``` - -The `.Client` project of a Blazor Web App must also register an for HTTP POST requests to a backend web API controller. Confirm or add the following to the `.Client` project's `Program` file: - -```csharp -builder.Services.AddScoped(sp => - new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) }); -``` - -The preceding example sets the base address with `builder.HostEnvironment.BaseAddress` (), which gets the base address for the app and is typically derived from the `` tag's `href` value in the host page. - -> [!NOTE] -> As an alternative to the use of a [validation component](#validator-components), custom data annotation validation attributes can be used. Custom attributes applied to the form's model activate with the use of the component. For more information, see the [Custom validation attributes](#custom-validation-attributes) section. - -:::moniker-end - -:::moniker range="< aspnetcore-8.0" - -*This section is focused on hosted Blazor WebAssembly scenarios, but the approach for any type of app that uses server-side validation with web API adopts the same general approach.* - -Remote validation is supported in addition to server-side validation in a hosted Blazor WebAssembly app: - -* Process client validation in the form with the component. -* When the form passes client validation ( is called), send the to a backend server API for form processing. -* Process model validation on the server. -* The server API includes both the built-in framework data annotations validation and custom validation logic supplied by the developer. If validation passes on the server, process the form and send back a success status code ([`200 - OK`](https://developer.mozilla.org/docs/Web/HTTP/Status/200)). If validation fails, return a failure status code ([`400 - Bad Request`](https://developer.mozilla.org/docs/Web/HTTP/Status/400)) and the field validation errors. -* Either disable the form on success or display the errors. - -Basic validation is useful in cases where the form's model is defined within the component hosting the form, either as members directly on the component or in a subclass. Use of a validator component is recommended where an independent model class is used across several components. - -The following example is based on: - -* A hosted Blazor WebAssembly [solution](xref:blazor/tooling#visual-studio-solution-file-sln) created from the [Blazor WebAssembly project template](xref:blazor/project-structure). The approach is supported for any of the secure hosted Blazor solutions described in the [hosted Blazor WebAssembly security documentation](xref:blazor/security/webassembly/index#implementation-guidance). -* The `Starship` model (`Starship.cs`) of the [Example form](xref:blazor/forms/input-components#example-form) section of the *Input components* article. -* The `CustomValidation` component shown in the [Validator components](#validator-components) section. - -Place the `Starship` model (`Starship.cs`) into the solution's **`Shared`** project so that both the client and server apps can use the model. Add or update the namespace to match the namespace of the shared app (for example, `namespace BlazorSample.Shared`). Since the model requires data annotations, add the [`System.ComponentModel.Annotations` package](https://www.nuget.org/packages/System.ComponentModel.Annotations) to the **`Shared`** project. - -[!INCLUDE[](~/includes/package-reference.md)] - -In the **:::no-loc text="Server":::** project, add a controller to process starship validation requests and return failed validation messages. Update the namespaces in the last `using` statement for the **`Shared`** project and the `namespace` for the controller class. In addition to client and server data annotations validation, the controller validates that a value is provided for the ship's description (`Description`) if the user selects the `Defense` ship classification (`Classification`). - -The validation for the `Defense` ship classification only occurs on the server in the controller because the upcoming form doesn't perform the same validation client-side when the form is submitted to the server. Remote validation is common in apps that require private business logic validation of user input on the server. For example, private information from data stored for a user might be required to validate user input. Private data obviously can't be sent to the client for client validation. - -> [!NOTE] -> The `StarshipValidation` controller in this section uses Microsoft Identity 2.0. The Web API only accepts tokens for users that have the "`API.Access`" scope for this API. Additional customization is required if the API's scope name is different from `API.Access`. -> -> For more information on security, see: -> -> * (and the other articles in the Blazor *Security and Identity* node) -> * [Microsoft identity platform documentation](/entra/identity-platform/) - -`Controllers/StarshipValidation.cs`: - -```csharp -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; -using BlazorSample.Shared; - -namespace BlazorSample.Server.Controllers; - -[Authorize] -[ApiController] -[Route("[controller]")] -public class StarshipValidationController( - ILogger logger) - : ControllerBase -{ - static readonly string[] scopeRequiredByApi = new[] { "API.Access" }; - - [HttpPost] - public async Task Post(Starship model) - { - HttpContext.VerifyUserHasAnyAcceptedScope(scopeRequiredByApi); - - try - { - if (model.Classification == "Defense" && - string.IsNullOrEmpty(model.Description)) - { - ModelState.AddModelError(nameof(model.Description), - "For a 'Defense' ship " + - "classification, 'Description' is required."); - } - else - { - logger.LogInformation("Processing the form asynchronously"); - - // async ... - - return Ok(ModelState); - } - } - catch (Exception ex) - { - logger.LogError("Validation Error: {Message}", ex.Message); - } - - return BadRequest(ModelState); - } -} -``` - -Confirm or update the namespace of the preceding controller (`BlazorSample.Server.Controllers`) to match the app's controllers' namespace. - -When a model binding validation error occurs on the server, an [`ApiController`](xref:web-api/index) () normally returns a [default bad request response](xref:web-api/index#default-badrequest-response) with a . The response contains more data than just the validation errors, as shown in the following example when all of the fields of the `Starfleet Starship Database` form aren't submitted and the form fails validation: - -```json -{ - "title": "One or more validation errors occurred.", - "status": 400, - "errors": { - "Id": [ "The Id field is required." ], - "Classification": [ "The Classification field is required." ], - "IsValidatedDesign": [ "This form disallows unapproved ships." ], - "MaximumAccommodation": [ "Accommodation invalid (1-100000)." ] - } -} -``` - -> [!NOTE] -> To demonstrate the preceding JSON response, you must either disable the form's client validation to permit empty field form submission or use a tool to send a request directly to the server API, such as [Firefox Browser Developer](https://www.mozilla.org/firefox/developer/). - -If the server API returns the preceding default JSON response, it's possible for the client to parse the response in developer code to obtain the children of the `errors` node for forms validation error processing. It's inconvenient to write developer code to parse the file. Parsing the JSON manually requires producing a [`Dictionary>`](xref:System.Collections.Generic.Dictionary%602) of errors after calling . Ideally, the server API should only return the validation errors, as the following example shows: - -```json -{ - "Id": [ "The Id field is required." ], - "Classification": [ "The Classification field is required." ], - "IsValidatedDesign": [ "This form disallows unapproved ships." ], - "MaximumAccommodation": [ "Accommodation invalid (1-100000)." ] -} -``` - -To modify the server API's response to make it only return the validation errors, change the delegate that's invoked on actions that are annotated with in the `Program` file. For the API endpoint (`/StarshipValidation`), return a with the . For any other API endpoints, preserve the default behavior by returning the object result with a new . - -Add the namespace to the top of the `Program` file in the **:::no-loc text="Server":::** app: - -```csharp -using Microsoft.AspNetCore.Mvc; -``` - -In the `Program` file, locate the extension method and add the following call to : - -```csharp -builder.Services.AddControllersWithViews() - .ConfigureApiBehaviorOptions(options => - { - options.InvalidModelStateResponseFactory = context => - { - if (context.HttpContext.Request.Path == "/StarshipValidation") - { - return new BadRequestObjectResult(context.ModelState); - } - else - { - return new BadRequestObjectResult( - new ValidationProblemDetails(context.ModelState)); - } - }; - }); -``` - -> [!NOTE] -> The preceding example explicitly registers controller services by calling to automatically [mitigate Cross-Site Request Forgery (XSRF/CSRF) attacks](xref:security/anti-request-forgery). If you merely use , antiforgery isn't enabled automatically. - -In the **:::no-loc text="Client":::** project, add the `CustomValidation` component shown in the [Validator components](#validator-components) section. Update the namespace to match the app (for example, `namespace BlazorSample.Client`). - -In the **:::no-loc text="Client":::** project, the `Starfleet Starship Database` form is updated to show validation errors with help of the `CustomValidation` component. When validation messages are returned, they're added to the `CustomValidation` component's . The errors are available in the form's for display by the form's validation summary. - -In the following component, update the namespace of the **`Shared`** project (`@using BlazorSample.Shared`) to the shared project's namespace. Note that the form requires authorization, so the user must be signed into the app to navigate to the form. - -`Starship10.razor`: - -```razor -@page "/starship-10" -@using System.Net -@using System.Net.Http.Json -@using Microsoft.AspNetCore.Authorization -@using Microsoft.AspNetCore.Components.WebAssembly.Authentication -@using BlazorSample.Shared -@attribute [Authorize] -@inject HttpClient Http -@inject ILogger Logger - -

Starfleet Starship Database

- -

New Ship Entry Form

- - - - - -
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- @message -
-
- -@code { - private CustomValidation? customValidation; - private bool disabled; - private string? message; - private string messageStyles = "visibility:hidden"; - - public Starship? Model { get; set; } - - protected override void OnInitialized() => - Model ??= new() { ProductionDate = DateTime.UtcNow }; - - private async Task Submit(EditContext editContext) - { - customValidation?.ClearErrors(); - - try - { - using var response = await Http.PostAsJsonAsync( - "StarshipValidation", (Starship)editContext.Model); - - var errors = await response.Content - .ReadFromJsonAsync>>() ?? - new Dictionary>(); - - if (response.StatusCode == HttpStatusCode.BadRequest && - errors.Any()) - { - customValidation?.DisplayErrors(errors); - } - else if (!response.IsSuccessStatusCode) - { - throw new HttpRequestException( - $"Validation failed. Status Code: {response.StatusCode}"); - } - else - { - disabled = true; - messageStyles = "color:green"; - message = "The form has been processed."; - } - } - catch (AccessTokenNotAvailableException ex) - { - ex.Redirect(); - } - catch (Exception ex) - { - Logger.LogError("Form processing error: {Message}", ex.Message); - disabled = true; - messageStyles = "color:red"; - message = "There was an error processing the form."; - } - } -} -``` - -> [!NOTE] -> As an alternative to the use of a [validation component](#validator-components), custom data annotation validation attributes can be used. Custom attributes applied to the form's model activate with the use of the component. For more information, see the [Custom validation attributes](#custom-validation-attributes) section. - -> [!NOTE] -> The remote validation approach in this section is suitable for any of the hosted Blazor WebAssembly solution examples in this documentation set: -> -> * [Microsoft Entra ID (ME-ID)](xref:blazor/security/webassembly/hosted-with-microsoft-entra-id) -> * [Azure Active Directory (AAD) B2C](xref:blazor/security/webassembly/hosted-with-azure-active-directory-b2c) -> * [Identity Server](xref:blazor/security/webassembly/hosted-with-identity-server) - -:::moniker-end - -## `InputText` based on the input event - -Use the component to create a custom component that uses the `oninput` event ([`input`](https://developer.mozilla.org/docs/Web/API/HTMLElement/input_event)) instead of the `onchange` event ([`change`](https://developer.mozilla.org/docs/Web/API/HTMLElement/change_event)). Use of the `input` event triggers field validation on each keystroke. - -The following `CustomInputText` component inherits the framework's `InputText` component and sets event binding to the `oninput` event ([`input`](https://developer.mozilla.org/docs/Web/API/HTMLElement/input_event)). - -`CustomInputText.razor`: - -:::code language="razor" source="~/../blazor-samples/8.0/BlazorSample_BlazorWebApp/Components/CustomInputText.razor"::: - -The `CustomInputText` component can be used anywhere is used. The following component uses the shared `CustomInputText` component. - -`Starship11.razor`: - -:::moniker range=">= aspnetcore-9.0" - -:::code language="razor" source="~/../blazor-samples/9.0/BlazorSample_BlazorWebApp/Components/Pages/Starship11.razor"::: - -:::moniker-end - -:::moniker range=">= aspnetcore-8.0 < aspnetcore-9.0" - -:::code language="razor" source="~/../blazor-samples/8.0/BlazorSample_BlazorWebApp/Components/Pages/Starship11.razor"::: - -:::moniker-end - -:::moniker range="< aspnetcore-8.0" - -```razor -@page "/starship-11" -@using System.ComponentModel.DataAnnotations -@inject ILogger Logger - - - - - - - - -
- CurrentValue: @Model?.Id -
- -@code { - public Starship? Model { get; set; } - - protected override void OnInitialized() => Model ??= new(); - - private void Submit() - { - Logger.LogInformation("Submit called: Processing the form"); - } - - public class Starship - { - [Required] - [StringLength(10, ErrorMessage = "Id is too long.")] - public string? Id { get; set; } - } -} -``` - - - -:::moniker-end - -## Validation Summary and Validation Message components - -The component summarizes all validation messages, which is similar to the [Validation Summary Tag Helper](xref:mvc/views/working-with-forms#the-validation-summary-tag-helper): - -```razor - -``` - -Output validation messages for a specific model with the `Model` parameter: - -```razor - -``` - -The component displays validation messages for a specific field, which is similar to the [Validation Message Tag Helper](xref:mvc/views/working-with-forms#the-validation-message-tag-helper). Specify the field for validation with the attribute and a lambda expression naming the model property: - -```razor - -``` - -The and components support arbitrary attributes. Any attribute that doesn't match a component parameter is added to the generated `
` or `
    ` element. If a class attribute is supplied, its value replaces the component's default CSS class. - -Control the style of validation messages in the app's stylesheet (`wwwroot/css/app.css` or `wwwroot/css/site.css`). The default `validation-message` class sets the text color of validation messages to red: - -```css -.validation-message { - color: red; -} -``` - -:::moniker range=">= aspnetcore-8.0" - -## Determine if a form field is valid - -Use to determine if a field is valid without obtaining validation messages. - - Supported, but not recommended: - -```csharp -var isValid = !editContext.GetValidationMessages(fieldIdentifier).Any(); -``` - - Recommended: - -```csharp -var isValid = editContext.IsValid(fieldIdentifier); -``` - -:::moniker-end - -## Custom validation attributes - -To ensure that a validation result is correctly associated with a field when using a [custom validation attribute](xref:mvc/models/validation#custom-attributes), pass the validation context's when creating the . - -`CustomValidator.cs`: - -:::moniker range=">= aspnetcore-8.0" - -```csharp -using System; -using System.ComponentModel.DataAnnotations; - -public class CustomValidator : ValidationAttribute -{ - protected override ValidationResult IsValid(object? value, - ValidationContext validationContext) - { - ... - - return new ValidationResult("Validation message to user.", - [ validationContext.MemberName! ]); - } -} -``` - -:::moniker-end - -:::moniker range=">= aspnetcore-6.0 < aspnetcore-8.0" - -```csharp -using System; -using System.ComponentModel.DataAnnotations; - -public class CustomValidator : ValidationAttribute -{ - protected override ValidationResult IsValid(object? value, - ValidationContext validationContext) - { - ... - - return new ValidationResult("Validation message to user.", - new[] { validationContext.MemberName! }); - } -} -``` - -:::moniker-end - -:::moniker range="< aspnetcore-6.0" - -```csharp -using System; -using System.ComponentModel.DataAnnotations; - -public class CustomValidator : ValidationAttribute -{ - protected override ValidationResult IsValid(object value, - ValidationContext validationContext) - { - ... - - return new ValidationResult("Validation message to user.", - new[] { validationContext.MemberName }); - } -} -``` - -:::moniker-end - -Inject services into custom validation attributes through the . The following example demonstrates a salad chef form that validates user input with dependency injection (DI). - -The `SaladChef` class indicates the approved starship ingredient list for a Ten Forward salad. - -`SaladChef.cs`: - -:::code language="csharp" source="~/../blazor-samples/8.0/BlazorSample_BlazorWebApp/SaladChef.cs"::: - -Register `SaladChef` in the app's DI container in the `Program` file: - -```csharp -builder.Services.AddTransient(); -``` - -The `IsValid` method of the following `SaladChefValidatorAttribute` class obtains the `SaladChef` service from DI to check the user's input. - -`SaladChefValidatorAttribute.cs`: - -:::code language="csharp" source="~/../blazor-samples/8.0/BlazorSample_BlazorWebApp/SaladChefValidatorAttribute.cs"::: - -The following component validates user input by applying the `SaladChefValidatorAttribute` (`[SaladChefValidator]`) to the salad ingredient string (`SaladIngredient`). - -`Starship12.razor`: - -:::moniker range=">= aspnetcore-9.0" - -:::code language="razor" source="~/../blazor-samples/9.0/BlazorSample_BlazorWebApp/Components/Pages/Starship12.razor"::: - -:::moniker-end - -:::moniker range=">= aspnetcore-8.0 < aspnetcore-9.0" - -:::code language="razor" source="~/../blazor-samples/8.0/BlazorSample_BlazorWebApp/Components/Pages/Starship12.razor"::: - -:::moniker-end - -:::moniker range="< aspnetcore-8.0" - -```razor -@page "/starship-12" -@inject SaladChef SaladChef - - - -

    - -

    - -
      - @foreach (var message in context.GetValidationMessages()) - { -
    • @message
    • - } -
    -
    - -@code { - private string? saladToppers; - - [SaladChefValidator] - public string? SaladIngredient { get; set; } - - protected override void OnInitialized() => - saladToppers ??= string.Join(", ", SaladChef.SaladToppers); -} -``` - -:::moniker-end - -:::moniker range=">= aspnetcore-7.0" - -## Custom validation CSS class attributes - -Custom validation CSS class attributes are useful when integrating with CSS frameworks, such as [Bootstrap](https://getbootstrap.com/). - -To specify custom validation CSS class attributes, start by providing CSS styles for custom validation. In the following example, valid (`validField`) and invalid (`invalidField`) styles are specified. - -Add the following CSS classes to the app's stylesheet: - -```css -.validField { - border-color: lawngreen; -} - -.invalidField { - background-color: tomato; -} -``` - -Create a class derived from that checks for field validation messages and applies the appropriate valid or invalid style. - -`CustomFieldClassProvider.cs`: - -:::moniker-end - -:::moniker range=">= aspnetcore-8.0" - -```csharp -using Microsoft.AspNetCore.Components.Forms; - -public class CustomFieldClassProvider : FieldCssClassProvider -{ - public override string GetFieldCssClass(EditContext editContext, - in FieldIdentifier fieldIdentifier) - { - var isValid = editContext.IsValid(fieldIdentifier); - - return isValid ? "validField" : "invalidField"; - } -} -``` - -:::moniker-end - -:::moniker range=">= aspnetcore-7.0 < aspnetcore-8.0" - -```csharp -using Microsoft.AspNetCore.Components.Forms; - -public class CustomFieldClassProvider : FieldCssClassProvider -{ - public override string GetFieldCssClass(EditContext editContext, - in FieldIdentifier fieldIdentifier) - { - var isValid = !editContext.GetValidationMessages(fieldIdentifier).Any(); - - return isValid ? "validField" : "invalidField"; - } -} -``` - -:::moniker-end - -:::moniker range=">= aspnetcore-7.0" - - - -Set the `CustomFieldClassProvider` class as the Field CSS Class Provider on the form's instance with . - -`Starship13.razor`: - -:::moniker-end - -:::moniker range=">= aspnetcore-9.0" - -:::code language="razor" source="~/../blazor-samples/9.0/BlazorSample_BlazorWebApp/Components/Pages/Starship13.razor"::: - -:::moniker-end - -:::moniker range=">= aspnetcore-8.0 < aspnetcore-9.0" - -:::code language="razor" source="~/../blazor-samples/8.0/BlazorSample_BlazorWebApp/Components/Pages/Starship13.razor"::: - -:::moniker-end - -:::moniker range=">= aspnetcore-7.0 < aspnetcore-8.0" - -```razor -@page "/starship-13" -@using System.ComponentModel.DataAnnotations -@inject ILogger Logger - - - - - - - - -@code { - private EditContext? editContext; - - public Starship? Model { get; set; } - - protected override void OnInitialized() - { - Model ??= new(); - editContext = new(Model); - editContext.SetFieldCssClassProvider(new CustomFieldClassProvider()); - } - - private void Submit() - { - Logger.LogInformation("Submit called: Processing the form"); - } - - public class Starship - { - [Required] - [StringLength(10, ErrorMessage = "Id is too long.")] - public string? Id { get; set; } - } -} -``` - - - -:::moniker-end - -:::moniker range=">= aspnetcore-7.0" - -The preceding example checks the validity of all form fields and applies a style to each field. If the form should only apply custom styles to a subset of the fields, make `CustomFieldClassProvider` apply styles conditionally. The following `CustomFieldClassProvider2` example only applies a style to the `Name` field. For any fields with names not matching `Name`, `string.Empty` is returned, and no style is applied. Using [reflection](/dotnet/csharp/advanced-topics/reflection-and-attributes/), the field is matched to the model member's property or field name, not an `id` assigned to the HTML entity. - -`CustomFieldClassProvider2.cs`: - -:::moniker-end - -:::moniker range=">= aspnetcore-8.0" - -```csharp -using Microsoft.AspNetCore.Components.Forms; - -public class CustomFieldClassProvider2 : FieldCssClassProvider -{ - public override string GetFieldCssClass(EditContext editContext, - in FieldIdentifier fieldIdentifier) - { - if (fieldIdentifier.FieldName == "Name") - { - var isValid = editContext.IsValid(fieldIdentifier); - - return isValid ? "validField" : "invalidField"; - } - - return string.Empty; - } -} -``` - -:::moniker-end - -:::moniker range=">= aspnetcore-7.0 < aspnetcore-8.0" - -```csharp -using Microsoft.AspNetCore.Components.Forms; - -public class CustomFieldClassProvider2 : FieldCssClassProvider -{ - public override string GetFieldCssClass(EditContext editContext, - in FieldIdentifier fieldIdentifier) - { - if (fieldIdentifier.FieldName == "Name") - { - var isValid = !editContext.GetValidationMessages(fieldIdentifier).Any(); - - return isValid ? "validField" : "invalidField"; - } - - return string.Empty; - } -} -``` - -:::moniker-end - -:::moniker range=">= aspnetcore-7.0" - - - -> [!NOTE] -> Matching the field name in the preceding example is case sensitive, so a model property member designated "`Name`" must match a conditional check on "`Name`": -> -> * Correctly matches: `fieldId.FieldName == "Name"` -> * Fails to match: `fieldId.FieldName == "name"` -> * Fails to match: `fieldId.FieldName == "NAME"` -> * Fails to match: `fieldId.FieldName == "nAmE"` - -Add an additional property to `Model`, for example: - -```csharp -[StringLength(10, ErrorMessage = "Description is too long.")] -public string? Description { get; set; } -``` - -Add the `Description` to the `CustomValidationForm` component's form: - -```razor - -``` - -Update the instance in the component's `OnInitialized` method to use the new Field CSS Class Provider: - -```csharp -editContext?.SetFieldCssClassProvider(new CustomFieldClassProvider2()); -``` - -Because a CSS validation class isn't applied to the `Description` field, it isn't styled. However, field validation runs normally. If more than 10 characters are provided, the validation summary indicates the error: - -> Description is too long. - -In the following example: - -* The custom CSS style is applied to the `Name` field. -* Any other fields apply logic similar to Blazor's default logic and using Blazor's default field CSS validation styles, `modified` with `valid` or `invalid`. Note that for the default styles, you don't need to add them to the app's stylesheet if the app is based on a Blazor project template. For apps not based on a Blazor project template, the default styles can be added to the app's stylesheet: - - ```css - .valid.modified:not([type=checkbox]) { - outline: 1px solid #26b050; - } - - .invalid { - outline: 1px solid red; - } - ``` - -`CustomFieldClassProvider3.cs`: - -:::moniker-end - -:::moniker range=">= aspnetcore-8.0" - -```csharp -using Microsoft.AspNetCore.Components.Forms; - -public class CustomFieldClassProvider3 : FieldCssClassProvider -{ - public override string GetFieldCssClass(EditContext editContext, - in FieldIdentifier fieldIdentifier) - { - var isValid = editContext.IsValid(fieldIdentifier); - - if (fieldIdentifier.FieldName == "Name") - { - return isValid ? "validField" : "invalidField"; - } - else - { - if (editContext.IsModified(fieldIdentifier)) - { - return isValid ? "modified valid" : "modified invalid"; - } - else - { - return isValid ? "valid" : "invalid"; - } - } - } -} -``` - -:::moniker-end - -:::moniker range=">= aspnetcore-7.0 < aspnetcore-8.0" - -```csharp -using Microsoft.AspNetCore.Components.Forms; - -public class CustomFieldClassProvider3 : FieldCssClassProvider -{ - public override string GetFieldCssClass(EditContext editContext, - in FieldIdentifier fieldIdentifier) - { - var isValid = !editContext.GetValidationMessages(fieldIdentifier).Any(); - - if (fieldIdentifier.FieldName == "Name") - { - return isValid ? "validField" : "invalidField"; - } - else - { - if (editContext.IsModified(fieldIdentifier)) - { - return isValid ? "modified valid" : "modified invalid"; - } - else - { - return isValid ? "valid" : "invalid"; - } - } - } -} -``` - -:::moniker-end - -:::moniker range=">= aspnetcore-7.0" - - - -Update the instance in the component's `OnInitialized` method to use the preceding Field CSS Class Provider: - -```csharp -editContext.SetFieldCssClassProvider(new CustomFieldClassProvider3()); -``` - -Using `CustomFieldClassProvider3`: - -* The `Name` field uses the app's custom validation CSS styles. -* The `Description` field uses logic similar to Blazor's logic and Blazor's default field CSS validation styles. - -:::moniker-end - -## Class-level validation with `IValidatableObject` - -[Class-level validation with `IValidatableObject`](xref:mvc/models/validation#ivalidatableobject) ([API documentation](xref:System.ComponentModel.DataAnnotations.IValidatableObject)) is supported for Blazor form models. validation only executes when the form is submitted and only if all other validation succeeds. - -:::moniker range="< aspnetcore-10.0" - -## Blazor data annotations validation package - -> [!NOTE] -> The [`Microsoft.AspNetCore.Components.DataAnnotations.Validation` package](https://www.nuget.org/packages/Microsoft.AspNetCore.Components.DataAnnotations.Validation) is no longer recommended for apps that target .NET 10 or later. For more information, see the [Nested objects, collection types, and complex types](#nested-objects-collection-types-and-complex-types) section. - -The [`Microsoft.AspNetCore.Components.DataAnnotations.Validation` package](https://www.nuget.org/packages/Microsoft.AspNetCore.Components.DataAnnotations.Validation) fills validation experience gaps using the component. The package is currently *experimental*. - -> [!WARNING] -> The [`Microsoft.AspNetCore.Components.DataAnnotations.Validation` package](https://www.nuget.org/packages/Microsoft.AspNetCore.Components.DataAnnotations.Validation) has a latest version of *release candidate* at [NuGet.org](https://www.nuget.org/packages/Microsoft.AspNetCore.Components.DataAnnotations.Validation). Continue to use the *experimental* release candidate package at this time. Experimental features are provided for the purpose of exploring feature viability and may not ship in a stable version. Watch the [Announcements GitHub repository](https://github.com/aspnet/Announcements), the [`dotnet/aspnetcore` GitHub repository](https://github.com/dotnet/aspnetcore), or this topic section for further updates. - -:::moniker-end - -:::moniker range="< aspnetcore-6.0" - -## `[CompareProperty]` attribute - -The doesn't work well with the component because the doesn't associate the validation result with a specific member. This can result in inconsistent behavior between field-level validation and when the entire model is validated on a submit. The [`Microsoft.AspNetCore.Components.DataAnnotations.Validation` *experimental* package](https://www.nuget.org/packages/Microsoft.AspNetCore.Components.DataAnnotations.Validation) introduces an additional validation attribute, `ComparePropertyAttribute`, that works around these limitations. In a Blazor app, `[CompareProperty]` is a direct replacement for the [`[Compare]` attribute](xref:System.ComponentModel.DataAnnotations.CompareAttribute). - -:::moniker-end - -:::moniker range=">= aspnetcore-10.0" - -## Nested objects and collection types - -Blazor form validation includes support for validating properties of nested objects and collection items with the built-in . - -To create a validated form, use a component inside an component. - -To opt into the nested objects and collection types validation feature: - -1. Call the extension method in the `Program` file where services are registered. -2. Declare the form model types in a C# class file, not in a Razor component (`.razor`). -3. Annotate the root form model type with the [`[ValidatableType]` attribute](xref:Microsoft.Extensions.Validation.ValidatableTypeAttribute), which indicates that a type is validatable to support discovery by the validation source generator. - -The following example demonstrates customer orders with nested collection form validation. - -In `Program.cs`, call on the service collection: - -```csharp -builder.Services.AddValidation(); -``` - -In the following `Order` class, the `[ValidatableType]` attribute is required on the top-level model type. The other types are discovered automatically. - -`Order.cs`: - -```csharp -using System.ComponentModel.DataAnnotations; - -[ValidatableType] -public class Order -{ - public Customer Customer { get; set; } = new(); - public List OrderItems { get; set; } = []; -} - -public class Customer -{ - [Required(ErrorMessage = "Name is required.")] - public string? FullName { get; set; } - - [Required(ErrorMessage = "Email is required.")] - public string? Email { get; set; } - - public ShippingAddress ShippingAddress { get; set; } = new(); -} -``` - -`OrderItem.cs`: - -```csharp -public class OrderItem -{ - [Required(ErrorMessage = "Id is required.")] - public int Id { get; set; } - - [Required(ErrorMessage = "Description is required.")] - public string? Description { get; set; } - - [Required(ErrorMessage = "Price is required.")] - public decimal Price { get; set; } -} -``` - -`ShippingAddress.cs`: - -```csharp -public class ShippingAddress -{ - [Required(ErrorMessage = "Street is required.")] - public string? Street { get; set; } - - [Required(ErrorMessage = "City is required.")] - public string? City { get; set; } - - [Required(ErrorMessage = "State/Province is required.")] - public string? StateProvince { get; set; } - - [Required(ErrorMessage = "PostalCode is required.")] - public string? PostalCode { get; set; } -} -``` - -In the following `OrderPage` component, the component is present in the component. - -`OrderPage.razor`: +For interactive forms, the form-level pending state can be used to disable submission while `ValidateAsync` is running: ```razor - - - -

    Customer Details

    -
    - - -
    - - // ... form continues ... -
    - -@code { - public Order? Model { get; set; } - - protected override void OnInitialized() => Model ??= new(); -} + ``` -The requirement to declare the model types outside of Razor components (`.razor` files) is due to the fact that both the nested collection validation feature and the Razor compiler itself are using a source generator. Currently, output of one source generator can't be used as an input for another source generator. - -For guidance on using validation models from a different assembly, such as a library or the `.Client` project of a Blazor Web App, see . - :::moniker-end -:::moniker range="< aspnetcore-10.0" - -## Nested objects, collection types, and complex types +:::moniker range="< aspnetcore-11.0" -> [!NOTE] -> For apps targeting .NET 10 or later, we no longer recommend using the [`Microsoft.AspNetCore.Components.DataAnnotations.Validation` *experimental* package](https://www.nuget.org/packages/Microsoft.AspNetCore.Components.DataAnnotations.Validation) and approach described in this section. We recommend using the built-in validation features of the component. - -Blazor provides support for validating form input using data annotations with the built-in . However, the in .NET 9 or earlier only validates top-level properties of the model bound to the form that aren't collection- or complex-type properties. - -To validate the bound model's entire object graph, including collection- and complex-type properties, use the `ObjectGraphDataAnnotationsValidator` provided by the *experimental* [`Microsoft.AspNetCore.Components.DataAnnotations.Validation` package](https://www.nuget.org/packages/Microsoft.AspNetCore.Components.DataAnnotations.Validation) in .NET 9 or earlier: +When handling `OnSubmit`, call before processing the form: ```razor - - - ... - -``` - -Annotate model properties with `[ValidateComplexType]`. In the following model classes, the `ShipDescription` class contains additional data annotations to validate when the model is bound to the form: - -`Starship.cs`: - -```csharp -using System; -using System.ComponentModel.DataAnnotations; - -public class Starship -{ - ... - - [ValidateComplexType] - public ShipDescription ShipDescription { get; set; } = new(); - + ... -} -``` - -`ShipDescription.cs`: - -```csharp -using System; -using System.ComponentModel.DataAnnotations; - -public class ShipDescription -{ - [Required] - [StringLength(40, ErrorMessage = "Description too long (40 char).")] - public string? ShortDescription { get; set; } - - [Required] - [StringLength(240, ErrorMessage = "Description too long (240 char).")] - public string? LongDescription { get; set; } -} -``` - -:::moniker-end - -## Enable the submit button based on form validation - -To enable and disable the submit button based on form validation, the following example: - -* Uses a shortened version of the earlier `Starfleet Starship Database` form (`Starship3` component) of the [Example form](xref:blazor/forms/input-components#example-form) section of the *Input components* article that only accepts a value for the ship's Id. The other `Starship` properties receive valid default values when an instance of the `Starship` type is created. -* Uses the form's to assign the model when the component is initialized. -* Validates the form in the context's callback to enable and disable the submit button. -* Implements and unsubscribes the event handler in the `Dispose` method. For more information, see . - -> [!NOTE] -> When assigning to the , don't also assign an to the . - -`Starship14.razor`: - -:::moniker range=">= aspnetcore-9.0" - -:::code language="razor" source="~/../blazor-samples/9.0/BlazorSample_BlazorWebApp/Components/Pages/Starship14.razor"::: - -:::moniker-end - -:::moniker range=">= aspnetcore-8.0 < aspnetcore-9.0" - -:::code language="razor" source="~/../blazor-samples/8.0/BlazorSample_BlazorWebApp/Components/Pages/Starship14.razor"::: - -:::moniker-end - -:::moniker range="< aspnetcore-8.0" - -```razor -@page "/starship-14" -@implements IDisposable -@inject ILogger Logger - - - - -
    - -
    -
    - -
    @code { - private bool formInvalid = false; - private EditContext? editContext; - - private Starship? Model { get; set; } - - protected override void OnInitialized() - { - Model ??= - new() - { - Id = "NCC-1701", - Classification = "Exploration", - MaximumAccommodation = 150, - IsValidatedDesign = true, - ProductionDate = new DateTime(2245, 4, 11) - }; - editContext = new(Model); - editContext.OnFieldChanged += HandleFieldChanged; - } - - private void HandleFieldChanged(object? sender, FieldChangedEventArgs e) - { - if (editContext is not null) - { - formInvalid = !editContext.Validate(); - StateHasChanged(); - } - } - - private void Submit() - { - Logger.LogInformation("Submit called: Processing the form"); - } - - public void Dispose() + private void HandleSubmit(EditContext editContext) { - if (editContext is not null) + if (editContext.Validate()) { - editContext.OnFieldChanged -= HandleFieldChanged; + Save(); } } } ``` - - :::moniker-end -If a form isn't preloaded with valid values and you wish to disable the **`Submit`** button on form load, set `formInvalid` to `true`. - -A side effect of the preceding approach is that a validation summary ( component) is populated with invalid fields after the user interacts with any one field. Address this scenario in either of the following ways: - -* Don't use a component on the form. -* Make the component visible when the submit button is selected (for example, in a `Submit` method). - -```razor - - - - - ... - - - - -@code { - private string displaySummary = "display:none"; - - ... +## Additional resources - private void Submit() - { - displaySummary = "display:block"; - } -} -``` +* +* +* +* :::moniker range=">= aspnetcore-10.0" -## `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`: +:::moniker-end -1. Member properties of `T` are validated, including recursively validating nested objects. -1. Type-level attributes of `T` are validated. -1. The method is executed, if `T` implements it. +:::moniker range=">= aspnetcore-11.0" -If one of the preceding steps produces a validation error, the remaining steps are skipped. +* :::moniker-end diff --git a/aspnetcore/blazor/globalization-localization.md b/aspnetcore/blazor/globalization-localization.md index 8f7d0064d06e..06ad50a3ec1d 100644 --- a/aspnetcore/blazor/globalization-localization.md +++ b/aspnetcore/blazor/globalization-localization.md @@ -35,7 +35,7 @@ For Blazor apps, localization of validation messages for [forms validation using For Blazor apps, localized validation messages for [forms validation using data annotations]() are supported through two paths: * The static resource path using for display names and for localized error messages. This approach is supported in every release. -* The `Microsoft.Extensions.Validation` package, which resolves validation messages and display names through . Available for Blazor apps that enable the new validation pipeline using `AddValidation()`. For details, see . +* , which resolves validation messages and display names through . Available for Blazor apps that enable the validation pipeline with `AddValidation()`. For details, see . :::moniker-end diff --git a/aspnetcore/fundamentals/localization/make-content-localizable.md b/aspnetcore/fundamentals/localization/make-content-localizable.md index c0af76fc0b55..bab10667bf7c 100644 --- a/aspnetcore/fundamentals/localization/make-content-localizable.md +++ b/aspnetcore/fundamentals/localization/make-content-localizable.md @@ -1,250 +1,172 @@ ---- -title: Make an ASP.NET Core app's content localizable -author: wadepickett -description: Learn how to make an ASP.NET Core app's content localizable to prepare the app for localizing content into different languages and cultures. -ms.author: wpickett -monikerRange: '>= aspnetcore-5.0' -ms.date: 06/20/2025 -uid: fundamentals/localization/make-content-localizable ---- -# Make an ASP.NET Core app's content localizable - -[!INCLUDE[](~/includes/not-latest-version.md)] - -:::moniker range="> aspnetcore-5.0" - -By [Hisham Bin Ateya](https://twitter.com/hishambinateya), [Damien Bowden](https://github.com/damienbod), [Bart Calixto](https://twitter.com/bartmax) and [Nadeem Afana](https://afana.me/) - -One task for localizing an app is to wrap localizable content with code that facilitates replacing that content for different cultures. - -## `IStringLocalizer` - - and were architected to improve productivity when developing localized apps. `IStringLocalizer` uses the and to provide culture-specific resources at run time. The interface has an indexer and an `IEnumerable` for returning localized strings. `IStringLocalizer` doesn't require storing the default language strings in a resource file. You can develop an app targeted for localization and not need to create resource files early in development. - -The following code example shows how to wrap the string "About Title" for localization. - -[!code-csharp[](~/fundamentals/localization/sample/8.x/Localization/Controllers/AboutController.cs)] - -In the preceding code, the `IStringLocalizer` implementation comes from [Dependency Injection](~/fundamentals/dependency-injection.md). If the localized value of "About Title" isn't found, then the indexer key is returned, that is, the string "About Title". - -You can leave the default language literal strings in the app and wrap them in the localizer, so that you can focus on developing the app. You develop an app with your default language and prepare it for the localization step without first creating a default resource file. - -Alternatively, you can use the traditional approach and provide a key to retrieve the default language string. For many developers, the new workflow of not having a default language *.resx* file and simply wrapping the string literals can reduce the overhead of localizing an app. Other developers prefer the traditional work flow as it can be easier to work with long string literals and easier to update localized strings. - -## `IHtmlLocalizer` - -Use the implementation for resources that contain HTML. HTML-encodes arguments that are formatted in the resource string, but doesn't HTML-encode the resource string itself. In the following highlighted code, only the value of the `name` parameter is HTML-encoded. - -[!code-csharp[](~/fundamentals/localization/sample/8.x/Localization/Controllers/BookController.cs?highlight=3,5,20&start=1&end=24)] - -***NOTE:*** Generally, only localize text, not HTML. - -## `IStringLocalizerFactory` - -At the lowest level, can be retrieved from of [Dependency Injection](~/fundamentals/dependency-injection.md): - -[!code-csharp[](~/fundamentals/localization/sample/8.x/Localization/Controllers/TestController.cs?highlight=6-12&name=snippet_1)] - -The preceding code demonstrates each of the two factory create methods. - -## Shared resources - -You can partition your localized strings by controller or area, or have just one container. In the sample app, a marker class named `SharedResource` is used for shared resources. The marker class is never called: - -[!code-csharp[](~/fundamentals/localization/sample/8.x/Localization/SharedResource.cs)] - -In the following sample, the `InfoController` and the `SharedResource` localizers are used: - -[!code-csharp[](~/fundamentals/localization/sample/8.x/Localization/Controllers/InfoController.cs?name=snippet_1)] - -## View localization - -The service provides localized strings for a [view](xref:mvc/views/overview). The `ViewLocalizer` class implements this interface and finds the resource location from the view file path. The following code shows how to use the default implementation of `IViewLocalizer`: - -[!code-cshtml[](~/fundamentals/localization/sample/8.x/Localization/Views/Home/About.cshtml)] - -The default implementation of `IViewLocalizer` finds the resource file based on the view's file name. There's no option to use a global shared resource file. `ViewLocalizer` implements the localizer using `IHtmlLocalizer`, so Razor doesn't HTML-encode the localized string. You can parameterize resource strings, and `IViewLocalizer` HTML-encodes the parameters but not the resource string. Consider the following Razor markup: - -```cshtml -@Localizer["Hello {0}!", UserManager.GetUserName(User)] -``` - -A French resource file could contain the following values: - -| Key | Value | -| -------------------------- | ----------------------------- | -| `Hello {0}!` | `Bonjour {0} !` | - -The rendered view would contain the HTML markup from the resource file. - -Generally, ***only localize text***, not HTML. - -To use a shared resource file in a view, inject `IHtmlLocalizer`: - -[!code-cshtml[](~/fundamentals/localization/sample/8.x/Localization/Views/Test/About.cshtml?highlight=5,12)] - -## DataAnnotations localization - -DataAnnotations error messages are localized with `IStringLocalizer`. Using the option `ResourcesPath = "Resources"`, the error messages in `RegisterViewModel` can be stored in either of the following paths: - -* *Resources/ViewModels.Account.RegisterViewModel.fr.resx* -* *Resources/ViewModels/Account/RegisterViewModel.fr.resx* - -[!code-csharp[](~/fundamentals/localization/sample/8.x/Localization/ViewModels/Account/RegisterViewModel.cs)] - -Non-validation attributes are localized. - - - -### How to use one resource string for multiple classes - -The following code shows how to use one resource string for validation attributes with multiple classes: - -```csharp - services.AddMvc() - .AddDataAnnotationsLocalization(options => { - options.DataAnnotationLocalizerProvider = (type, factory) => - factory.Create(typeof(SharedResource)); - }); -``` - -In the preceding code, `SharedResource` is the class corresponding to the *.resx* file where the validation messages are stored. With this approach, DataAnnotations only uses `SharedResource`, rather than the resource for each class. - -:::moniker-end - -:::moniker range=">= aspnetcore-11.0" - -## DataAnnotations localization in Minimal APIs and Blazor - -Validation 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: - -```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: - -```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.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. - -The conventions run only when `ErrorMessage` isn't set on the attribute instance, so model-specific overrides via `ErrorMessage = "MyKey"` continue to take precedence. - -:::moniker-end - -:::moniker range="> aspnetcore-5.0" - -## Configure localization services - -Localization services are configured in `Program.cs`: - -[!code-csharp[](~/fundamentals/localization/sample/6.x/Localization/program.cs?name=snippet_LocalizationConfigurationServices)] - -* adds the localization services to the services container, including implementations for `IStringLocalizer` and `IStringLocalizerFactory`. The preceding code also sets the resources path to "Resources". - -* adds support for localized view files. In this sample, view localization is based on the view file suffix. For example "fr" in the `Index.fr.cshtml` file. - -* adds support for localized `DataAnnotations` validation messages through `IStringLocalizer` abstractions. - -[!INCLUDE[](~/includes/localization/currency.md)] - -## Next steps - -Localizing an app also involves the following tasks: - -* [Provide localized resources for the languages and cultures the app supports](xref:fundamentals/localization/provide-resources) -* [Implement a strategy to select the language/culture for each request](xref:fundamentals/localization/select-language-culture) - -## Additional resources - -* [Url culture provider using middleware as filters in ASP.NET Core](https://andrewlock.net/url-culture-provider-using-middleware-as-mvc-filter-in-asp-net-core-1-1-0/) -* [Applying the RouteDataRequest CultureProvider globally with middleware as filters](https://andrewlock.net/applying-the-routedatarequest-cultureprovider-globally-with-middleware-as-filters/) -* -* -* -* -* [Globalizing and localizing .NET applications](/dotnet/standard/globalization-localization/index) -* [Localization.StarterWeb project](https://github.com/aspnet/Entropy/tree/master/samples/Localization.StarterWeb) used in the article. -* [Resources in .resx Files](/dotnet/framework/resources/working-with-resx-files-programmatically) -* [Localization & Generics](http://hishambinateya.com/localization-and-generics) - -:::moniker-end - -[!INCLUDE [make-content-localizable5](~/fundamentals/localization/includes/make-content-localizable5.md)] +--- +title: Make an ASP.NET Core app's content localizable +author: wadepickett +description: Learn how to make an ASP.NET Core app's content localizable to prepare the app for localizing content into different languages and cultures. +ms.author: wpickett +monikerRange: '>= aspnetcore-5.0' +ms.date: 09/22/2026 +uid: fundamentals/localization/make-content-localizable +--- +# Make an ASP.NET Core app's content localizable + +[!INCLUDE[](~/includes/not-latest-version.md)] + +:::moniker range="> aspnetcore-5.0" + +By [Hisham Bin Ateya](https://twitter.com/hishambinateya), [Damien Bowden](https://github.com/damienbod), [Bart Calixto](https://twitter.com/bartmax) and [Nadeem Afana](https://afana.me/) + +One task for localizing an app is to wrap localizable content with code that facilitates replacing that content for different cultures. + +## `IStringLocalizer` + + and were architected to improve productivity when developing localized apps. `IStringLocalizer` uses the and to provide culture-specific resources at run time. The interface has an indexer and an `IEnumerable` for returning localized strings. `IStringLocalizer` doesn't require storing the default language strings in a resource file. You can develop an app targeted for localization and not need to create resource files early in development. + +The following code example shows how to wrap the string "About Title" for localization. + +[!code-csharp[](~/fundamentals/localization/sample/8.x/Localization/Controllers/AboutController.cs)] + +In the preceding code, the `IStringLocalizer` implementation comes from [Dependency Injection](~/fundamentals/dependency-injection.md). If the localized value of "About Title" isn't found, then the indexer key is returned, that is, the string "About Title". + +You can leave the default language literal strings in the app and wrap them in the localizer, so that you can focus on developing the app. You develop an app with your default language and prepare it for the localization step without first creating a default resource file. + +Alternatively, you can use the traditional approach and provide a key to retrieve the default language string. For many developers, the new workflow of not having a default language *.resx* file and simply wrapping the string literals can reduce the overhead of localizing an app. Other developers prefer the traditional work flow as it can be easier to work with long string literals and easier to update localized strings. + +## `IHtmlLocalizer` + +Use the implementation for resources that contain HTML. HTML-encodes arguments that are formatted in the resource string, but doesn't HTML-encode the resource string itself. In the following highlighted code, only the value of the `name` parameter is HTML-encoded. + +[!code-csharp[](~/fundamentals/localization/sample/8.x/Localization/Controllers/BookController.cs?highlight=3,5,20&start=1&end=24)] + +***NOTE:*** Generally, only localize text, not HTML. + +## `IStringLocalizerFactory` + +At the lowest level, can be retrieved from of [Dependency Injection](~/fundamentals/dependency-injection.md): + +[!code-csharp[](~/fundamentals/localization/sample/8.x/Localization/Controllers/TestController.cs?highlight=6-12&name=snippet_1)] + +The preceding code demonstrates each of the two factory create methods. + +## Shared resources + +You can partition your localized strings by controller or area, or have just one container. In the sample app, a marker class named `SharedResource` is used for shared resources. The marker class is never called: + +[!code-csharp[](~/fundamentals/localization/sample/8.x/Localization/SharedResource.cs)] + +In the following sample, the `InfoController` and the `SharedResource` localizers are used: + +[!code-csharp[](~/fundamentals/localization/sample/8.x/Localization/Controllers/InfoController.cs?name=snippet_1)] + +## View localization + +The service provides localized strings for a [view](xref:mvc/views/overview). The `ViewLocalizer` class implements this interface and finds the resource location from the view file path. The following code shows how to use the default implementation of `IViewLocalizer`: + +[!code-cshtml[](~/fundamentals/localization/sample/8.x/Localization/Views/Home/About.cshtml)] + +The default implementation of `IViewLocalizer` finds the resource file based on the view's file name. There's no option to use a global shared resource file. `ViewLocalizer` implements the localizer using `IHtmlLocalizer`, so Razor doesn't HTML-encode the localized string. You can parameterize resource strings, and `IViewLocalizer` HTML-encodes the parameters but not the resource string. Consider the following Razor markup: + +```cshtml +@Localizer["Hello {0}!", UserManager.GetUserName(User)] +``` + +A French resource file could contain the following values: + +| Key | Value | +| -------------------------- | ----------------------------- | +| `Hello {0}!` | `Bonjour {0} !` | + +The rendered view would contain the HTML markup from the resource file. + +Generally, ***only localize text***, not HTML. + +To use a shared resource file in a view, inject `IHtmlLocalizer`: + +[!code-cshtml[](~/fundamentals/localization/sample/8.x/Localization/Views/Test/About.cshtml?highlight=5,12)] + +## DataAnnotations localization + +DataAnnotations error messages are localized with `IStringLocalizer`. Using the option `ResourcesPath = "Resources"`, the error messages in `RegisterViewModel` can be stored in either of the following paths: + +* *Resources/ViewModels.Account.RegisterViewModel.fr.resx* +* *Resources/ViewModels/Account/RegisterViewModel.fr.resx* + +[!code-csharp[](~/fundamentals/localization/sample/8.x/Localization/ViewModels/Account/RegisterViewModel.cs)] + +Non-validation attributes are localized. + + + +### How to use one resource string for multiple classes + +The following code shows how to use one resource string for validation attributes with multiple classes: + +```csharp + services.AddMvc() + .AddDataAnnotationsLocalization(options => { + options.DataAnnotationLocalizerProvider = (type, factory) => + factory.Create(typeof(SharedResource)); + }); +``` + +In the preceding code, `SharedResource` is the class corresponding to the *.resx* file where the validation messages are stored. With this approach, DataAnnotations only uses `SharedResource`, rather than the resource for each class. + +:::moniker-end + +:::moniker range=">= aspnetcore-11.0" + +## DataAnnotations localization in Minimal APIs and Blazor + +Validation error messages and the display names of validated members are localized by , which is the validation pipeline used by Minimal APIs and Blazor forms. + +Localization activates automatically when an is registered. Call together with : + +```csharp +builder.Services.AddLocalization(); +builder.Services.AddValidation(); +``` + +For the message lookup key conventions, shared resource files, custom message formatting, and the full set of options, see . + +The integration doesn't apply to MVC and Razor Pages apps. For those frameworks, see . + +:::moniker-end + +:::moniker range="> aspnetcore-5.0" + +## Configure localization services + +Localization services are configured in `Program.cs`: + +[!code-csharp[](~/fundamentals/localization/sample/6.x/Localization/program.cs?name=snippet_LocalizationConfigurationServices)] + +* adds the localization services to the services container, including implementations for `IStringLocalizer` and `IStringLocalizerFactory`. The preceding code also sets the resources path to "Resources". + +* adds support for localized view files. In this sample, view localization is based on the view file suffix. For example "fr" in the `Index.fr.cshtml` file. + +* adds support for localized `DataAnnotations` validation messages through `IStringLocalizer` abstractions. + +[!INCLUDE[](~/includes/localization/currency.md)] + +## Next steps + +Localizing an app also involves the following tasks: + +* [Provide localized resources for the languages and cultures the app supports](xref:fundamentals/localization/provide-resources) +* [Implement a strategy to select the language/culture for each request](xref:fundamentals/localization/select-language-culture) + +## Additional resources + +* [Url culture provider using middleware as filters in ASP.NET Core](https://andrewlock.net/url-culture-provider-using-middleware-as-mvc-filter-in-asp-net-core-1-1-0/) +* [Applying the RouteDataRequest CultureProvider globally with middleware as filters](https://andrewlock.net/applying-the-routedatarequest-cultureprovider-globally-with-middleware-as-filters/) +* +* +* +* +* [Globalizing and localizing .NET applications](/dotnet/standard/globalization-localization/index) +* [Localization.StarterWeb project](https://github.com/aspnet/Entropy/tree/master/samples/Localization.StarterWeb) used in the article. +* [Resources in .resx Files](/dotnet/framework/resources/working-with-resx-files-programmatically) +* [Localization & Generics](http://hishambinateya.com/localization-and-generics) + +:::moniker-end + +[!INCLUDE [make-content-localizable5](~/fundamentals/localization/includes/make-content-localizable5.md)] diff --git a/aspnetcore/fundamentals/minimal-apis.md b/aspnetcore/fundamentals/minimal-apis.md index aa1b47e11a17..89e5b65aac0b 100644 --- a/aspnetcore/fundamentals/minimal-apis.md +++ b/aspnetcore/fundamentals/minimal-apis.md @@ -6,7 +6,7 @@ content_well_notification: AI-contribution description: Provides an overview of Minimal APIs in ASP.NET Core monikerRange: '>= aspnetcore-6.0' ms.author: wpickett -ms.date: 08/14/2026 +ms.date: 09/22/2026 uid: fundamentals/minimal-apis --- @@ -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..fc6082ed5fcd 100644 --- a/aspnetcore/fundamentals/validation.md +++ b/aspnetcore/fundamentals/validation.md @@ -5,248 +5,278 @@ 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: 09/22/2026 uid: fundamentals/validation --- # Validation in ASP.NET Core - supports complex model validation in Blazor and Minimal API projects. + provides model validation for Blazor and Minimal API projects. + +Validation rules are declared the same way in both frameworks, using [data annotations attributes](xref:System.ComponentModel.DataAnnotations) and . This article describes the validation behavior that both frameworks share: + +:::moniker range=">= aspnetcore-11.0" + +Asynchronous validation attributes and are also supported. + +:::moniker-end + +* Minimal APIs use the service to validate a request before the endpoint handler runs. For guidance on how validation is surfaced in an endpoint, see . +* Blazor uses the service through the component. For guidance on how validation is surfaced in a form, see . While the API in the [`Microsoft.Extensions.Validation` NuGet package](https://www.nuget.org/packages/Microsoft.Extensions.Validation) can be used in scenarios outside ASP.NET Core, this article focuses on ASP.NET Core. The API isn't supported for MVC or Razor Pages. For validation guidance that applies to MVC and Razor Pages, see . -To enable validation, call on in the app's `Program` file: +## Register validation services + +Call on in the app's `Program` file: ```csharp builder.Services.AddValidation(); ``` -For Minimal APIs, the implementation automatically discovers types that are defined in handlers or as base types of the types defined in handlers. An endpoint filter performs validation on these types and is added for each endpoint. +For Minimal APIs, this enables automatic validation of supported parameters before the endpoint handler runs. -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. +Blazor forms can perform basic top-level DataAnnotations validation without calling `AddValidation`. Registering the service enables nested object and collection validation when the form model is discovered by the validation source generator. -## Register validation in multi-assembly apps +:::moniker range=">= aspnetcore-11.0" -To validate types from separate assemblies: +Generated validation metadata also enables message localization. -* 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-end -### Minimal API example +Validation uses a source generator that creates metadata for validatable types in the assembly where `AddValidation` is called. For types declared in another assembly, see [Register validation across assemblies](#register-validation-across-assemblies). -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. +### Behavior without generated validation metadata -Create a service collection extension method in an assembly that defines Minimal API endpoints and call it from the host app. +The consequence of omitting , or of calling it without the required type being discovered by the source generator, differs by framework, as shown in the following table. -`ServiceCollectionExtensions.cs` in the assembly that defines the endpoints, which uses the example namespace `MinimalApisAssembly.Extensions`: +:::moniker range=">= aspnetcore-11.0" -```csharp -namespace MinimalApisAssembly.Extensions; +Framework | Behavior without generated validation metadata +--- | --- +Minimal APIs | No automatic validation runs. Invalid input reaches the endpoint handler instead of being rejected before the handler executes. +Blazor | The component falls back to , which validates top-level properties only. Nested objects, collection items, and the [`Microsoft.Extensions.Validation` message-localization pipeline](#localize-validation-messages) aren't supported on the fallback path. -public static class ServiceCollectionExtensions -{ - public static IServiceCollection AddApiValidation( - this IServiceCollection services) - { - return services.AddValidation(); - } -} -``` +:::moniker-end -In the host app's `Program` file, call the extension method instead of calling `AddValidation` directly: +:::moniker range="< aspnetcore-11.0" -```csharp -using MinimalApisAssembly.Extensions; +Framework | Behavior without generated validation metadata +--- | --- +Minimal APIs | No automatic validation runs. Invalid input reaches the endpoint handler instead of being rejected before the handler executes. +Blazor | The component falls back to , which validates top-level properties only. Nested objects and collection items aren't validated on the fallback path. -... +:::moniker-end -builder.Services.AddApiValidation(); +Missing metadata doesn't produce a runtime exception or log entry. Build analyzers report many unsupported configurations, but other missing-metadata cases might not produce a diagnostic. For checks to perform when expected validation is missing, see [Troubleshoot generated validation metadata](#troubleshoot-generated-validation-metadata). -... + -var app = builder.Build(); +## How validation runs -app.MapApi(); -``` +Minimal APIs begin with endpoint parameter validation. Blazor begins with validation of the form's model. Both frameworks then use the same model validation order and object-graph traversal. -In the preceding example, `MapApi` is an extension method defined in the endpoints assembly that maps the Minimal API endpoints. Define it alongside `AddApiValidation` so both the endpoint mappings and validation are registered from the same assembly. + -### Blazor Web App example +### Minimal API parameter validation -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. +For each supported endpoint parameter: -Create a service collection extension method in the assembly that defines the validatable types and call it from the host app. +1. Validate instances applied directly to the parameter. +1. If the parameter value is an `IEnumerable`, validate each non-`null` element. Otherwise, validate the parameter value itself. -For model validation defined in the `.Client` project of a Blazor Web App: +:::moniker range="< aspnetcore-11.0" -* 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 . +> [!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). -The preceding approach results in validation of the types from both assemblies. +:::moniker-end -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. + + -`ServiceCollectionExtensions.cs` in the `.Client` project that defines validatable types, which uses the example namespace `BlazorSample.Client.Extensions`: +### Model validation order -```csharp -namespace BlazorSample.Client.Extensions; +When validating a model: -public static class ServiceCollectionExtensions -{ - public static IServiceCollection AddValidationForClientTypes( - this IServiceCollection services) - { - return services.AddValidation(); - } -} -``` +1. Validate the attributes on each property, then validate the property's value. If the value is an `IEnumerable`, validate each non-`null` element. If property validation produces an error, the remaining steps are skipped. +1. Validate attributes applied to the model type. If type-level validation produces an error, the remaining step is skipped. +1. Run if the model implements . -In the server project's `Program` file: +### Nested objects and collections -* Call the `.Client` project's service collection extension method to validate types in the `.Client` project. -* Call to validate types in the server project. +Validation recurses into nested objects and collection items, so a rule declared on a nested property is enforced when the root model is validated. Without generated validation metadata, Blazor only validates the top-level properties of a form model. -```csharp -using BlazorSample.Client.Extensions; +To validate a nested object graph: -... +1. Call in the `Program` file where services are registered. +1. Declare model types in C# files (`.cs`), not in Razor component files (`.razor`). +1. Annotate the root model type with (`[ValidatableType]`). Types reachable from the root are discovered automatically. -builder.Services.AddValidationForClientTypes(); -builder.Services.AddValidation(); -``` +In the following example, only the root `Order` type is annotated. The other model types are reachable from `Order` and are included in its validation graph. -:::moniker range="= aspnetcore-10.0" +`Order.cs`: -## Experimental API in apps that target .NET 10 +```csharp +using System.ComponentModel.DataAnnotations; +using Microsoft.Extensions.Validation; -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. +[ValidatableType] +public class Order +{ + public Customer Customer { get; set; } = new(); + public List OrderItems { get; set; } = []; +} -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. +public class Customer +{ + [Required(ErrorMessage = "Name is required.")] + public string? FullName { get; set; } -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: + public ShippingAddress ShippingAddress { get; set; } = new(); +} -> :::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."::: +public class ShippingAddress +{ + [Required(ErrorMessage = "Street is required.")] + public string? Street { get; set; } +} -The warning can be suppressed using any of the following approaches: +public class OrderItem +{ + [Required(ErrorMessage = "Description is required.")] + public string? Description { get; set; } -* A `` property in the project file: + [Range(1, 1000)] + public int Quantity { get; set; } +} +``` - ```xml - - $(NoWarn);ASP0029 - - ``` +Errors from nested members use paths such as `Customer.ShippingAddress.Street` or `OrderItems[0].Description`. -* A [`pragma` directive](/cpp/preprocessor/pragma-directives-and-the-pragma-keyword) where the attribute is used: +For model types defined in another assembly or in a Blazor Web App's `.Client` project, see [Register validation across assemblies](#register-validation-across-assemblies). - ```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: +### Skip validation - ``` - dotnet_diagnostic.ASP0029.severity = none - ``` +Apply to a property or parameter to skip validation for that property or parameter. Apply it to a type to skip validation for all properties and parameters of that type, including nested properties when the type is complex. -If suppressing the warning isn't acceptable, manually create the embedded attribute in the library that the Web and Razor SDKs generate automatically. +## Write custom validation rules -`ValidatableTypeAttribute.cs`: +When the [built-in validation attributes](xref:mvc/models/validation#built-in-attributes) don't express a rule, write a custom or implement on the model. Both are discovered and executed by in Blazor and Minimal API apps. + +### Custom validation attributes + +Derive from and override to validate a single value. + +Pass the validation context's when creating the . Without a member name, the result isn't associated with a field, which prevents the error from being displayed next to the corresponding input in a Blazor form: ```csharp -namespace Microsoft.Extensions.Validation.Embedded +using System.ComponentModel.DataAnnotations; + +public class EvenNumberAttribute : ValidationAttribute { - [AttributeUsage(AttributeTargets.Class)] - internal sealed class ValidatableTypeAttribute : Attribute + protected override ValidationResult? IsValid(object? value, + ValidationContext validationContext) { + if (value is int number && number % 2 != 0) + { + return new ValidationResult( + "The value must be an even number.", + [ validationContext.MemberName! ]); + } + + return ValidationResult.Success; } } ``` -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 - -## Validatable entities - -Three types of entities can be validated: - -* [Parameters](#parameter-validation) (specific to Minimal API endpoint parameters) -* [Types](#type-validation) -* [Properties](#property-validation) +Apply the attribute to a property in the same way as a built-in attribute: -### 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" +```csharp +public class Order +{ + [EvenNumber] + public int Quantity { get; set; } +} +``` -> [!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). +### Resolve services in a validation attribute -:::moniker-end +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: -### Type validation +```csharp +protected override ValidationResult? IsValid(object? value, + ValidationContext validationContext) +{ + var catalog = validationContext.GetService(); -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. +For a service that must be resolved, use . Services resolved this way must be registered in the app's service container. -### Property validation +### Class-level validation with `IValidatableObject` -Property validation happens as part of the type validation as explained in the previous section. It involves the following steps: +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: -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. +```csharp +using System.ComponentModel.DataAnnotations; -## Explicit validation skipping +public class DateRange : IValidatableObject +{ + public DateOnly Start { get; set; } + public DateOnly End { get; set; } -When needed, you can skip validation for a specific parameter, type, or property by applying the . + public IEnumerable Validate(ValidationContext validationContext) + { + if (End < Start) + { + yield return new ValidationResult( + "End date must fall on or after the start date.", + [ nameof(End) ]); + } + } +} +``` -## Force-generate validatable type information +:::moniker range=">= aspnetcore-11.0" - works via a Roslyn source generator that detects the object graph and types for Minimal API endpoint parameters. +For rules that require I/O, such as a database or web API call, see the [Asynchronous validation support](#asynchronous-validation-support) section instead. -In some cases, not all of the types that are part of the object graph can be determined at compile time. In these cases, you can force the source generator to consider a type for validation by applying to the type. +:::moniker-end :::moniker range=">= aspnetcore-11.0" - +:::moniker range=">= aspnetcore-11.0" ## Asynchronous validation support supports asynchronous validation. Apply custom implementations of `AsyncValidationAttribute` to parameters, types, or properties, and they're called asynchronously. In addition, types can implement `IAsyncValidatableObject` as well. -When validating properties on a type, all validation tasks are started concurrently. Similarly, elements of `IEnumerable` collections are validated concurrently. +Asynchronous validation operations can run in parallel, and their execution and completion order isn't guaranteed. Validation rules must not depend on a particular order. `IAsyncValidatableObject` and `AsyncValidationAttribute` require synchronous **and** asynchronous validation logic. For example, the `Validate` and `ValidateAsync` methods of `IAsyncValidatableObject` must be implemented for objects that use the interface. However, validation never calls both methods. If validation is called through an asynchronous code path, only `ValidateAsync` is called. If validation is called through a synchronous code path, only `Validate` is called. 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 in .NET 11 or later. Asynchronous rules therefore work in Blazor forms without additional configuration. If your implementation can't support the synchronous path, throw . -The following example demonstrates a validation class that implements the `IAsyncValidatableObject` interface. In the following scenario, validation requires an asynchronous call path to check a database for a valid email username via a hypothetical `IUserService` service. Because validation requires an asynchronous database call in this scenario, the synchronous `Validate` method, which is required by the interface's contract, shouldn't be called by developer code elsewhere and throws if it ever is called. +The following example shows object-level asynchronous validation with `IAsyncValidatableObject`. It uses a hypothetical `IUserService` to check a database for an existing email address. Because the rule requires asynchronous I/O, the required synchronous `Validate` implementation throws . ```csharp using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; +using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; @@ -283,17 +313,288 @@ public class ValidateUser : IAsyncValidatableObject :::moniker-end +:::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. + +> [!IMPORTANT] +> `AddLocalization` registers localization services but doesn't select the culture for a request or circuit. Validation resource lookup uses . For request culture providers, including query string, cookie, and `Accept-Language` header selection, see . For configuring culture selection across Blazor render modes, see . + +```csharp +using System.ComponentModel.DataAnnotations; +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 in this order: `CustomerModel_Name_RequiredAttribute_Error`, `CustomerModel_RequiredAttribute_Error`, and `RequiredAttribute_Error`. If none resolve, the attribute's built-in message is used. + +This makes it possible to translate or override the default message of an attribute across an entire app without setting `ErrorMessage` on every attribute instance: + +```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 a project root namespace of `Contoso`, French messages for `Contoso.Models.Customer` are read from `Resources/Models/Customer.fr.resx` (equivalently `Resources/Models.Customer.fr.resx`). For a full description of the conventions, see . + +For per-type lookup, place the resource files in the project that declares the validated type. For example, if a Blazor Web App's form models are declared in its `.Client` project, place their per-type resources in that project. The model assembly's root namespace and the configured `ResourcesPath` determine the resource name. + +### 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 different resources for different model types. + +The marker type passed to `factory.Create` identifies both the resource name and the assembly containing the resource. This makes a shared resource in the host project an alternative to placing per-type resources in a referenced model assembly. + +### Localize from a source other than resource files + +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 configured with or perform their own resource lookup and aren't processed by the `Microsoft.Extensions.Validation` localizer. + +### 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] +> This localization pipeline requires . A Blazor form whose model isn't discovered by the validation source generator falls back to . Attributes configured to localize themselves with `ErrorMessageResourceType` continue to do so, but the generated lookup conventions and `ValidationOptions.LocalizerProvider` aren't available. For more information, see [Behavior without generated validation metadata](#behavior-without-generated-validation-metadata). + +:::moniker-end + +## Configure generated validation metadata + + uses a Roslyn source generator to create validation metadata at build time. Minimal API parameter types are discovered from endpoint handler signatures. Blazor form model types are included by applying . + + + +### Include root model types + +Apply `[ValidatableType]` to a Blazor form's root model type and to any other root type that the source generator can't discover from a Minimal API endpoint signature. Types reachable from the root are included automatically. + +### Model types can't be declared in Razor component files + +The Razor compiler and the validation feature both use source generators. A source generator can't inspect another generator's output, so the validation generator can't include model types declared in Razor component files (`.razor`). Declare model types in regular C# files (`.cs`) instead. + +:::moniker range=">= aspnetcore-11.0" + +Applying `[ValidatableType]` to a type in generated code produces warning ASP0037. + +:::moniker-end + + + +### Register validation across assemblies + +The source generator creates metadata only for the assembly where is called. Calling `AddValidation` only from the host app doesn't generate metadata for types declared in a referenced assembly, such as a class library or the `.Client` project of a Blazor Web App. + +To validate types from separate assemblies: + +* 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. + +For a Blazor Web App whose form models are declared in the `.Client` project, create the validation-registration extension method in that project and call it from the server project's `Program` file. + +For Minimal API endpoints and models defined in referenced assemblies, see . + +### Troubleshoot generated validation metadata + +Missing generated validation metadata doesn't produce a runtime exception or log entry. If expected validation behavior is missing, confirm all of the following: + +* `AddValidation` is called from the assembly that declares the validatable types. +* Blazor root form models and other roots not discovered from Minimal API signatures have `[ValidatableType]`. +* Model types are declared in `.cs` files. +* Validated types and properties are accessible to generated code. + +:::moniker range=">= aspnetcore-11.0" + +Build analyzers report common unsupported configurations: + +* and report inaccessible validatable types and endpoint parameter types. +* and report inaccessible validated properties or property types. +* reports `[ValidatableType]` applied to generated code. +* reports `[ValidatableType]` used without a matching `AddValidation` call. + +Other missing-metadata cases might not produce a diagnostic. + +:::moniker-end + +For the runtime behavior when metadata isn't available, see [Behavior without generated validation metadata](#behavior-without-generated-validation-metadata). + +:::moniker range=">= aspnetcore-10.0 < aspnetcore-11.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,7 +602,6 @@ 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) * diff --git a/aspnetcore/release-notes/aspnetcore-11.md b/aspnetcore/release-notes/aspnetcore-11.md index cc5cfcefd437..7c52e2e2e504 100644 --- a/aspnetcore/release-notes/aspnetcore-11.md +++ b/aspnetcore/release-notes/aspnetcore-11.md @@ -4,7 +4,7 @@ ai-usage: ai-assisted author: wadepickett description: Learn about the new features in ASP.NET Core in .NET 11. ms.author: wpickett -ms.date: 09/14/2026 +ms.date: 09/22/2026 uid: aspnetcore-11 --- # What's new in ASP.NET Core in .NET 11 diff --git a/aspnetcore/release-notes/aspnetcore-11/includes/blazor.md b/aspnetcore/release-notes/aspnetcore-11/includes/blazor.md index aca58f2882d5..fd80c27205bc 100644 --- a/aspnetcore/release-notes/aspnetcore-11/includes/blazor.md +++ b/aspnetcore/release-notes/aspnetcore-11/includes/blazor.md @@ -708,7 +708,7 @@ Blazor static server-side rendering (static SSR) forms now get instant, in-brows The feature is enabled by default for all static SSR forms that include the `DataAnnotationsValidator` component. Both enhanced and non-enhanced forms are supported. -Complete feature coverage is available in . +Complete feature coverage is available in . For more information, see the following resources: @@ -719,12 +719,14 @@ Please don't comment on closed issues and PRs. If you have feedback on this feat ### Asynchronous form validation support -Blazor forms receive support for async validation rules, such as database lookups or remote API calls. In any rendering mode, `EditForm` submit validation now properly awaits async validators end-to-end. In interactive modes, validator components can register per-field async validation via `EditContext.RegisterAsyncFieldValidator`. The framework tracks them, cancels superseded validations, and exposes progress status via `IsValidationPending(field)` and `IsValidationFaulted(field)`. +Blazor forms receive support for async validation rules, such as database lookups or remote API calls. In any rendering mode, `EditForm` submit validation awaits async validators end-to-end. The built-in `DataAnnotationsValidator` component runs the asynchronous `DataAnnotations` APIs (`AsyncValidationAttribute` and `IAsyncValidatableObject`), so asynchronous rules declared on the model work without additional configuration. +Validator components register asynchronous work with `ValidationRequestedEventArgs.AddAsyncValidator` for the whole form and `EditContext.RegisterAsyncFieldValidator` for a single field. The framework owns the cancellation token source, cancels superseded validations, and exposes progress with `IsValidationPending(field)` and `IsValidationFaulted(field)`. + ```razor - + @if (editContext.IsValidationPending(() => model.Username)) { @@ -765,75 +767,16 @@ The built-in `DataAnnotationsValidator` component runs the asynchronous `DataAnn editContext.NotifyValidationStateChanged(); } - private async Task HandleSubmit() => await editContext.ValidateAsync(); + private Task HandleSubmit() => RegisterAsync(); } ``` -Complete feature coverage is available in . +Complete feature coverage is available in . For more information, see [Add built-in support for async form validation in Blazor (`dotnet/aspnetcore` #66526)](https://github.com/dotnet/aspnetcore/pull/66526). Please don't comment on closed issues and PRs. If you have feedback on this feature, please open a new issue on the `dotnet/aspnetcore` GitHub repository. -### Blazor and Minimal APIs support error localization - -Validation of Blazor forms and Minimal API endpoints receives first-class support for localization of error messages and property names. Localization activates automatically once an `IStringLocalizerFactory` is available. By default, localization registered by `AddLocalization` uses language-specific RESX files deployed as part of the assembly. - -```csharp -builder.Services.AddLocalization(); -builder.Services.AddValidation(); -``` - -```csharp -[ValidatableType] -public class ContactModel -{ - // Values of ErrorMessage are used as localization keys. - [Required(ErrorMessage = "RequiredError")] - [EmailAddress(ErrorMessage = "EmailError")] - [Display(Name = "ContactEmail")] - public string? Email { get; set; } -} -``` - -Apps can also register custom `IStringLocalizerFactory` implementations to read the localized strings from other sources, such as databases or JSON files. A user registered type takes precedence over the default RESX localization. - -```csharp -builder.Services.AddSingleton(); -builder.Services.AddValidation(); -``` - -To resolve keys from a shared resource file instead of the validated type's own resources, set `ValidationOptions.LocalizerProvider`: - -```csharp -builder.Services.AddValidation(options => -{ - options.LocalizerProvider = (_, factory) => factory.Create(typeof(ValidationMessages)); -}); -``` - -When an attribute doesn't set `ErrorMessage`, conventional lookup keys are tried from most to least specific, removing the need to specify localization keys on every validation attribute: - -```csharp -[ValidatableType] -public class ContactModel -{ - // Looks up 'ContactModel_Username_RequiredAttribute_Error', then - // 'ContactModel_RequiredAttribute_Error', then 'RequiredAttribute_Error'. - [Required] - public string? Username { get; set; } -} -``` - -Complete feature coverage is available in the following articles: - -* -* - -For more information, see [Add localization support to Microsoft.Extensions.Validation (`dotnet/aspnetcore` #66646)](https://github.com/dotnet/aspnetcore/pull/66646). - -Please don't comment on closed issues and PRs. If you have feedback on this feature, please open a new issue on the `dotnet/aspnetcore` GitHub repository. - ### Fixes to TempData and `[SupplyParameterFromSession]` persistence for streaming SSR When a page uses session-backed features, where a component has a `[SupplyParameterFromSession]` parameter (which creates a subscription) or the session-storage TempData provider is active, the session cookie (`.AspNetCore.Session`) is now issued before streaming begins, even if no value is ultimately written. Pages that don't use session-backed features are unaffected. @@ -914,15 +857,6 @@ The following new [`QuickGrid` component](xref:Microsoft.AspNetCore.Components.Q For more information, see . -### `ValidatableTypeAttribute` and `SkipValidationAttribute` are no longer experimental - -The and attributes from the [`Microsoft.Extensions.Validation` NuGet package](https://www.nuget.org/packages/Microsoft.Extensions.Validation) are no longer experimental. - -For more information, see the following resources: - -* -* - ### Cache rendered output of a component subtree during static SSR The new `CacheView` component caches the rendered output of a Razor component subtree during static server-side rendering (static SSR). On a cache hit, cached markup is replayed without instantiating or running the lifecycle of the child components that were included in the cached output. diff --git a/aspnetcore/release-notes/aspnetcore-11/includes/validation-attributes-no-longer-experimental.md b/aspnetcore/release-notes/aspnetcore-11/includes/validation-attributes-no-longer-experimental.md index 733eb1fb93f4..c1f4d257806a 100644 --- a/aspnetcore/release-notes/aspnetcore-11/includes/validation-attributes-no-longer-experimental.md +++ b/aspnetcore/release-notes/aspnetcore-11/includes/validation-attributes-no-longer-experimental.md @@ -1,3 +1,8 @@ ### Validation attributes are no longer experimental -`ValidatableTypeAttribute` and `SkipValidationAttribute` are no longer marked experimental. If you suppressed `ASP0029` to use either attribute, remove the suppression. +The and attributes from the [`Microsoft.Extensions.Validation` NuGet package](https://www.nuget.org/packages/Microsoft.Extensions.Validation) are no longer marked experimental. If you suppressed `ASP0029` to use either attribute, remove the suppression. + +For more information, see the following resources: + +* +* diff --git a/aspnetcore/release-notes/aspnetcore-11/includes/validation-localization.md b/aspnetcore/release-notes/aspnetcore-11/includes/validation-localization.md index aac1c914078c..d9e0ddb7d9bd 100644 --- a/aspnetcore/release-notes/aspnetcore-11/includes/validation-localization.md +++ b/aspnetcore/release-notes/aspnetcore-11/includes/validation-localization.md @@ -19,13 +19,13 @@ public class CustomerModel } ``` -An explicit `ErrorMessage` value, such as `NameRequired` in the preceding example, is the first resource key that localization tries. When an attribute doesn't specify `ErrorMessage`, localization instead tries built-in resource-name conventions from most to least specific: +By default, keys resolve against the resources of the type that declares the validated member. An explicit `ErrorMessage` value, such as `NameRequired` in the preceding example, is the first resource key that localization tries. When an attribute doesn't specify `ErrorMessage`, localization instead tries built-in resource-name conventions from most to least specific: 1. `{DeclaringType}_{MemberName}_{AttributeType}_Error` 1. `{DeclaringType}_{AttributeType}_Error` 1. `{AttributeType}_Error` -For example, a `[Required]` attribute on `CustomerModel.Name` resolves against `CustomerModel_Name_RequiredAttribute_Error`, `CustomerModel_RequiredAttribute_Error`, or the shared `RequiredAttribute_Error` resource. If no resource resolves, validation falls back to the attribute's built-in message. Use `ValidationOptions.LocalizerProvider` to resolve keys from a shared resource file instead: +For example, a `[Required]` attribute on `CustomerModel.Name` resolves against `CustomerModel_Name_RequiredAttribute_Error`, `CustomerModel_RequiredAttribute_Error`, or the shared `RequiredAttribute_Error` resource. This allows the default message of an attribute to be translated once for the whole app. If no resource resolves, validation falls back to the attribute's built-in message. Use `ValidationOptions.LocalizerProvider` to resolve keys from a shared resource file instead: ```csharp builder.Services.AddValidation(options => @@ -34,6 +34,13 @@ builder.Services.AddValidation(options => }); ``` +Localized strings don't have to come from resource files. Registering a custom `IStringLocalizerFactory` switches validation messages to that factory's backing store, such as a database or JSON files. A user-registered factory takes precedence over the default resource file implementation: + +```csharp +builder.Services.AddSingleton(); +builder.Services.AddValidation(); +``` + Attributes that already localize themselves (`ErrorMessageResourceType`, `[Display(ResourceType = ...)]`) bypass the pipeline entirely. A custom attribute that needs to substitute its own values into the message template can implement `IValidationMessageFormatter`: ```csharp @@ -47,3 +54,10 @@ public sealed class DivisibleByAttribute : ValidationAttribute, IValidationMessa ``` The same localization rules apply to validation for minimal APIs and Blazor, so a message localizes identically wherever the model is used. + +Complete feature coverage is available in the following articles: + +* +* + +For more information, see [Add localization support to Microsoft.Extensions.Validation (`dotnet/aspnetcore` #66646)](https://github.com/dotnet/aspnetcore/pull/66646). diff --git a/aspnetcore/release-notes/aspnetcore-5.0.md b/aspnetcore/release-notes/aspnetcore-5.0.md index db0b38cc5ad0..7cdba6c962bc 100644 --- a/aspnetcore/release-notes/aspnetcore-5.0.md +++ b/aspnetcore/release-notes/aspnetcore-5.0.md @@ -141,7 +141,7 @@ Use the `FocusAsync` convenience method on element references to set the UI focu ### Custom validation CSS class attributes -Custom validation CSS class attributes are useful when integrating with CSS frameworks, such as Bootstrap. For more information, see . +Custom validation CSS class attributes are useful when integrating with CSS frameworks, such as Bootstrap. For more information, see . ### IAsyncDisposable support 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 c1e87ac552bd..a3c0ba64d5a7 100644 --- a/aspnetcore/toc.yml +++ b/aspnetcore/toc.yml @@ -862,6 +862,10 @@ items: uid: blazor/forms/binding - name: Validation uid: blazor/forms/validation + - name: Advanced validation + uid: blazor/forms/validation-advanced + - name: Client-side validation (static SSR) + uid: blazor/forms/validation-client-side - name: Troubleshoot uid: blazor/forms/troubleshoot - name: File uploads