Local validation runs before the form calls the remote endpoint. The endpoint validates the model again and applies a private business rule that requires a description for defense ships.
+ Swapping to Development environment will display more detailed information about the error that occurred.
+
+
+ The Development environment shouldn't be enabled for deployed applications.
+ It can result in displaying sensitive information from exceptions to end users.
+ For local debugging, enable the Development environment by setting the ASPNETCORE_ENVIRONMENT environment variable to Development
+ and restarting the app.
+
+
+@code{
+ [CascadingParameter]
+ private HttpContext? HttpContext { get; set; }
+
+ [PersistentState]
+ public string? RequestId { get; set; }
+
+ private bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
+
+ protected override void OnInitialized()
+ {
+ RequestId ??= Activity.Current?.Id ?? HttpContext?.TraceIdentifier;
+ }
+}
diff --git a/11.0/BlazorWebAppRemoteValidation/BlazorWebAppRemoteValidation/Components/_Imports.razor b/11.0/BlazorWebAppRemoteValidation/BlazorWebAppRemoteValidation/Components/_Imports.razor
new file mode 100644
index 000000000..6516c25c4
--- /dev/null
+++ b/11.0/BlazorWebAppRemoteValidation/BlazorWebAppRemoteValidation/Components/_Imports.razor
@@ -0,0 +1,11 @@
+@using System.Net.Http
+@using System.Net.Http.Json
+@using Microsoft.AspNetCore.Components.Forms
+@using Microsoft.AspNetCore.Components.Routing
+@using Microsoft.AspNetCore.Components.Web
+@using static Microsoft.AspNetCore.Components.Web.RenderMode
+@using Microsoft.AspNetCore.Components.Web.Virtualization
+@using Microsoft.JSInterop
+@using BlazorWebAppRemoteValidation
+@using BlazorWebAppRemoteValidation.Client
+@using BlazorWebAppRemoteValidation.Components
diff --git a/11.0/BlazorWebAppRemoteValidation/BlazorWebAppRemoteValidation/Program.cs b/11.0/BlazorWebAppRemoteValidation/BlazorWebAppRemoteValidation/Program.cs
new file mode 100644
index 000000000..2740aa8ef
--- /dev/null
+++ b/11.0/BlazorWebAppRemoteValidation/BlazorWebAppRemoteValidation/Program.cs
@@ -0,0 +1,48 @@
+using BlazorWebAppRemoteValidation.Components;
+using BlazorWebAppRemoteValidation.Client.Models;
+using BlazorWebAppRemoteValidation.Client.Validation;
+
+var builder = WebApplication.CreateBuilder(args);
+
+// Add services to the container.
+builder.Services.AddRazorComponents()
+ .AddInteractiveWebAssemblyComponents();
+builder.Services.AddClientModelValidation();
+builder.Services.AddValidation();
+
+var app = builder.Build();
+
+// Configure the HTTP request pipeline.
+if (!app.Environment.IsDevelopment())
+{
+ app.UseExceptionHandler("/Error", createScopeForErrors: true);
+}
+app.UseStatusCodePagesWithReExecute("/not-found", createScopeForStatusCodePages: true);
+app.MapStaticAssets();
+
+//
+app.MapPost("/api/starships/validate", (StarshipModel model) =>
+{
+ Dictionary errors = [];
+
+ if (model.Classification == "Defense" &&
+ string.IsNullOrWhiteSpace(model.Description))
+ {
+ errors[nameof(model.Description)] =
+ ["A defense ship requires a description."];
+ }
+
+ if (errors.Count > 0)
+ {
+ return Results.ValidationProblem(errors);
+ }
+
+ return Results.NoContent();
+});
+//
+
+app.MapRazorComponents()
+ .AddInteractiveWebAssemblyRenderMode()
+ .AddAdditionalAssemblies(typeof(BlazorWebAppRemoteValidation.Client._Imports).Assembly);
+
+app.Run();
diff --git a/11.0/BlazorWebAppRemoteValidation/BlazorWebAppRemoteValidation/Properties/launchSettings.json b/11.0/BlazorWebAppRemoteValidation/BlazorWebAppRemoteValidation/Properties/launchSettings.json
new file mode 100644
index 000000000..7d2665004
--- /dev/null
+++ b/11.0/BlazorWebAppRemoteValidation/BlazorWebAppRemoteValidation/Properties/launchSettings.json
@@ -0,0 +1,14 @@
+{
+ "$schema": "https://json.schemastore.org/launchsettings.json",
+ "profiles": {
+ "http": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "applicationUrl": "http://localhost:5257",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ }
+ }
+ }
diff --git a/11.0/BlazorWebAppRemoteValidation/BlazorWebAppRemoteValidation/appsettings.Development.json b/11.0/BlazorWebAppRemoteValidation/BlazorWebAppRemoteValidation/appsettings.Development.json
new file mode 100644
index 000000000..0c208ae91
--- /dev/null
+++ b/11.0/BlazorWebAppRemoteValidation/BlazorWebAppRemoteValidation/appsettings.Development.json
@@ -0,0 +1,8 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ }
+}
diff --git a/11.0/BlazorWebAppRemoteValidation/BlazorWebAppRemoteValidation/appsettings.json b/11.0/BlazorWebAppRemoteValidation/BlazorWebAppRemoteValidation/appsettings.json
new file mode 100644
index 000000000..10f68b8c8
--- /dev/null
+++ b/11.0/BlazorWebAppRemoteValidation/BlazorWebAppRemoteValidation/appsettings.json
@@ -0,0 +1,9 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ },
+ "AllowedHosts": "*"
+}
diff --git a/11.0/BlazorWebAppRemoteValidation/BlazorWebAppRemoteValidation/wwwroot/app.css b/11.0/BlazorWebAppRemoteValidation/BlazorWebAppRemoteValidation/wwwroot/app.css
new file mode 100644
index 000000000..2db4212ee
--- /dev/null
+++ b/11.0/BlazorWebAppRemoteValidation/BlazorWebAppRemoteValidation/wwwroot/app.css
@@ -0,0 +1,38 @@
+h1:focus {
+ outline: none;
+}
+
+.valid.modified:not([type=checkbox]) {
+ outline: 1px solid #26b050;
+}
+
+.invalid {
+ outline: 1px solid #e50000;
+}
+
+.validation-message {
+ color: #e50000;
+}
+
+.blazor-error-boundary {
+ background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121;
+ padding: 1rem 1rem 1rem 3.7rem;
+ color: white;
+}
+
+ .blazor-error-boundary::after {
+ content: "An error has occurred."
+ }
+
+.darker-border-checkbox.form-check-input {
+ border-color: #929292;
+}
+
+.form-floating > .form-control-plaintext::placeholder, .form-floating > .form-control::placeholder {
+ color: var(--bs-secondary-color);
+ text-align: end;
+}
+
+.form-floating > .form-control-plaintext:focus::placeholder, .form-floating > .form-control:focus::placeholder {
+ text-align: start;
+}
diff --git a/11.0/BlazorWebAppRemoteValidation/README.md b/11.0/BlazorWebAppRemoteValidation/README.md
new file mode 100644
index 000000000..0b3b43b62
--- /dev/null
+++ b/11.0/BlazorWebAppRemoteValidation/README.md
@@ -0,0 +1,20 @@
+# Blazor Web App remote validation
+
+This sample uses an Interactive WebAssembly form and a Minimal API in the Blazor Web App host to demonstrate remote form validation.
+
+The form:
+
+1. Runs data annotations validation locally.
+1. Sends locally valid input to the Minimal API.
+1. Displays field-keyed errors returned by the endpoint.
+1. Proceeds only when local and remote validation succeed.
+
+The endpoint validates data annotations before its handler runs and applies a private business rule in the handler.
+
+WebAssembly prerendering is disabled so the form's `HttpClient` dependency only needs to be registered in the `.Client` project.
+
+Run the sample from the solution directory:
+
+```dotnetcli
+dotnet run --project BlazorWebAppRemoteValidation
+```
diff --git a/8.0/BlazorSample_BlazorWebApp/CustomFieldClassProvider.cs b/8.0/BlazorSample_BlazorWebApp/CustomFieldClassProvider.cs
index 4adab8609..158681e1f 100644
--- a/8.0/BlazorSample_BlazorWebApp/CustomFieldClassProvider.cs
+++ b/8.0/BlazorSample_BlazorWebApp/CustomFieldClassProvider.cs
@@ -4,7 +4,11 @@ namespace BlazorSample;
public class CustomFieldClassProvider : FieldCssClassProvider
{
- public override string GetFieldCssClass(
- EditContext editContext, in FieldIdentifier fieldIdentifier) =>
- editContext.IsValid(fieldIdentifier) ? "validField" : "invalidField";
+ public override string GetFieldCssClass(EditContext editContext,
+ in FieldIdentifier fieldIdentifier)
+ {
+ var isValid = editContext.IsValid(fieldIdentifier);
+
+ return isValid ? "validField" : "invalidField";
+ }
}
diff --git a/8.0/BlazorSample_BlazorWebApp/CustomFieldClassProvider2.cs b/8.0/BlazorSample_BlazorWebApp/CustomFieldClassProvider2.cs
index 290dc8d3f..e228f20a2 100644
--- a/8.0/BlazorSample_BlazorWebApp/CustomFieldClassProvider2.cs
+++ b/8.0/BlazorSample_BlazorWebApp/CustomFieldClassProvider2.cs
@@ -9,8 +9,9 @@ public override string GetFieldCssClass(EditContext editContext,
{
if (fieldIdentifier.FieldName == "Name")
{
- return editContext.IsValid(fieldIdentifier) ?
- "validField" : "invalidField";
+ var isValid = editContext.IsValid(fieldIdentifier);
+
+ return isValid ? "validField" : "invalidField";
}
return string.Empty;
diff --git a/9.0/BlazorSample_BlazorWebApp/CustomFieldClassProvider.cs b/9.0/BlazorSample_BlazorWebApp/CustomFieldClassProvider.cs
index 4adab8609..158681e1f 100644
--- a/9.0/BlazorSample_BlazorWebApp/CustomFieldClassProvider.cs
+++ b/9.0/BlazorSample_BlazorWebApp/CustomFieldClassProvider.cs
@@ -4,7 +4,11 @@ namespace BlazorSample;
public class CustomFieldClassProvider : FieldCssClassProvider
{
- public override string GetFieldCssClass(
- EditContext editContext, in FieldIdentifier fieldIdentifier) =>
- editContext.IsValid(fieldIdentifier) ? "validField" : "invalidField";
+ public override string GetFieldCssClass(EditContext editContext,
+ in FieldIdentifier fieldIdentifier)
+ {
+ var isValid = editContext.IsValid(fieldIdentifier);
+
+ return isValid ? "validField" : "invalidField";
+ }
}
diff --git a/9.0/BlazorSample_BlazorWebApp/CustomFieldClassProvider2.cs b/9.0/BlazorSample_BlazorWebApp/CustomFieldClassProvider2.cs
index 290dc8d3f..e228f20a2 100644
--- a/9.0/BlazorSample_BlazorWebApp/CustomFieldClassProvider2.cs
+++ b/9.0/BlazorSample_BlazorWebApp/CustomFieldClassProvider2.cs
@@ -9,8 +9,9 @@ public override string GetFieldCssClass(EditContext editContext,
{
if (fieldIdentifier.FieldName == "Name")
{
- return editContext.IsValid(fieldIdentifier) ?
- "validField" : "invalidField";
+ var isValid = editContext.IsValid(fieldIdentifier);
+
+ return isValid ? "validField" : "invalidField";
}
return string.Empty;