From f6850f6bc5fb0e5cfa67d431ea5c1a398d171d08 Mon Sep 17 00:00:00 2001 From: guardrex <1622880+guardrex@users.noreply.github.com> Date: Wed, 6 May 2026 13:52:00 -0400 Subject: [PATCH 1/5] Server validation with a validator component upgrade --- aspnetcore/blazor/forms/validation.md | 574 ++++++++++++++++++++------ 1 file changed, 451 insertions(+), 123 deletions(-) diff --git a/aspnetcore/blazor/forms/validation.md b/aspnetcore/blazor/forms/validation.md index 7ade18ddff30..d1397dd3abe1 100644 --- a/aspnetcore/blazor/forms/validation.md +++ b/aspnetcore/blazor/forms/validation.md @@ -359,124 +359,353 @@ When validation messages are set in the component, they're added to the validato ## Server validation with a validator component -:::moniker range=">= aspnetcore-8.0" - -*This section is focused on Blazor Web App scenarios, but the approach for any type of app that uses server validation with web API adopts the same general approach.* - -:::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 validation with web API adopts the same general approach.* +:::moniker range=">= aspnetcore-10.0" -:::moniker-end +*This section is focused on Blazor Web App scenarios, but the approach for any type of app that uses server validation with web API adopts the same general approach. The approach in this section is based on a [Blazor Web App (Interactive Auto render mode) app hosted in Entra with Microsoft Azure packages](xref:blazor/security/blazor-web-app-entra).* Server validation is supported in addition to client 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. +* The server API includes custom validation logic supplied by the developer. * 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: -:::moniker range=">= aspnetcore-8.0" - * 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. +In the main project of the Blazor Web App, a Minimal API is added to validate 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 because the upcoming form doesn't perform the same validation client-side when the form is submitted to the server. Server 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 obviously can't be sent to the client for client validation. + +> [!NOTE] +> The Minimal 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/) + +Place the `Starship` model (`Starship.cs`) into a `Starship` folder in the `.Client` project. 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`). +Add an interface for a form validation service to the `.Client` project. -:::moniker-end +`Starship/IFormValidator.cs`: -:::moniker range="< aspnetcore-8.0" +```csharp +namespace BlazorSample.Client.Starship; -* 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. +public interface IFormValidator +{ + Task>> ValidateStarshipFormAsync(StarshipModel starship); +} +``` -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. +Create a server form validator that implements the preceding interface in the Blazor Web App. -[!INCLUDE[](~/includes/package-reference.md)] +`Starship/ServerFormValidator.cs`: -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`). +```csharp +using BlazorSample.Client.Starship; +using Microsoft.Identity.Abstractions; -:::moniker-end +namespace BlazorSample.Server.Starship; -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. Server 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 obviously can't be sent to the client for client validation. +internal sealed class ServerFormValidator(IDownstreamApi downstreamApi) : IFormValidator +{ + public async Task>> ValidateStarshipFormAsync(StarshipModel starship) + { + var validationErrors = await downstreamApi.CallApiForUserAsync>>( + "DownstreamApi", starship, + options => + { + options.HttpMethod = HttpMethod.Post.Method; + options.RelativePath = "/starship-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/) + return validationErrors ?? new Dictionary> + { + { "Validation", [ "An error occurred during validation." ] } + }; + } +} +``` -`Controllers/StarshipValidation.cs`: +Confirm or update the namespace of the preceding class (`BlazorSample.Server.Starship`) to match the app's namespace. -:::moniker range=">= aspnetcore-8.0" +Add a client form validator class to the `.Client` project. + +`Starship/ClientFormValidator.cs`: ```csharp -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; -using BlazorSample.Shared; +using System.Net.Http.Json; -namespace BlazorSample.Server.Controllers; +namespace BlazorSample.Client.Starship; -[Authorize] -[ApiController] -[Route("[controller]")] -public class StarshipValidationController( - ILogger logger) - : ControllerBase +internal sealed class ClientFormValidator(HttpClient httpClient) : IFormValidator { - static readonly string[] scopeRequiredByApi = [ "API.Access" ]; + public async Task>> ValidateStarshipFormAsync(StarshipModel starship) + { + using var response = await httpClient.PostAsJsonAsync("/starship-validation", starship); - [HttpPost] - public async Task Post(Starship model) + if (response.IsSuccessStatusCode) + { + var validationResult = await response.Content.ReadFromJsonAsync>>(); + + if (validationResult is not null) + { + return validationResult; + } + } + + return new Dictionary> + { + { "Validation", [ "An error occurred during validation." ] } + }; + } +} +``` + +In the `Program` file of the Blazor Web App: + +```csharp +builder.Services.AddScoped(); + +... + +app.MapPost("/starship-validation", ([FromServices] IFormValidator formValidator, StarshipModel model) => +{ + return formValidator.ValidateStarshipFormAsync(model); +}).RequireAuthorization(); +``` + +The `.Client` project of a Blazor Web App must also register an for HTTP POST requests to a backend web API. Confirm or 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` app: + +```csharp +app.MapPost("/starship-validation", (StarshipModel model, ILogger logger) => +{ + Dictionary> validationErrors = []; + + try { - HttpContext.VerifyUserHasAnyAcceptedScope(scopeRequiredByApi); + if (model.Classification == "Defense" && + string.IsNullOrEmpty(model.Description)) + { + if (!validationErrors.TryGetValue(nameof(model.Description), + out List? value)) + { + value = []; + validationErrors[nameof(model.Description)] = value; + } + + value.Add("For a 'Defense' ship " + + "classification, 'Description' is required."); + } + +#if DEBUG + foreach (var kv in validationErrors) + { + logger.LogInformation("Field: {Field}, Errors: {Errors}", kv.Key, string.Join(", ", kv.Value)); + } +#endif + + } + catch (Exception ex) + { + logger.LogError("Validation Error: {Message}", ex.Message); + validationErrors.Add("Validation", [ "An error occurred during validation." ]); + } + + return validationErrors; +}).RequireAuthorization(); +``` + +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 server validation errors with help of the `CustomValidation` component. When the server API returns validation messages, they're added to the `CustomValidation` component's . The errors are available in the form's for display by the form's validation summary. + +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). + +```razor +@page "/starship-10" +@using Microsoft.AspNetCore.Authorization +@using Microsoft.AspNetCore.Components.WebAssembly.Authentication +@using BlazorWebAppEntra.Client.Starship +@attribute [Authorize] +@inject IFormValidator 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 { - if (model.Classification == "Defense" && - string.IsNullOrEmpty(model.Description)) + var validationErrors = + await FormValidation.ValidateStarshipFormAsync( + (StarshipModel)editContext.Model); + + if (validationErrors.Any()) { - ModelState.AddModelError(nameof(model.Description), - "For a 'Defense' ship " + - "classification, 'Description' is required."); + customValidation?.DisplayErrors(validationErrors); } else { - logger.LogInformation("Processing the form asynchronously"); - - // async ... - - return Ok(ModelState); + disabled = true; + messageStyles = "color:green"; + message = "The form has been processed."; } } + catch (AccessTokenNotAvailableException ex) + { + ex.Redirect(); + } catch (Exception ex) { - logger.LogError("Validation Error: {Message}", ex.Message); + Logger.LogError("Form processing error: {Message}", ex.Message); + disabled = true; + messageStyles = "color:red"; + message = "There was an error processing the form."; } - - return BadRequest(ModelState); } } ``` -:::moniker-end +> [!NOTE] +> As an alternative to the use of a [validation component](#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 validation, the attributes must be executable on the server. For more information, see the [Custom validation attributes](#custom-validation-attributes) section. -:::moniker range="< aspnetcore-8.0" +:::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 validation with web API adopts the same general approach.* + +Server validation is supported in addition to client 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. Server 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 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; @@ -492,7 +721,7 @@ public class StarshipValidationController( ILogger logger) : ControllerBase { - static readonly string[] scopeRequiredByApi = new[] { "API.Access" }; + static readonly string[] scopeRequiredByApi = [ "API.Access" ]; [HttpPost] public async Task Post(Starship model) @@ -527,8 +756,6 @@ public class StarshipValidationController( } ``` -:::moniker-end - 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: @@ -562,8 +789,6 @@ If the server API returns the preceding default JSON response, it's possible for 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 . -:::moniker range=">= aspnetcore-8.0" - Add the namespace to the top of the `Program` file in the main project of the Blazor Web App: ```csharp @@ -611,52 +836,8 @@ In the `.Client` project, the `Starfleet Starship Database` form is updated to s 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. -:::moniker-end - -:::moniker range="< aspnetcore-8.0" - -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 server validation errors with help of the `CustomValidation` component. When the server API returns validation messages, 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. - -:::moniker-end - `Starship10.razor`: -:::moniker range=">= aspnetcore-8.0" - > [!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. @@ -798,14 +979,169 @@ builder.Services.AddScoped(sp => 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), 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 validation, the attributes must be executable on the server. 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 validation with web API adopts the same general approach.* + +Server validation is supported in addition to client 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 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. Server 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 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 server validation errors with help of the `CustomValidation` component. When the server API returns validation messages, 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 @@ -933,17 +1269,9 @@ The preceding example sets the base address with `builder.HostEnvironment.BaseAd } ``` -:::moniker-end - - - > [!NOTE] > As an alternative to the use of a [validation component](#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 validation, the attributes must be executable on the server. For more information, see the [Custom validation attributes](#custom-validation-attributes) section. -:::moniker range="< aspnetcore-8.0" - > [!NOTE] > The server validation approach in this section is suitable for any of the hosted Blazor WebAssembly solution examples in this documentation set: > From 7824b417e4e081f5e0545a995406b5b654d26c75 Mon Sep 17 00:00:00 2001 From: guardrex <1622880+guardrex@users.noreply.github.com> Date: Wed, 6 May 2026 14:01:10 -0400 Subject: [PATCH 2/5] Updates --- aspnetcore/blazor/forms/validation.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/aspnetcore/blazor/forms/validation.md b/aspnetcore/blazor/forms/validation.md index d1397dd3abe1..7e6327cf1257 100644 --- a/aspnetcore/blazor/forms/validation.md +++ b/aspnetcore/blazor/forms/validation.md @@ -669,6 +669,8 @@ Note that the form requires authorization, so the user must be signed into the a > [!NOTE] > As an alternative to the use of a [validation component](#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 validation, the attributes must be executable on the server. For more information, see the [Custom validation attributes](#custom-validation-attributes) section. +::: 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 validation with web API adopts the same general approach.* From 1ea4e2a260048cb9fda9ededbd0ebb77b7e290d6 Mon Sep 17 00:00:00 2001 From: guardrex <1622880+guardrex@users.noreply.github.com> Date: Thu, 7 May 2026 09:04:06 -0400 Subject: [PATCH 3/5] Updates --- aspnetcore/blazor/forms/validation.md | 186 ++++++++++++++++++-------- 1 file changed, 127 insertions(+), 59 deletions(-) diff --git a/aspnetcore/blazor/forms/validation.md b/aspnetcore/blazor/forms/validation.md index 7e6327cf1257..40ab0432969a 100644 --- a/aspnetcore/blazor/forms/validation.md +++ b/aspnetcore/blazor/forms/validation.md @@ -361,86 +361,92 @@ When validation messages are set in the component, they're added to the validato :::moniker range=">= aspnetcore-10.0" -*This section is focused on Blazor Web App scenarios, but the approach for any type of app that uses server validation with web API adopts the same general approach. The approach in this section is based on a [Blazor Web App (Interactive Auto render mode) app hosted in Entra with Microsoft Azure packages](xref:blazor/security/blazor-web-app-entra).* +*This section demonstrates server validation using a [Blazor Web App (Interactive Auto render mode) hosted in Entra with `Microsoft.Identity.Web` packages](xref:blazor/security/blazor-web-app-entra), but the same general approach is recommended for any Blazor app.* Server validation is supported in addition to client 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 custom validation logic supplied by the developer. -* Either disable the form on success or display the errors. +* When the form passes client validation ( is called), send the to a backend server web API for server-side validation. +* Process model validation on the server with custom validation logic. If the model is valid, process the form on the server. +* Send validation errors, if any, back to the client. +* Either disable the form on success or display the errors, allowing the user to 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. +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 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. +* A Blazor Web App with global Interactive Auto components created from the [Blazor Web App project template](xref:blazor/project-structure). * The `CustomValidation` component shown in the [Validator components](#validator-components) section. +* A [Minimal API](xref:fundamentals/minimal-apis) project validates that a ship description field (`Description`) has a value if the user selects the `Defense` ship classification (`Classification`). -In the main project of the Blazor Web App, a Minimal API is added to validate 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 because the upcoming form doesn't perform the same validation client-side when the form is submitted to the server. Server 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 obviously can't be sent to the client for client validation. +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. Server 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] -> The Minimal 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: +> 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/) -Place the `Starship` model (`Starship.cs`) into a `Starship` folder in the `.Client` project. 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. +> [!NOTE] +> Long code lines in the following examples are broken into shorter lines to reduce the appearance of horizontal scrollbars on mobile devices. Where appropriate, you're welcome to combine lines to shorten the length of the code. -[!INCLUDE[](~/includes/package-reference.md)] +Create a `Starship` folder in the `.Client` project of the Blazor Web App. -Add an interface for a form validation service to the `.Client` project. +Place the following `StarshipModel` model (`StarshipModel.cs`) into the `Starship` folder ***and*** into the Minimal API project of the solution. -`Starship/IFormValidator.cs`: +> [!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.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 than what is demonstrated here, set the namespaces appropriately. + +`Starship/StarshipModel.cs` (Blazor Web App) or `Models/StarshipModel.cs` (Minimal API project): ```csharp -namespace BlazorSample.Client.Starship; +using System.ComponentModel.DataAnnotations; -public interface IFormValidator +namespace {NAMESPACE}; + +public class StarshipModel { - Task>> ValidateStarshipFormAsync(StarshipModel starship); + [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; } } ``` -Create a server form validator that implements the preceding interface in the Blazor Web App. +Add an interface for a form validation service to the `.Client` project in the `Starship` folder. The interface is used to register a service for server validation on the server or client validation on the client. -`Starship/ServerFormValidator.cs`: +`Starship/IFormValidator.cs`: ```csharp -using BlazorSample.Client.Starship; -using Microsoft.Identity.Abstractions; - -namespace BlazorSample.Server.Starship; +namespace BlazorSample.Client.Starship; -internal sealed class ServerFormValidator(IDownstreamApi downstreamApi) : IFormValidator +public interface IFormValidator { - public async Task>> ValidateStarshipFormAsync(StarshipModel starship) - { - var validationErrors = await downstreamApi.CallApiForUserAsync>>( - "DownstreamApi", starship, - options => - { - options.HttpMethod = HttpMethod.Post.Method; - options.RelativePath = "/starship-validation"; - }); - - return validationErrors ?? new Dictionary> - { - { "Validation", [ "An error occurred during validation." ] } - }; - } + Task>> ValidateStarshipFormAsync( + StarshipModel starship); } ``` -Confirm or update the namespace of the preceding class (`BlazorSample.Server.Starship`) to match the app's namespace. - -Add a client form validator class to the `.Client` project. +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 the form's model to the backend Minimal API for processing. `Starship/ClientFormValidator.cs`: @@ -451,13 +457,17 @@ namespace BlazorSample.Client.Starship; internal sealed class ClientFormValidator(HttpClient httpClient) : IFormValidator { - public async Task>> ValidateStarshipFormAsync(StarshipModel starship) + public async Task>> ValidateStarshipFormAsync( + StarshipModel starship) { - using var response = await httpClient.PostAsJsonAsync("/starship-validation", starship); + using var response = await httpClient.PostAsJsonAsync( + "/starship-validation", starship); if (response.IsSuccessStatusCode) { - var validationResult = await response.Content.ReadFromJsonAsync>>(); + var validationResult = + await response.Content.ReadFromJsonAsync>>(); if (validationResult is not null) { @@ -473,20 +483,64 @@ internal sealed class ClientFormValidator(HttpClient httpClient) : IFormValidato } ``` +Confirm or update the namespace of the preceding class. + +Create a `Starship` folder in the server project of the Blazor Web App. + +Create a server form validator that implements the preceding interface in the Blazor Web App. 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/ServerFormValidator.cs`: + +```csharp +using BlazorSample.Client.Starship; +using Microsoft.Identity.Abstractions; + +namespace BlazorSample.Server.Starship; + +internal sealed class ServerFormValidator(IDownstreamApi downstreamApi) + : IFormValidator +{ + public async Task>> ValidateStarshipFormAsync( + StarshipModel starship) + { + var validationErrors = + await downstreamApi.CallApiForUserAsync>>( + "DownstreamApi", starship, + options => + { + options.HttpMethod = HttpMethod.Post.Method; + options.RelativePath = "/starship-validation"; + }); + + return validationErrors ?? new Dictionary> + { + { "Validation", [ "An error occurred during validation." ] } + }; + } +} +``` + +Confirm or update the namespace of the preceding class. + In the `Program` file of the Blazor Web App: +* Register the server form validator (`ServerFormValidator`) for the `IFormValidator` 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", ([FromServices] IFormValidator formValidator, StarshipModel model) => +app.MapPost("/starship-validation", ([FromServices] IFormValidator formValidator, + StarshipModel model) => { return formValidator.ValidateStarshipFormAsync(model); }).RequireAuthorization(); ``` -The `.Client` project of a Blazor Web App must also register an for HTTP POST requests to a backend web API. Confirm or add the following to the `.Client` project's `Program` file: +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 => @@ -497,7 +551,9 @@ builder.Services.AddHttpClient(httpClient = The preceding example sets the base address with `builder.HostEnvironment.BaseAddress` (), which gets the base address for the app and is typically derived from the `` tag's `href` value in the host page. -In the `Program` file of the `MinimalApiJwt` app: +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 dictionary of validation errors returns a dictionary entry with the failed field and a description of the error. If validation passes, an empty dictionary is returned. In a typical production app, any number of form model checks are made, and the validation errors dictionary can include multiple failures (`List` value) for each model property (`validationErrors[nameof({PROPERTY})]`). + +In the `Program` file of the Minimal API project: ```csharp app.MapPost("/starship-validation", (StarshipModel model, ILogger logger) => @@ -523,7 +579,8 @@ app.MapPost("/starship-validation", (StarshipModel model, ILogger logge #if DEBUG foreach (var kv in validationErrors) { - logger.LogInformation("Field: {Field}, Errors: {Errors}", kv.Key, string.Join(", ", kv.Value)); + logger.LogInformation("Field: {Field}, Errors: {Errors}", kv.Key, + string.Join(", ", kv.Value)); } #endif @@ -531,29 +588,30 @@ app.MapPost("/starship-validation", (StarshipModel model, ILogger logge catch (Exception ex) { logger.LogError("Validation Error: {Message}", ex.Message); - validationErrors.Add("Validation", [ "An error occurred during validation." ]); + validationErrors.Add( + "Validation", [ "An error occurred during validation." ]); } return validationErrors; }).RequireAuthorization(); ``` -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, add the `CustomValidation` component shown in the [Validator components](#validator-components) section. Confirm or update the namespace. -In the `.Client` project, the `Starfleet Starship Database` form is updated to show server validation errors with help of the `CustomValidation` component. When the server API returns validation messages, 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 `.Client` project, the `Starfleet Starship Database` form is updated to show server validation errors with help of the `CustomValidation` component. When the server API returns validation messages, 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. -`Starship10.razor`: - > [!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 BlazorWebAppEntra.Client.Starship +@using BlazorSample.Client.Starship @attribute [Authorize] @inject IFormValidator FormValidation @inject ILogger Logger @@ -669,6 +727,16 @@ Note that the form requires authorization, so the user must be signed into the a > [!NOTE] > As an alternative to the use of a [validation component](#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 validation, the attributes must be executable on the server. 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 + +``` + ::: moniker-end :::moniker range=">= aspnetcore-8.0 < aspnetcore-10.0" From 57ba3decb99af3a2fd68642f2b7d3568ff43dbc7 Mon Sep 17 00:00:00 2001 From: guardrex <1622880+guardrex@users.noreply.github.com> Date: Fri, 8 May 2026 14:37:29 -0400 Subject: [PATCH 4/5] Updates --- aspnetcore/blazor/forms/validation.md | 257 +++++++++++++++++++------- 1 file changed, 195 insertions(+), 62 deletions(-) diff --git a/aspnetcore/blazor/forms/validation.md b/aspnetcore/blazor/forms/validation.md index 40ab0432969a..50e3e70302d9 100644 --- a/aspnetcore/blazor/forms/validation.md +++ b/aspnetcore/blazor/forms/validation.md @@ -1,5 +1,6 @@ --- title: ASP.NET Core Blazor forms validation +ai-usage: ai-assisted author: guardrex description: Learn how to use validation in Blazor forms. monikerRange: '>= aspnetcore-3.1' @@ -376,7 +377,7 @@ Basic validation is useful in cases where the form's model is defined within the 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). -* The `CustomValidation` component shown in the [Validator components](#validator-components) section. +* 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 validates that a ship description field (`Description`) has a value if the user selects the `Defense` ship classification (`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. Server 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. @@ -400,7 +401,7 @@ Place the following `StarshipModel` model (`StarshipModel.cs`) into the `Starshi > > [!INCLUDE[](~/includes/package-reference.md)] -In the two `StarshipModel` classes, set the namespace (`{NAMESPACE}`) appropriately for each project (for example, `BlazorSample.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 than what is demonstrated here, set the namespaces appropriately. +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 than what is demonstrated here, set the namespaces appropriately. `Starship/StarshipModel.cs` (Blazor Web App) or `Models/StarshipModel.cs` (Minimal API project): @@ -437,12 +438,13 @@ Add an interface for a form validation service to the `.Client` project in the ` `Starship/IFormValidator.cs`: ```csharp +using Microsoft.AspNetCore.Mvc; + namespace BlazorSample.Client.Starship; public interface IFormValidator { - Task>> ValidateStarshipFormAsync( - StarshipModel starship); + Task ValidateStarshipFormAsync(StarshipModel starship); } ``` @@ -451,34 +453,43 @@ Add a client form validator class to the `.Client` project's `Starship` folder. `Starship/ClientFormValidator.cs`: ```csharp +using System.Net; using System.Net.Http.Json; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; namespace BlazorSample.Client.Starship; internal sealed class ClientFormValidator(HttpClient httpClient) : IFormValidator { - public async Task>> ValidateStarshipFormAsync( - StarshipModel starship) + public async Task ValidateStarshipFormAsync(StarshipModel starship) { - using var response = await httpClient.PostAsJsonAsync( - "/starship-validation", starship); + ValidationProblemDetails problemDetails = new() + { + Title = "Validation Error", + Detail = "Form validation failed.", + Instance = "BlazorSample.Client.ClientFormValidator", + Status = StatusCodes.Status500InternalServerError + }; + + using var response = await httpClient.PostAsJsonAsync("/starship-validation", starship); if (response.IsSuccessStatusCode) { - var validationResult = - await response.Content.ReadFromJsonAsync>>(); - - if (validationResult is not null) + return await response.Content.ReadFromJsonAsync() ?? new ValidationProblemDetails { - return validationResult; - } + Title = "Validation succeeded", + Detail = "The starship form is valid.", + Instance = "BlazorSample.Client.ClientFormValidator", + Status = StatusCodes.Status500InternalServerError + }; } - - return new Dictionary> + else if (response.StatusCode == HttpStatusCode.BadRequest) { - { "Validation", [ "An error occurred during validation." ] } - }; + return await response.Content.ReadFromJsonAsync() ?? problemDetails; + } + + return problemDetails; } } ``` @@ -492,31 +503,67 @@ Create a server form validator that implements the preceding interface in the Bl `Starship/ServerFormValidator.cs`: ```csharp -using BlazorSample.Client.Starship; +using System.Net; +using System.Text.Json; +using Microsoft.AspNetCore.Mvc; using Microsoft.Identity.Abstractions; +using BlazorSample.Client.Starship; -namespace BlazorSample.Server.Starship; +namespace BlazorSample.Starship; internal sealed class ServerFormValidator(IDownstreamApi downstreamApi) : IFormValidator { - public async Task>> ValidateStarshipFormAsync( + private readonly static JsonSerializerOptions jsonSerializerOptions = new() + { + PropertyNameCaseInsensitive = true + }; + + public async Task ValidateStarshipFormAsync( StarshipModel starship) { - var validationErrors = - await downstreamApi.CallApiForUserAsync>>( - "DownstreamApi", starship, + ValidationProblemDetails problemDetails = new() + { + Title = "Validation Error", + Detail = "Form validation failed.", + Instance = "BlazorSample.Server.ServerFormValidator", + Status = StatusCodes.Status500InternalServerError + }; + + try + { + var response = await downstreamApi.CallApiForUserAsync( + "DownstreamApi", + starship, options => { options.HttpMethod = HttpMethod.Post.Method; options.RelativePath = "/starship-validation"; }); - return validationErrors ?? new Dictionary> + return response ?? problemDetails; + } + catch (HttpRequestException ex) when + (ex.StatusCode == HttpStatusCode.BadRequest) { - { "Validation", [ "An error occurred during validation." ] } - }; + int index = ex.Message.IndexOf('{'); + + if (index != -1) + { + var problemDetailsJson = ex.Message[index..]; + + if (problemDetailsJson is not null) + { + var deserializedProblemDetails = JsonSerializer.Deserialize + (problemDetailsJson, jsonSerializerOptions); + + return deserializedProblemDetails ?? problemDetails; + } + } + } + + return problemDetails; } } ``` @@ -533,7 +580,7 @@ builder.Services.AddScoped(); ... -app.MapPost("/starship-validation", ([FromServices] IFormValidator formValidator, +app.MapPost("/starship-validation", ([IFormValidator formValidator, StarshipModel model) => { return formValidator.ValidateStarshipFormAsync(model); @@ -558,46 +605,98 @@ In the `Program` file of the Minimal API project: ```csharp app.MapPost("/starship-validation", (StarshipModel model, ILogger logger) => { - Dictionary> validationErrors = []; + Dictionary errors = []; - try + if (model.Classification == "Defense" && string.IsNullOrEmpty(model.Description)) { - if (model.Classification == "Defense" && - string.IsNullOrEmpty(model.Description)) - { - if (!validationErrors.TryGetValue(nameof(model.Description), - out List? value)) - { - value = []; - validationErrors[nameof(model.Description)] = value; - } + errors.Add(nameof(model.Description), ["For a 'Defense' ship, 'Description' is required."]); + } - value.Add("For a 'Defense' ship " + - "classification, 'Description' is required."); - } + if (errors.Count > 0) + { + return Results.ValidationProblem( + errors: errors, + detail: "One or more validation errors occurred.", + instance: "MinimalApiJwt", + statusCode: 400, + 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 code for `/starship-validation`. + +In the `.Client` project, add the following `CustomValidation` component. Confirm or update the namespace. -#if DEBUG - foreach (var kv in validationErrors) +`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) { - logger.LogInformation("Field: {Field}, Errors: {Errors}", kv.Key, - string.Join(", ", kv.Value)); + throw new InvalidOperationException( + $"{nameof(CustomValidation)} requires a cascading " + + $"parameter of type {nameof(EditContext)}. " + + $"For example, you can use {nameof(CustomValidation)} " + + $"inside an {nameof(EditForm)}."); } -#endif + messageStore = new(CurrentEditContext); + + CurrentEditContext.OnValidationRequested += (s, e) => + messageStore?.Clear(); + CurrentEditContext.OnFieldChanged += (s, e) => + messageStore?.Clear(e.FieldIdentifier); } - catch (Exception ex) + + public void DisplayErrors(ValidationProblemDetails problemDetails) { - logger.LogError("Validation Error: {Message}", ex.Message); - validationErrors.Add( - "Validation", [ "An error occurred during validation." ]); + if (CurrentEditContext is not null) + { + foreach (var err in problemDetails.Errors) + { + foreach (var value in err.Value) + { + messageStore?.Add(CurrentEditContext.Field(err.Key), value); + } + } + + CurrentEditContext.NotifyValidationStateChanged(); + } } - return validationErrors; -}).RequireAuthorization(); + public void ClearErrors() + { + messageStore?.Clear(); + CurrentEditContext?.NotifyValidationStateChanged(); + } +} ``` -In the `.Client` project, add the `CustomValidation` component shown in the [Validator components](#validator-components) section. Confirm or update the namespace. - In the `.Client` project, the `Starfleet Starship Database` form is updated to show server validation errors with help of the `CustomValidation` component. When the server API returns validation messages, 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. @@ -611,6 +710,7 @@ Note that the form requires authorization, so the user must be signed into the a @page "/starship-10" @using Microsoft.AspNetCore.Authorization @using Microsoft.AspNetCore.Components.WebAssembly.Authentication +@using Microsoft.AspNetCore.Http @using BlazorSample.Client.Starship @attribute [Authorize] @inject IFormValidator FormValidation @@ -694,13 +794,13 @@ Note that the form requires authorization, so the user must be signed into the a try { - var validationErrors = + var validationProblemDetails = await FormValidation.ValidateStarshipFormAsync( (StarshipModel)editContext.Model); - if (validationErrors.Any()) + if (validationProblemDetails.Status != StatusCodes.Status204NoContent) { - customValidation?.DisplayErrors(validationErrors); + customValidation?.DisplayErrors(validationProblemDetails); } else { @@ -715,7 +815,7 @@ Note that the form requires authorization, so the user must be signed into the a } catch (Exception ex) { - Logger.LogError("Form processing error: {Message}", ex.Message); + Logger.LogError(ex, "Form processing error."); disabled = true; messageStyles = "color:red"; message = "There was an error processing the form."; @@ -737,7 +837,40 @@ To reach the form easily, add the following entry to the `NavMenu` component (`L ``` -::: moniker-end +When automatic model binding validation error occurs 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)." ] + } +} +``` + +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." ] + } +} +``` + +> [!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 server API, such as [Firefox Browser Developer](https://www.mozilla.org/firefox/developer/). + +:::moniker-end :::moniker range=">= aspnetcore-8.0 < aspnetcore-10.0" From c293df06ebfee5fda55094ef98b1bcf0f09f6068 Mon Sep 17 00:00:00 2001 From: guardrex <1622880+guardrex@users.noreply.github.com> Date: Fri, 8 May 2026 15:42:20 -0400 Subject: [PATCH 5/5] Updates --- aspnetcore/blazor/forms/validation.md | 33 +++++++++++++++++---------- 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/aspnetcore/blazor/forms/validation.md b/aspnetcore/blazor/forms/validation.md index 50e3e70302d9..12e206bcfc9d 100644 --- a/aspnetcore/blazor/forms/validation.md +++ b/aspnetcore/blazor/forms/validation.md @@ -472,21 +472,28 @@ internal sealed class ClientFormValidator(HttpClient httpClient) : IFormValidato Status = StatusCodes.Status500InternalServerError }; - using var response = await httpClient.PostAsJsonAsync("/starship-validation", starship); - - if (response.IsSuccessStatusCode) + try { - return await response.Content.ReadFromJsonAsync() ?? new ValidationProblemDetails + using var response = await httpClient.PostAsJsonAsync("/starship-validation", starship); + + if (response.IsSuccessStatusCode) { - Title = "Validation succeeded", - Detail = "The starship form is valid.", - Instance = "BlazorSample.Client.ClientFormValidator", - Status = StatusCodes.Status500InternalServerError - }; + return await response.Content.ReadFromJsonAsync() ?? new ValidationProblemDetails + { + Title = "Validation succeeded", + Detail = "The starship form is valid.", + Instance = "BlazorSample.Client.ClientFormValidator", + Status = StatusCodes.Status500InternalServerError + }; + } + else if (response.StatusCode == HttpStatusCode.BadRequest) + { + return await response.Content.ReadFromJsonAsync() ?? problemDetails; + } } - else if (response.StatusCode == HttpStatusCode.BadRequest) + catch (Exception ex) { - return await response.Content.ReadFromJsonAsync() ?? problemDetails; + //Log the exception or handle it as needed } return problemDetails; @@ -547,6 +554,8 @@ internal sealed class ServerFormValidator(IDownstreamApi downstreamApi) catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.BadRequest) { + // The response content is only available via + // the Message property of the exception int index = ex.Message.IndexOf('{'); if (index != -1) @@ -598,7 +607,7 @@ builder.Services.AddHttpClient(httpClient = The preceding example sets the base address with `builder.HostEnvironment.BaseAddress` (), which gets the base address for the app and is typically derived from the `` tag's `href` value in the host page. -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 dictionary of validation errors returns a dictionary entry with the failed field and a description of the error. If validation passes, an empty dictionary is returned. In a typical production app, any number of form model checks are made, and the validation errors dictionary can include multiple failures (`List` value) for each model property (`validationErrors[nameof({PROPERTY})]`). +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: