diff --git a/aspnetcore/blazor/forms/validation.md b/aspnetcore/blazor/forms/validation.md index 7ade18ddff30..12e206bcfc9d 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' @@ -359,124 +360,564 @@ 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" +:::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.* +*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.* -:::moniker-end +Server validation is supported in addition to client validation: -:::moniker range="< aspnetcore-8.0" +* Process client validation in the form with the component. +* 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. -*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.* +Basic validation is useful in cases where the form's model is defined within the component hosting the form, either as members directly on the component or in a subclass. Use of a *validator component* is recommended where an independent model class is used across several components. The approach demonstrated by the following guidance uses a validator component. -:::moniker-end +The following example is based on: -Server validation is supported in addition to client validation: +* A Blazor Web App with global Interactive Auto components created from the [Blazor Web App project template](xref:blazor/project-structure). +* A `CustomValidation` component to handle adding model errors to the form's validation message store for display in the UI. +* A [Minimal API](xref:fundamentals/minimal-apis) project validates that a ship description field (`Description`) has a value if the user selects the `Defense` ship classification (`Classification`). -* 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. +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. -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. +> [!NOTE] +> For more information on security pertaining to the following example, see the following resources: +> +> * +> * (and the other articles in the Blazor *Security and Identity* node) +> * [Microsoft identity platform documentation](/entra/identity-platform/) -The following example is based on: +> [!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. -:::moniker range=">= aspnetcore-8.0" +Create a `Starship` folder in the `.Client` project of the Blazor Web App. -* 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 following `StarshipModel` model (`StarshipModel.cs`) into the `Starship` folder ***and*** into the Minimal API project of the solution. -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. +> [!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)] -[!INCLUDE[](~/includes/package-reference.md)] +In the two `StarshipModel` classes, set the namespace (`{NAMESPACE}`) appropriately for each project (for example, `BlazorSample.Client.Starship` in the Blazor Web App and `MinimalApiJwt.Models` in the Minimal API project). Some developers prefer to use a different folder scheme. If you position the classes in the projects in different locations than what is demonstrated here, set the namespaces appropriately. -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`). +`Starship/StarshipModel.cs` (Blazor Web App) or `Models/StarshipModel.cs` (Minimal API project): -:::moniker-end +```csharp +using System.ComponentModel.DataAnnotations; -:::moniker range="< aspnetcore-8.0" +namespace {NAMESPACE}; -* 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 class StarshipModel +{ + [Required] + [StringLength(16, ErrorMessage = "Identifier too long (16 character limit).")] + public string? Id { get; set; } -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. + public string? Description { get; set; } -[!INCLUDE[](~/includes/package-reference.md)] + [Required] + public string? Classification { get; set; } -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`). + [Range(1, 100000, ErrorMessage = "Accommodation invalid (1-100000).")] + public int MaximumAccommodation { get; set; } -:::moniker-end + [Required] + [Range(typeof(bool), "true", "true", ErrorMessage = "Approval required.")] + public bool IsValidatedDesign { get; set; } -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. + [Required] + public DateTime ProductionDate { get; set; } +} +``` -> [!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/) +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. -`Controllers/StarshipValidation.cs`: +`Starship/IFormValidator.cs`: -:::moniker range=">= aspnetcore-8.0" +```csharp +using Microsoft.AspNetCore.Mvc; + +namespace BlazorSample.Client.Starship; + +public interface IFormValidator +{ + Task ValidateStarshipFormAsync(StarshipModel starship); +} +``` + +Add a client form validator class to the `.Client` project's `Starship` folder. The client form validator is used when the app is running on the client. The validator class posts the form's model to the backend Minimal API for processing. + +`Starship/ClientFormValidator.cs`: ```csharp -using Microsoft.AspNetCore.Authorization; +using System.Net; +using System.Net.Http.Json; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -using BlazorSample.Shared; -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) + { + ValidationProblemDetails problemDetails = new() + { + Title = "Validation Error", + Detail = "Form validation failed.", + Instance = "BlazorSample.Client.ClientFormValidator", + Status = StatusCodes.Status500InternalServerError + }; - [HttpPost] - public async Task Post(Starship model) + try + { + using var response = await httpClient.PostAsJsonAsync("/starship-validation", starship); + + if (response.IsSuccessStatusCode) + { + 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; + } + } + catch (Exception ex) + { + //Log the exception or handle it as needed + } + + return problemDetails; + } +} +``` + +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 System.Net; +using System.Text.Json; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Identity.Abstractions; +using BlazorSample.Client.Starship; + +namespace BlazorSample.Starship; + +internal sealed class ServerFormValidator(IDownstreamApi downstreamApi) + : IFormValidator +{ + private readonly static JsonSerializerOptions jsonSerializerOptions = new() { - HttpContext.VerifyUserHasAnyAcceptedScope(scopeRequiredByApi); + PropertyNameCaseInsensitive = true + }; + + public async Task ValidateStarshipFormAsync( + StarshipModel starship) + { + ValidationProblemDetails problemDetails = new() + { + Title = "Validation Error", + Detail = "Form validation failed.", + Instance = "BlazorSample.Server.ServerFormValidator", + Status = StatusCodes.Status500InternalServerError + }; try { - if (model.Classification == "Defense" && - string.IsNullOrEmpty(model.Description)) + var response = await downstreamApi.CallApiForUserAsync( + "DownstreamApi", + starship, + options => + { + options.HttpMethod = HttpMethod.Post.Method; + options.RelativePath = "/starship-validation"; + }); + + return response ?? problemDetails; + } + 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) { - ModelState.AddModelError(nameof(model.Description), - "For a 'Defense' ship " + - "classification, 'Description' is required."); + var problemDetailsJson = ex.Message[index..]; + + if (problemDetailsJson is not null) + { + var deserializedProblemDetails = JsonSerializer.Deserialize + (problemDetailsJson, jsonSerializerOptions); + + return deserializedProblemDetails ?? problemDetails; + } } - else + } + + return problemDetails; + } +} +``` + +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", ([IFormValidator formValidator, + StarshipModel model) => +{ + return formValidator.ValidateStarshipFormAsync(model); +}).RequireAuthorization(); +``` + +The `.Client` project of a Blazor Web App must register an for HTTP POST requests to the Minimal API. Add the following to the `.Client` project's `Program` file: + +```csharp +builder.Services.AddHttpClient(httpClient => +{ + httpClient.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress); +}); +``` + +The preceding example sets the base address with `builder.HostEnvironment.BaseAddress` (), which gets the base address for the app and is typically derived from the `` tag's `href` value in the host page. + +In the `Program` file of the `MinimalApiJwt` project, add the following starship form validation endpoint. The endpoint validates that the model's `Description` property has a value when the model's `Classification` property is `Defense`. If validation fails, a `ValidationProblem` returns a dictionary with the failed field and a description of the error. If validation passes, a *204 - No Content* response is issued. In a typical production app, any number of custom form model checks are made, and the validation errors dictionary can include multiple failures (`string[]` value) for each model property. + +In the `Program` file of the Minimal API project: + +```csharp +app.MapPost("/starship-validation", (StarshipModel model, ILogger logger) => +{ + Dictionary errors = []; + + if (model.Classification == "Defense" && string.IsNullOrEmpty(model.Description)) + { + errors.Add(nameof(model.Description), ["For a 'Defense' ship, 'Description' is required."]); + } + + if (errors.Count > 0) + { + return Results.ValidationProblem( + errors: errors, + detail: "One or more validation errors occurred.", + instance: "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. + +`CustomValidation.cs`: + +```csharp +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Forms; +using Microsoft.AspNetCore.Mvc; + +namespace BlazorSample.Client; + +public class CustomValidation : ComponentBase +{ + private ValidationMessageStore? messageStore; + + [CascadingParameter] + private EditContext? CurrentEditContext { get; set; } + + protected override void OnInitialized() + { + if (CurrentEditContext is null) + { + throw new InvalidOperationException( + $"{nameof(CustomValidation)} requires a cascading " + + $"parameter of type {nameof(EditContext)}. " + + $"For example, you can use {nameof(CustomValidation)} " + + $"inside an {nameof(EditForm)}."); + } + + messageStore = new(CurrentEditContext); + + CurrentEditContext.OnValidationRequested += (s, e) => + messageStore?.Clear(); + CurrentEditContext.OnFieldChanged += (s, e) => + messageStore?.Clear(e.FieldIdentifier); + } + + public void DisplayErrors(ValidationProblemDetails problemDetails) + { + if (CurrentEditContext is not null) + { + foreach (var err in problemDetails.Errors) { - logger.LogInformation("Processing the form asynchronously"); + foreach (var value in err.Value) + { + messageStore?.Add(CurrentEditContext.Field(err.Key), value); + } + } - // async ... + CurrentEditContext.NotifyValidationStateChanged(); + } + } - return Ok(ModelState); + public void ClearErrors() + { + messageStore?.Clear(); + CurrentEditContext?.NotifyValidationStateChanged(); + } +} +``` + +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. + +> [!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 Microsoft.AspNetCore.Http +@using BlazorSample.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 + { + var validationProblemDetails = + await FormValidation.ValidateStarshipFormAsync( + (StarshipModel)editContext.Model); + + if (validationProblemDetails.Status != StatusCodes.Status204NoContent) + { + customValidation?.DisplayErrors(validationProblemDetails); + } + else + { + disabled = true; + messageStyles = "color:green"; + message = "The form has been processed."; } } + catch (AccessTokenNotAvailableException ex) + { + ex.Redirect(); + } catch (Exception ex) { - logger.LogError("Validation Error: {Message}", ex.Message); + Logger.LogError(ex, "Form processing error."); + disabled = true; + messageStyles = "color:red"; + message = "There was an error processing the form."; } + } +} +``` - return BadRequest(ModelState); +> [!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 + +``` + +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" +:::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 +933,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 +968,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 +1001,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 +1048,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 +1191,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 +1481,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: >