From 251f5ddfad6be37378776a93ab421bd7af5528f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Rozto=C4=8Dil?= Date: Mon, 8 Jun 2026 12:02:57 +0200 Subject: [PATCH 01/10] Add Blazor async validation docs --- aspnetcore/blazor/forms/validation.md | 210 ++++++++++++++++++++++++++ 1 file changed, 210 insertions(+) diff --git a/aspnetcore/blazor/forms/validation.md b/aspnetcore/blazor/forms/validation.md index d86e7c21a3ad..d67aab5f5541 100644 --- a/aspnetcore/blazor/forms/validation.md +++ b/aspnetcore/blazor/forms/validation.md @@ -211,6 +211,216 @@ After making the preceding changes, the form's behavior matches the following sp * Any manual validation that you want to perform in the `HandleValidationRequested` method assigned to the form's event executes when the user selects the form's **`Update`** button. In the existing code of the `Starship8` component example, the user must select either or both of the checkboxes to validate the form. * The form doesn't process the `Submit` method until both the data annotations and manual validation pass. +:::moniker range=">= aspnetcore-11.0" + +## Asynchronous validation + + exposes an asynchronous validation pipeline that custom validator components and custom submit handlers can use to run validation work that performs I/O, such as calling a server endpoint to check a value's uniqueness. The pipeline is built around three additions to : + +* : an asynchronous counterpart to that awaits any registered async work and accepts a . +* `EditContext.OnValidationRequestedAsync`: an event for validators that run asynchronous work when the form is validated as a whole (typically on submit). +* `EditContext.AddValidationTask`: a method that registers an in-flight against a so the framework can track per-field async work. + + awaits any registered async work before invoking . Sync-only forms continue to work without changes. The new APIs apply only when a validator or a handler opts in. + +> [!NOTE] +> In Preview 5, the built-in component does not yet route data annotations attributes through the async pipeline. You can adopt the patterns in this section from a custom validator component or from a handler attached directly to . + +### Form-level async validation with `OnValidationRequestedAsync` + +Subscribe to `OnValidationRequestedAsync` to run async work whenever the form is validated as a whole. The event arguments expose a that should be passed to any I/O the handler performs, so the work is cancelled when the framework supersedes the current validation pass. + +In the following example, a custom validator component checks a username against a remote endpoint when the form is submitted: + +```razor +@implements IDisposable +@inject HttpClient Http + +@code { + [CascadingParameter] + private EditContext? CurrentEditContext { get; set; } + + [Parameter, EditorRequired] + public RegistrationModel Model { get; set; } = default!; + + private ValidationMessageStore? _messages; + + protected override void OnInitialized() + { + ArgumentNullException.ThrowIfNull(CurrentEditContext); + _messages = new ValidationMessageStore(CurrentEditContext); + CurrentEditContext.OnValidationRequestedAsync += ValidateUsernameAsync; + } + + private async Task ValidateUsernameAsync( + object? sender, ValidationRequestedEventArgs e) + { + var field = CurrentEditContext!.Field(nameof(Model.Username)); + _messages!.Clear(field); + + var available = await Http.GetFromJsonAsync( + $"api/usernames/available?value={Uri.EscapeDataString(Model.Username)}", + e.CancellationToken); + + if (!available) + { + _messages.Add(field, "The username is already taken."); + } + } + + public void Dispose() + { + if (CurrentEditContext is not null) + { + CurrentEditContext.OnValidationRequestedAsync -= ValidateUsernameAsync; + } + } +} +``` + +Place the component inside an alongside the form's inputs. Because awaits the async handlers before invoking , the submit handler runs only after the remote check completes successfully: + +```razor + + + + + + +``` + +### Per-field async validation with `AddValidationTask` + +For async work that should run when the user edits a single field, register the in-flight against the field's with `AddValidationTask`. The framework tracks each task so the field's pending and faulted state can be queried and visualised independently of other fields. + +The following validator component re-runs the uniqueness check whenever the `Username` field changes. If the user edits the same field again while a check is in flight, the prior task is cancelled and a new one is registered: + +```csharp +private CancellationTokenSource? _usernameCts; + +protected override void OnInitialized() +{ + ArgumentNullException.ThrowIfNull(CurrentEditContext); + _messages = new ValidationMessageStore(CurrentEditContext); + CurrentEditContext.OnFieldChanged += OnFieldChanged; +} + +private void OnFieldChanged(object? sender, FieldChangedEventArgs e) +{ + if (e.FieldIdentifier.FieldName != nameof(RegistrationModel.Username)) + { + return; + } + + _usernameCts?.Cancel(); + _usernameCts = new CancellationTokenSource(); + var token = _usernameCts.Token; + + var task = CheckAsync(e.FieldIdentifier, token); + CurrentEditContext!.AddValidationTask(e.FieldIdentifier, task); +} + +private async Task CheckAsync(FieldIdentifier field, CancellationToken token) +{ + _messages!.Clear(field); + + var available = await Http.GetFromJsonAsync( + $"api/usernames/available?value={Uri.EscapeDataString(Model.Username)}", + token); + + if (!available) + { + _messages.Add(field, "The username is already taken."); + } +} +``` + +A cancelled task is discarded silently and does not change the field's faulted state. A task that throws an exception other than places the field in the faulted state described in the next section. + +### Pending and faulted state + +While an async task is in flight, the field is *pending*. If an async task throws an exception other than , the field is *faulted*. Each state has both a per-field and a form-level query: + +| State | Per-field | Form-level (any field) | +|----------|----------------------------------------------------|----------------------------------| +| Pending | `EditContext.IsValidationPending(fieldIdentifier)` | `EditContext.IsValidationPending()` | +| Faulted | `EditContext.IsValidationFaulted(fieldIdentifier)` | `EditContext.IsValidationFaulted()` | + +The per-field overloads accept either a or a `() => model.Property` lambda for ergonomic use in Razor markup: + +```razor + + + +@if (EditContext.IsValidationPending(() => Model.Username)) +{ + Checking… +} +else if (EditContext.IsValidationFaulted(() => Model.Username)) +{ + + Validation could not be completed. + +} +``` + +The form-level parameterless overloads return `true` when any field is currently pending or faulted. A common use is disabling the submit button while validation is in flight: + +```razor + +``` + + automatically adds the `pending` and `faulted` CSS classes to its rendered element while the bound field is in the corresponding state, in addition to the existing `modified` / `valid` / `invalid` classes. The classes compose, so unmodified pending styling and modified pending styling can be targeted independently: + +```css +.pending { + background-image: url('spinner.gif'); + background-repeat: no-repeat; + background-position: right center; +} + +.modified.faulted { + border-color: orange; +} +``` + +### Calling `ValidateAsync` from a custom submit handler + +When a form uses instead of , call from the handler to await any registered async work before deciding whether to proceed: + +```razor + + + + + + +@code { + private EditContext _editContext = default!; + + protected override void OnInitialized() => + _editContext = new EditContext(Model); + + private async Task HandleSubmitAsync() + { + if (await _editContext.ValidateAsync(CancellationToken.None)) + { + await RegisterAsync(); + } + } +} +``` + +The synchronous method continues to work for forms that only have synchronous validators. When a registered async handler returns an incomplete , calling throws an directing the caller to use instead. + +### Async validation across rendering modes + +The async validation API is the same in every Blazor rendering mode. Validator code runs wherever the component runs: in the browser on Interactive WebAssembly, on the server over the connection on Interactive Server, and on the server during the form POST for static SSR. Static SSR renders the full response after async validation completes. + +:::moniker-end + ## Validator components Validator components support form validation by managing a for a form's . From 296f0b6f0ecbb7f0c824bf2ebb7e9e29a8e2f58a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Rozto=C4=8Dil?= Date: Mon, 8 Jun 2026 12:50:10 +0200 Subject: [PATCH 02/10] Add Blazor SSR client-side validation docs --- aspnetcore/blazor/forms/index.md | 31 ++++++++++- aspnetcore/blazor/forms/validation.md | 74 ++++++++++++++++++++++++++- 2 files changed, 103 insertions(+), 2 deletions(-) diff --git a/aspnetcore/blazor/forms/index.md b/aspnetcore/blazor/forms/index.md index 32f45fe7b295..c5268afb2317 100644 --- a/aspnetcore/blazor/forms/index.md +++ b/aspnetcore/blazor/forms/index.md @@ -34,6 +34,12 @@ The Blazor framework supports forms and provides built-in input components: :::moniker-end +:::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 . + +:::moniker-end + > [!NOTE] > Unsupported ASP.NET Core validation features are covered in the [Unsupported validation features](#unsupported-validation-features) section. @@ -428,7 +434,7 @@ To demonstrate how forms work with [data annotations](xref:mvc/models/validation Form examples reference aspects of the [Star Trek](http://www.startrek.com/) universe. Star Trek is a copyright ©1966-2023 of [CBS Studios](https://www.paramount.com/brand/cbs-studios) and [Paramount](https://www.paramount.com). -:::moniker range=">= aspnetcore-8.0" +:::moniker range=">= aspnetcore-8.0 < aspnetcore-11.0" ## Client-side validation requires a circuit @@ -436,12 +442,22 @@ In Blazor Web Apps, client-side validation requires an active Blazor SignalR cir :::moniker-end +:::moniker range=">= aspnetcore-11.0" + +## 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 . + +:::moniker-end + ## Unsupported validation features All of the [data annotation built-in validators](xref:mvc/models/validation#built-in-attributes) are supported in Blazor except for the [`[Remote]` validation attribute](xref:mvc/models/validation#remote-attribute). jQuery validation isn't supported in Razor components. We recommend any of the following approaches: +:::moniker range="< aspnetcore-11.0" + * Follow the guidance in for either: * Server-side validation in a Blazor Web App that adopts an interactive render mode. * Client-side validation in a standalone Blazor Web Assembly app. @@ -452,6 +468,19 @@ jQuery validation isn't supported in Razor components. We recommend any of the f For statically-rendered forms on the server, a new mechanism for client-side validation is under consideration. For more information, see [Create server rendered forms with client validation using Blazor without a circuit (`dotnet/aspnetcore` #51040)](https://github.com/dotnet/aspnetcore/issues/51040). +:::moniker-end + +:::moniker range=">= aspnetcore-11.0" + +* Follow the guidance in for either: + * Server-side validation in a Blazor Web App that adopts an interactive render mode. + * Client-side validation in [Blazor SSR forms](xref:blazor/forms/validation#client-side-validation-in-blazor-ssr-forms). + * 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. + +:::moniker-end + ## Additional resources :::moniker range=">= aspnetcore-8.0" diff --git a/aspnetcore/blazor/forms/validation.md b/aspnetcore/blazor/forms/validation.md index d67aab5f5541..b1bec38a5076 100644 --- a/aspnetcore/blazor/forms/validation.md +++ b/aspnetcore/blazor/forms/validation.md @@ -20,12 +20,18 @@ In basic form validation scenarios, an 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 the [Client-side validation in Blazor SSR forms](#client-side-validation-in-blazor-ssr-forms) section. + +:::moniker-end + In the following component, the `HandleValidationRequested` handler method clears any existing validation messages by calling before validating the form. `Starship8.razor`: @@ -159,6 +165,72 @@ There are two general approaches for achieving custom validation, which are desc * [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 validation). +:::moniker range=">= aspnetcore-11.0" + +## Client-side validation in Blazor SSR forms + +When a Blazor static server-side rendered (static SSR) form contains a component, Blazor automatically validates the form in the browser before the form is submitted. Server-side data annotations validation continues to run after the form is posted, so the client-side check supplements but never replaces the server-side check. + +Client-side validation activates automatically when both conditions are met: + +* The form's hosting component uses static SSR (no `@rendermode` directive applied to the component). +* The form contains a component. + +### Supported validation attributes + +The following attributes are enforced client-side, matching the server-side data annotations behavior: + +* +* +* +* +* +* +* +* +* +* +* +* + +Validation attributes that don't appear in this list, including custom -derived attributes, aren't enforced client-side. They continue to run server-side after the form is submitted. + +### Validation timing + +A field validates when it loses focus (blur) for the first time. After a field has shown a validation error or after the form has been submitted at least once, the field re-validates on every change so corrections appear immediately. Submitting the form validates every field. + +### Validation messages and accessibility + +The existing and components display client-side validation errors without any changes. ARIA attributes on input elements and on the validation message containers are managed by Blazor automatically so that assistive technologies announce validation errors without additional configuration. + +### CSS framework integration + +Client-side validation marks invalid inputs through 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 the current state of each input. + +### Enhanced navigation + +Client-side validation is preserved across [enhanced navigation](xref:blazor/fundamentals/navigation#enhanced-navigation-and-form-handling). When the user navigates to a page that contains an SSR form, the form is wired up automatically. Multiple forms on the same page validate independently of each other. + +### Localized validation messages + +When validation localization is configured through `Microsoft.Extensions.Validation`, error messages are localized at server-render time before being included in the page, so the client-side experience uses the same localized strings as the server-side experience. For more information, see the [Localizing validation messages](#localizing-validation-messages) section. + +### Opting out + +To keep server-side data annotations validation but disable client-side enforcement for a single form, set the component's `EnableClientValidation` parameter to `false`: + +```razor + +``` + +To bypass client-side validation for a single submit button, use the standard HTML `formnovalidate` attribute on the button. The form is then posted without a client-side check, and server-side validation still runs after the post: + +```razor + +``` + +:::moniker-end + ## Manual validation using the `OnValidationRequested` event You can manually validate a form with a custom event handler assigned to the event to manage a . From 72f2a07561370fc0fbe43908eb545b34fe70cd67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Rozto=C4=8Dil?= Date: Mon, 8 Jun 2026 14:46:44 +0200 Subject: [PATCH 03/10] Add Blazor validation localization docs --- aspnetcore/blazor/forms/validation.md | 2 +- .../blazor/globalization-localization.md | 13 +++ .../localization/make-content-localizable.md | 97 +++++++++++++++++++ aspnetcore/fundamentals/minimal-apis.md | 20 ++++ 4 files changed, 131 insertions(+), 1 deletion(-) diff --git a/aspnetcore/blazor/forms/validation.md b/aspnetcore/blazor/forms/validation.md index b1bec38a5076..f85e1487371a 100644 --- a/aspnetcore/blazor/forms/validation.md +++ b/aspnetcore/blazor/forms/validation.md @@ -213,7 +213,7 @@ Client-side validation is preserved across [enhanced navigation](xref:blazor/fun ### Localized validation messages -When validation localization is configured through `Microsoft.Extensions.Validation`, error messages are localized at server-render time before being included in the page, so the client-side experience uses the same localized strings as the server-side experience. For more information, see the [Localizing validation messages](#localizing-validation-messages) section. +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 . ### Opting out diff --git a/aspnetcore/blazor/globalization-localization.md b/aspnetcore/blazor/globalization-localization.md index 35cea2b43cae..6d5532e9c523 100644 --- a/aspnetcore/blazor/globalization-localization.md +++ b/aspnetcore/blazor/globalization-localization.md @@ -24,8 +24,21 @@ A limited set of ASP.NET Core's localization features are supported: Not supported: and are ASP.NET Core MVC features and *not supported* in Blazor apps. +:::moniker range="< aspnetcore-11.0" + For Blazor apps, localized validation messages for [forms validation using data annotations]() is supported if and are implemented. +:::moniker-end + +:::moniker range=">= aspnetcore-11.0" + +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.Localization` package, which resolves validation messages and display names through . Available for Blazor apps that enable the new validation pipeline using `AddValidation()`. For details, see . + +:::moniker-end + This article describes how to use Blazor's globalization and localization features based on: * The [`Accept-Language` header](https://developer.mozilla.org/docs/Web/HTTP/Headers/Accept-Language), which is set by the browser based on a user's language preferences in browser settings. diff --git a/aspnetcore/fundamentals/localization/make-content-localizable.md b/aspnetcore/fundamentals/localization/make-content-localizable.md index 471dedca6f22..be3de459e82b 100644 --- a/aspnetcore/fundamentals/localization/make-content-localizable.md +++ b/aspnetcore/fundamentals/localization/make-content-localizable.md @@ -110,6 +110,103 @@ The following code shows how to use one resource string for validation attribute 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 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`. Localize validation error messages and the display names of validated properties and parameters by also calling `AddValidationLocalization`: + +```csharp +builder.Services.AddValidation(); +builder.Services.AddValidationLocalization(); +``` + +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.Localization` package, which builds on the `Microsoft.Extensions.Validation` package. Both packages are included in the Web SDK (`Microsoft.NET.Sdk.Web`) and the Razor SDK (`Microsoft.NET.Sdk.Razor`), so ASP.NET Core and Blazor apps don't need explicit package references. Standalone Blazor WebAssembly apps and class libraries based on `Microsoft.NET.Sdk` reference both packages 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, pass a marker type argument to `AddValidationLocalization`: + +```csharp +builder.Services.AddValidationLocalization(); +``` + +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. + +For per-type resource file resolution, use the non-generic overload of `AddValidationLocalization`: + +```csharp +builder.Services.AddLocalization(options => options.ResourcesPath = "Resources"); +builder.Services.AddValidationLocalization(); +``` + +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 `ValidationLocalizationOptions.LocalizerProvider`. The delegate receives the validated type and an , and returns the to use: + +```csharp +builder.Services.AddValidationLocalization(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(); +builder.Services.AddValidationLocalization(); +``` + +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 registered by `AddValidationLocalization`. + +### 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. This can be achieved by an `ErrorMessageKeyProvider` that derives a resource key programmatically: + +```csharp +builder.Services.AddValidationLocalization(options => +{ + options.ErrorMessageKeyProvider = ctx => ctx.Attribute.ErrorMessage is not null + ? ctx.Attribute.ErrorMessage + : $"{ctx.Attribute.GetType().Name}_Error"; +}); +``` + +With the preceding configuration, a `[Required]` attribute with no `ErrorMessage` looks up the resource key `RequiredAttribute_Error`, a `[StringLength(50)]` looks up `StringLengthAttribute_Error`, and so on. The key provider runs only when `ErrorMessage` isn't set on the attribute instance, so model-specific overrides via `ErrorMessage = "MyKey"` continue to take precedence. + +:::moniker-end + ## Configure localization services Localization services are configured in `Program.cs`: diff --git a/aspnetcore/fundamentals/minimal-apis.md b/aspnetcore/fundamentals/minimal-apis.md index c7e516f6433a..4e9e022f3af1 100644 --- a/aspnetcore/fundamentals/minimal-apis.md +++ b/aspnetcore/fundamentals/minimal-apis.md @@ -132,6 +132,26 @@ To implement custom validation error responses: For more information on customizing validation error responses with IProblemDetailsService, see . +:::moniker range=">= aspnetcore-11.0" + +### Localizing validation messages + +Validation error messages and display names can be localized through the `Microsoft.Extensions.Validation.Localization` package, which is included in the Web SDK (`Microsoft.NET.Sdk.Web`) and doesn't require an explicit package reference in Minimal API projects. + +Register the validation pipeline, the standard ASP.NET Core localization services, and the validation localization integration in `Program.cs`: + +```csharp +builder.Services.AddLocalization(options => options.ResourcesPath = "Resources"); +builder.Services.AddValidation(); +builder.Services.AddValidationLocalization(); +``` + +Use the typed `AddValidationLocalization()` overload 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 typed overload supplies one explicitly. A shared resource file resolves messages and display names against one *.resx* file (`Resources/ValidationResources.fr.resx`, and so on). + +For the full set of options, including loading messages from sources other than resource files, see . + +:::moniker-end + ## Responses Route handlers support the following types of return values: From 421802601dd65c27a07fdaa7f6370b610b78d2fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Rozto=C4=8Dil?= Date: Mon, 8 Jun 2026 15:20:12 +0200 Subject: [PATCH 04/10] Improve async validation docs --- aspnetcore/blazor/forms/validation.md | 28 ++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/aspnetcore/blazor/forms/validation.md b/aspnetcore/blazor/forms/validation.md index f85e1487371a..a9f78062a608 100644 --- a/aspnetcore/blazor/forms/validation.md +++ b/aspnetcore/blazor/forms/validation.md @@ -293,14 +293,14 @@ After making the preceding changes, the form's behavior matches the following sp * `EditContext.OnValidationRequestedAsync`: an event for validators that run asynchronous work when the form is validated as a whole (typically on submit). * `EditContext.AddValidationTask`: a method that registers an in-flight against a so the framework can track per-field async work. - awaits any registered async work before invoking . Sync-only forms continue to work without changes. The new APIs apply only when a validator or a handler opts in. + awaits any registered async work before invoking . Sync-only forms continue to work without changes. > [!NOTE] > In Preview 5, the built-in component does not yet route data annotations attributes through the async pipeline. You can adopt the patterns in this section from a custom validator component or from a handler attached directly to . ### Form-level async validation with `OnValidationRequestedAsync` -Subscribe to `OnValidationRequestedAsync` to run async work whenever the form is validated as a whole. The event arguments expose a that should be passed to any I/O the handler performs, so the work is cancelled when the framework supersedes the current validation pass. +Subscribe to `OnValidationRequestedAsync` to run async work whenever the form is validated as a whole. The event arguments expose a that should be passed to any I/O the handler performs, so the work is cancelled when the framework supersedes the current validation pass (for example, when a new validation pass starts before the previous one completes). In the following example, a custom validator component checks a username against a remote endpoint when the form is submitted: @@ -363,9 +363,9 @@ Place the component inside an against the field's with `AddValidationTask`. The framework tracks each task so the field's pending and faulted state can be queried and visualised independently of other fields. +For async work that should run when the user edits a single field, register the in-flight against the field's with `AddValidationTask`. The framework tracks each task so the field's pending and faulted state can be queried and visualized independently of other fields. -The following validator component re-runs the uniqueness check whenever the `Username` field changes. If the user edits the same field again while a check is in flight, the prior task is cancelled and a new one is registered: +Add the following members to the validator component shown in the previous section to re-run the uniqueness check whenever the `Username` field changes. If the user edits the same field again while a check is in flight, the prior task is canceled and a new one is registered: ```csharp private CancellationTokenSource? _usernameCts; @@ -374,6 +374,7 @@ protected override void OnInitialized() { ArgumentNullException.ThrowIfNull(CurrentEditContext); _messages = new ValidationMessageStore(CurrentEditContext); + CurrentEditContext.OnValidationRequestedAsync += ValidateUsernameAsync; CurrentEditContext.OnFieldChanged += OnFieldChanged; } @@ -405,9 +406,18 @@ private async Task CheckAsync(FieldIdentifier field, CancellationToken token) _messages.Add(field, "The username is already taken."); } } + +public void Dispose() +{ + if (CurrentEditContext is not null) + { + CurrentEditContext.OnValidationRequestedAsync -= ValidateUsernameAsync; + CurrentEditContext.OnFieldChanged -= OnFieldChanged; + } +} ``` -A cancelled 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. +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 @@ -418,7 +428,7 @@ While an async task is in flight, the field is *pending*. If an async task throw | Pending | `EditContext.IsValidationPending(fieldIdentifier)` | `EditContext.IsValidationPending()` | | Faulted | `EditContext.IsValidationFaulted(fieldIdentifier)` | `EditContext.IsValidationFaulted()` | -The per-field overloads accept either a or a `() => model.Property` lambda for ergonomic use in Razor markup: +The per-field overloads accept either a or a `() => model.Property` lambda for convenient use in Razor markup: ```razor @@ -453,6 +463,10 @@ The form-level parameterless overloads return `true` when any field is currently background-position: right center; } +.modified.pending { + border-color: lightblue; +} + .modified.faulted { border-color: orange; } @@ -489,7 +503,7 @@ The synchronous Date: Mon, 8 Jun 2026 15:29:17 +0200 Subject: [PATCH 05/10] Improve client-side validation docs --- aspnetcore/blazor/forms/index.md | 6 +++--- aspnetcore/blazor/forms/validation.md | 16 ++++++++-------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/aspnetcore/blazor/forms/index.md b/aspnetcore/blazor/forms/index.md index c5268afb2317..45cb11881e86 100644 --- a/aspnetcore/blazor/forms/index.md +++ b/aspnetcore/blazor/forms/index.md @@ -36,7 +36,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 @@ -446,7 +446,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 @@ -474,7 +474,7 @@ For statically-rendered forms on the server, a new mechanism for client-side val * Follow the guidance in for either: * Server-side validation in a Blazor Web App that adopts an interactive render mode. - * Client-side validation in [Blazor SSR forms](xref:blazor/forms/validation#client-side-validation-in-blazor-ssr-forms). + * Client-side validation in [Blazor SSR forms](xref:blazor/forms/validation#client-side-validation-in-static-ssr-forms). * 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.md b/aspnetcore/blazor/forms/validation.md index a9f78062a608..4f12ce4a2854 100644 --- a/aspnetcore/blazor/forms/validation.md +++ b/aspnetcore/blazor/forms/validation.md @@ -28,7 +28,7 @@ In Blazor Web Apps, client-side validation requires an active Blazor SignalR cir :::moniker range=">= aspnetcore-11.0" -In Blazor Web Apps that use interactive render modes (Server, WebAssembly, or Auto), client-side validation runs through the live pipeline as in earlier releases. Forms that adopt static server-side rendering (static SSR) gain client-side validation automatically when a component is present in the form. For details, see the [Client-side validation in Blazor SSR forms](#client-side-validation-in-blazor-ssr-forms) section. +In Blazor Web Apps that use interactive render modes (Server, WebAssembly, or Auto), client-side validation runs through the live pipeline as in earlier releases. Forms that adopt static server-side rendering (static SSR) gain client-side validation automatically when a component is present in the form. For details, see . :::moniker-end @@ -167,9 +167,9 @@ There are two general approaches for achieving custom validation, which are desc :::moniker range=">= aspnetcore-11.0" -## Client-side validation in Blazor SSR forms +## Client-side validation in static SSR forms -When a Blazor static server-side rendered (static SSR) form 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. +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: @@ -203,18 +203,18 @@ A field validates when it loses focus (blur) for the first time. After a field h The existing and components display client-side validation errors without any changes. ARIA attributes on input elements and on the validation message containers are managed by Blazor automatically so that assistive technologies announce validation errors without additional configuration. +### Localized validation messages + +When validation localization is configured through `Microsoft.Extensions.Validation`, error messages are localized at server-render time before being included in the page, so the client-side validation shows the same localized strings as the server-side experience. For more information, see . + ### CSS framework integration -Client-side validation marks invalid inputs through 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 the current state of each input. +Client-side validation integrates with the browser's [Constraint Validation API](https://developer.mozilla.org/docs/Web/API/Constraint_validation), so the standard CSS pseudo-classes `:valid` and `:invalid` reflect each input's current validation state. ### Enhanced navigation Client-side validation is preserved across [enhanced navigation](xref:blazor/fundamentals/navigation#enhanced-navigation-and-form-handling). When the user navigates to a page that contains an SSR form, the form is wired up automatically. Multiple forms on the same page validate independently of each other. -### Localized validation messages - -When validation localization is configured through `Microsoft.Extensions.Validation`, error messages are localized at server-render time before being included in the page, so the client-side validation shows the same localized strings as the server-side experience. For more information, see . - ### Opting out To keep server-side data annotations validation but disable client-side enforcement for a single form, set the component's `EnableClientValidation` parameter to `false`: From 30d269bb1f30a5027e9a15401fcd46144aa24ae6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Rozto=C4=8Dil?= Date: Tue, 9 Jun 2026 02:19:22 +0200 Subject: [PATCH 06/10] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Co-authored-by: Ondřej Roztočil --- aspnetcore/blazor/forms/index.md | 2 +- aspnetcore/blazor/forms/validation.md | 9 +++++++++ aspnetcore/blazor/globalization-localization.md | 2 +- .../localization/make-content-localizable.md | 2 +- 4 files changed, 12 insertions(+), 3 deletions(-) diff --git a/aspnetcore/blazor/forms/index.md b/aspnetcore/blazor/forms/index.md index 45cb11881e86..a644f0b9eaf4 100644 --- a/aspnetcore/blazor/forms/index.md +++ b/aspnetcore/blazor/forms/index.md @@ -460,7 +460,7 @@ jQuery validation isn't supported in Razor components. We recommend any of the f * Follow the guidance in for either: * Server-side validation in a Blazor Web App that adopts an interactive render mode. - * Client-side validation in a standalone Blazor Web Assembly app. + * 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.md b/aspnetcore/blazor/forms/validation.md index 4f12ce4a2854..d4b47591e846 100644 --- a/aspnetcore/blazor/forms/validation.md +++ b/aspnetcore/blazor/forms/validation.md @@ -338,6 +338,8 @@ In the following example, a custom validator component checks a username against { _messages.Add(field, "The username is already taken."); } + + CurrentEditContext!.NotifyValidationStateChanged(); } public void Dispose() @@ -386,6 +388,7 @@ private void OnFieldChanged(object? sender, FieldChangedEventArgs e) } _usernameCts?.Cancel(); + _usernameCts?.Dispose(); _usernameCts = new CancellationTokenSource(); var token = _usernameCts.Token; @@ -405,10 +408,16 @@ private async Task CheckAsync(FieldIdentifier field, CancellationToken token) { _messages.Add(field, "The username is already taken."); } + + CurrentEditContext!.NotifyValidationStateChanged(); } public void Dispose() { + _usernameCts?.Cancel(); + _usernameCts?.Dispose(); + _usernameCts = null; + if (CurrentEditContext is not null) { CurrentEditContext.OnValidationRequestedAsync -= ValidateUsernameAsync; diff --git a/aspnetcore/blazor/globalization-localization.md b/aspnetcore/blazor/globalization-localization.md index 6d5532e9c523..ea7ae4a8493e 100644 --- a/aspnetcore/blazor/globalization-localization.md +++ b/aspnetcore/blazor/globalization-localization.md @@ -26,7 +26,7 @@ A limited set of ASP.NET Core's localization features are supported: :::moniker range="< aspnetcore-11.0" -For Blazor apps, localized validation messages for [forms validation using data annotations]() is supported if and are implemented. +For Blazor apps, localization of validation messages for [forms validation using data annotations]() is supported if and are implemented. :::moniker-end diff --git a/aspnetcore/fundamentals/localization/make-content-localizable.md b/aspnetcore/fundamentals/localization/make-content-localizable.md index be3de459e82b..b81b6a2e1d0c 100644 --- a/aspnetcore/fundamentals/localization/make-content-localizable.md +++ b/aspnetcore/fundamentals/localization/make-content-localizable.md @@ -124,7 +124,7 @@ builder.Services.AddValidationLocalization(); 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.Localization` package, which builds on the `Microsoft.Extensions.Validation` package. Both packages are included in the Web SDK (`Microsoft.NET.Sdk.Web`) and the Razor SDK (`Microsoft.NET.Sdk.Razor`), so ASP.NET Core and Blazor apps don't need explicit package references. Standalone Blazor WebAssembly apps and class libraries based on `Microsoft.NET.Sdk` reference both packages explicitly: + > The integration is provided by the `Microsoft.Extensions.Validation.Localization` package, which builds on the `Microsoft.Extensions.Validation` package. Both packages are 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 explicit package references. Standalone Blazor WebAssembly apps and other project that do not use the Web SDK or the Razor SDK must reference both packages explicitly: > > ```xml > From f7452819ddf38965487c6f5b02148ae508e005ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Rozto=C4=8Dil?= Date: Tue, 9 Jun 2026 02:23:25 +0200 Subject: [PATCH 07/10] Update ms.date --- aspnetcore/fundamentals/minimal-apis.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aspnetcore/fundamentals/minimal-apis.md b/aspnetcore/fundamentals/minimal-apis.md index 4e9e022f3af1..abbce34ba069 100644 --- a/aspnetcore/fundamentals/minimal-apis.md +++ b/aspnetcore/fundamentals/minimal-apis.md @@ -5,7 +5,7 @@ description: Provides an overview of Minimal APIs in ASP.NET Core ms.author: wpickett content_well_notification: AI-contribution monikerRange: '>= aspnetcore-6.0' -ms.date: 03/17/2026 +ms.date: 06/09/2026 uid: fundamentals/minimal-apis ai-usage: ai-assisted --- From 130ef620f0e7c24bfe63c9db788dd14daac832d4 Mon Sep 17 00:00:00 2001 From: Luke Latham <1622880+guardrex@users.noreply.github.com> Date: Thu, 11 Jun 2026 00:10:54 -0400 Subject: [PATCH 08/10] Apply suggestions from code review Co-authored-by: Luke Latham <1622880+guardrex@users.noreply.github.com> --- aspnetcore/blazor/forms/validation.md | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/aspnetcore/blazor/forms/validation.md b/aspnetcore/blazor/forms/validation.md index d4b47591e846..ca1904fac955 100644 --- a/aspnetcore/blazor/forms/validation.md +++ b/aspnetcore/blazor/forms/validation.md @@ -289,7 +289,13 @@ After making the preceding changes, the form's behavior matches the following sp 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 three additions to : -* : an asynchronous counterpart to that awaits any registered async work and accepts a . + + +* `Microsoft.AspNetCore.Components.Forms.EditContext.ValidateAsync`: an asynchronous counterpart to that awaits any registered async work and accepts a . * `EditContext.OnValidationRequestedAsync`: an event for validators that run asynchronous work when the form is validated as a whole (typically on submit). * `EditContext.AddValidationTask`: a method that registers an in-flight against a so the framework can track per-field async work. @@ -483,7 +489,13 @@ The form-level parameterless overloads return `true` when any field is currently ### Calling `ValidateAsync` from a custom submit handler -When a form uses instead of , call from the handler to await any registered async work before deciding whether to proceed: + + +When a form uses instead of , call `Microsoft.AspNetCore.Components.Forms.EditContext.ValidateAsync` from the handler to await any registered async work before deciding whether to proceed: ```razor @@ -508,7 +520,13 @@ When a form uses } ``` -The synchronous method continues to work for forms that only have synchronous validators. When a registered async handler returns an incomplete , calling throws an directing the caller to use instead. + + +The synchronous method continues to work for forms that only have synchronous validators. When a registered async handler returns an incomplete , calling throws an directing the caller to use `Microsoft.AspNetCore.Components.Forms.EditContext.ValidateAsync` instead. ### Async validation across rendering modes From 2d2556a4478db6d3156d4130c5eea8b650230536 Mon Sep 17 00:00:00 2001 From: Luke Latham <1622880+guardrex@users.noreply.github.com> Date: Thu, 11 Jun 2026 00:12:35 -0400 Subject: [PATCH 09/10] Update make-content-localizable.md --- .../localization/make-content-localizable.md | 494 +++++++++--------- 1 file changed, 249 insertions(+), 245 deletions(-) diff --git a/aspnetcore/fundamentals/localization/make-content-localizable.md b/aspnetcore/fundamentals/localization/make-content-localizable.md index b81b6a2e1d0c..c824076e4e73 100644 --- a/aspnetcore/fundamentals/localization/make-content-localizable.md +++ b/aspnetcore/fundamentals/localization/make-content-localizable.md @@ -1,246 +1,250 @@ ---- -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 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`. Localize validation error messages and the display names of validated properties and parameters by also calling `AddValidationLocalization`: - -```csharp -builder.Services.AddValidation(); -builder.Services.AddValidationLocalization(); -``` - -The localization integration does not apply to MVC and Razor Pages apps, or to Blazor forms that don't include `AddValidation`. - -> [!NOTE] +--- +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`. Localize validation error messages and the display names of validated properties and parameters by also calling `AddValidationLocalization`: + +```csharp +builder.Services.AddValidation(); +builder.Services.AddValidationLocalization(); +``` + +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.Localization` package, which builds on the `Microsoft.Extensions.Validation` package. Both packages are 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 explicit package references. Standalone Blazor WebAssembly apps and other project that do not use the Web SDK or the Razor SDK must reference both packages 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, pass a marker type argument to `AddValidationLocalization`: - -```csharp -builder.Services.AddValidationLocalization(); -``` - -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. - -For per-type resource file resolution, use the non-generic overload of `AddValidationLocalization`: - -```csharp -builder.Services.AddLocalization(options => options.ResourcesPath = "Resources"); -builder.Services.AddValidationLocalization(); -``` - -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 `ValidationLocalizationOptions.LocalizerProvider`. The delegate receives the validated type and an , and returns the to use: - -```csharp -builder.Services.AddValidationLocalization(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(); -builder.Services.AddValidationLocalization(); -``` - -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 registered by `AddValidationLocalization`. - -### 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. This can be achieved by an `ErrorMessageKeyProvider` that derives a resource key programmatically: - -```csharp -builder.Services.AddValidationLocalization(options => -{ - options.ErrorMessageKeyProvider = ctx => ctx.Attribute.ErrorMessage is not null - ? ctx.Attribute.ErrorMessage - : $"{ctx.Attribute.GetType().Name}_Error"; -}); -``` - -With the preceding configuration, a `[Required]` attribute with no `ErrorMessage` looks up the resource key `RequiredAttribute_Error`, a `[StringLength(50)]` looks up `StringLengthAttribute_Error`, and so on. The key provider runs only when `ErrorMessage` isn't set on the attribute instance, so model-specific overrides via `ErrorMessage = "MyKey"` continue to take precedence. - -:::moniker-end - -## 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)] +> +> ```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, pass a marker type argument to `AddValidationLocalization`: + +```csharp +builder.Services.AddValidationLocalization(); +``` + +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. + +For per-type resource file resolution, use the non-generic overload of `AddValidationLocalization`: + +```csharp +builder.Services.AddLocalization(options => options.ResourcesPath = "Resources"); +builder.Services.AddValidationLocalization(); +``` + +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 `ValidationLocalizationOptions.LocalizerProvider`. The delegate receives the validated type and an , and returns the to use: + +```csharp +builder.Services.AddValidationLocalization(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(); +builder.Services.AddValidationLocalization(); +``` + +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 registered by `AddValidationLocalization`. + +### 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. This can be achieved by an `ErrorMessageKeyProvider` that derives a resource key programmatically: + +```csharp +builder.Services.AddValidationLocalization(options => +{ + options.ErrorMessageKeyProvider = ctx => ctx.Attribute.ErrorMessage is not null + ? ctx.Attribute.ErrorMessage + : $"{ctx.Attribute.GetType().Name}_Error"; +}); +``` + +With the preceding configuration, a `[Required]` attribute with no `ErrorMessage` looks up the resource key `RequiredAttribute_Error`, a `[StringLength(50)]` looks up `StringLengthAttribute_Error`, and so on. The key provider runs 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)] From 4fcdcaaea61eb25abb396e4c1bc573961086418e Mon Sep 17 00:00:00 2001 From: Luke Latham <1622880+guardrex@users.noreply.github.com> Date: Thu, 11 Jun 2026 00:14:42 -0400 Subject: [PATCH 10/10] Update minimal-apis.md --- aspnetcore/fundamentals/minimal-apis.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/aspnetcore/fundamentals/minimal-apis.md b/aspnetcore/fundamentals/minimal-apis.md index abbce34ba069..ba44f7fb59bb 100644 --- a/aspnetcore/fundamentals/minimal-apis.md +++ b/aspnetcore/fundamentals/minimal-apis.md @@ -5,7 +5,7 @@ description: Provides an overview of Minimal APIs in ASP.NET Core ms.author: wpickett content_well_notification: AI-contribution monikerRange: '>= aspnetcore-6.0' -ms.date: 06/09/2026 +ms.date: 06/11/2026 uid: fundamentals/minimal-apis ai-usage: ai-assisted --- @@ -132,6 +132,8 @@ To implement custom validation error responses: For more information on customizing validation error responses with IProblemDetailsService, see . +:::moniker-end + :::moniker range=">= aspnetcore-11.0" ### Localizing validation messages @@ -152,6 +154,8 @@ For the full set of options, including loading messages from sources other than :::moniker-end +:::moniker range=">= aspnetcore-10.0" + ## Responses Route handlers support the following types of return values: