Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions .github/workflows/build-samples.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ on:
- "aspnet-core/caching-outputcache-redis-invalidation/**"
- "aspnet-core/minimal-apis-real-world/**"
- "blazor/create-interactive-ui-csharp-12/**"
- "blazor/forms-validation-masterclass/**"
- ".github/workflows/build-samples.yml"

pull_request:
Expand All @@ -30,6 +31,7 @@ on:
- "aspnet-core/caching-outputcache-redis-invalidation/**"
- "aspnet-core/minimal-apis-real-world/**"
- "blazor/create-interactive-ui-csharp-12/**"
- "blazor/forms-validation-masterclass/**"
- ".github/workflows/build-samples.yml"

workflow_dispatch:
Expand Down Expand Up @@ -387,3 +389,35 @@ jobs:
blazor/create-interactive-ui-csharp-12/BlazorTodoMinimal.slnx
--configuration Release
--no-build

test-blazor-profile-validation:
name: Test Blazor profile validation sample
runs-on: ubuntu-latest

steps:
- name: Check out repository
uses: actions/checkout@v5

- name: Install .NET 10 SDK
uses: actions/setup-dotnet@v5
with:
dotnet-version: "10.0.x"

- name: Restore
run: >
dotnet restore
blazor/forms-validation-masterclass/BlazorProfileValidation.slnx

- name: Build
run: >
dotnet build
blazor/forms-validation-masterclass/BlazorProfileValidation.slnx
--configuration Release
--no-restore

- name: Test
run: >
dotnet test
blazor/forms-validation-masterclass/BlazorProfileValidation.slnx
--configuration Release
--no-build
51 changes: 40 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ Each sample folder contains a focused implementation of one tutorial topic. The
| [`aspnet-core/caching-outputcache-redis-invalidation`](aspnet-core/caching-outputcache-redis-invalidation/) | Minimal .NET 10 Catalog API demonstrating Output Cache policies, query and route variation, tag eviction, write-path invalidation, and integration testing | [ASP.NET Core Caching: Output Cache, Redis & Invalidation Strategies That Actually Work](https://www.dotnet-guide.com/tutorials/aspnet-core/caching-outputcache-redis-invalidation/) |
| [`aspnet-core/minimal-apis-real-world`](aspnet-core/minimal-apis-real-world/) | Focused .NET 10 Orders API demonstrating endpoint filters, FluentValidation, URL-segment versioning, typed results, and partitioned rate limiting | [ASP.NET Core / Minimal APIs in the Real World: Filters, Validation, Versioning & Rate Limiting](https://www.dotnet-guide.com/tutorials/aspnet-core/minimal-apis-real-world/) |
| [`blazor/create-interactive-ui-csharp-12`](blazor/create-interactive-ui-csharp-12/) | Interactive .NET 10 Todo Dashboard demonstrating Razor components, form binding, DataAnnotations validation, scoped state, EventCallback communication, filtering, and bUnit component tests | [Blazor Web Development: Create Interactive UIs with C# 12](https://www.dotnet-guide.com/tutorials/blazor/create-interactive-ui-csharp-12/) |
| [`blazor/forms-validation-masterclass`](blazor/forms-validation-masterclass/) | Focused .NET 10 Profile Settings form demonstrating manual EditContext management, DataAnnotations, FluentValidation, backend field-error mapping, accessible inputs, dirty state, and bUnit testing | [Blazor .NET 8 Forms & Validation: EditForm, FluentValidation & Server Error Handling](https://www.dotnet-guide.com/tutorials/blazor/forms-validation-masterclass/) |

## Companion articles
- [Common Microsoft.Extensions.AI mistakes](https://www.dotnet-guide.com/articles/dotnet-ai/microsoft-extensions-ai-common-mistakes/)
Expand Down Expand Up @@ -124,27 +125,55 @@ tutorials/
| |-- TransactionalOutboxMinimal.Tests.csproj
| `-- OutboxFlowTests.cs
|-- blazor/
| `-- create-interactive-ui-csharp-12/
| |-- BlazorTodoMinimal.slnx
| |-- create-interactive-ui-csharp-12/
| | |-- BlazorTodoMinimal.slnx
| | |-- README.md
| | |-- src/
| | | `-- BlazorTodoMinimal/
| | | |-- BlazorTodoMinimal.csproj
| | | |-- Program.cs
| | | |-- TodoState.cs
| | | `-- Components/
| | | |-- _Imports.razor
| | | |-- App.razor
| | | |-- Routes.razor
| | | |-- Pages/
| | | | |-- NotFound.razor
| | | | `-- Todos.razor
| | | `-- Shared/
| | | `-- TodoList.razor
| | `-- tests/
| | `-- BlazorTodoMinimal.Tests/
| | |-- BlazorTodoMinimal.Tests.csproj
| | `-- TodoDashboardTests.cs
| `-- forms-validation-masterclass/
| |-- BlazorProfileValidation.slnx
| |-- README.md
| |-- src/
| | `-- BlazorTodoMinimal/
| | |-- BlazorTodoMinimal.csproj
| | `-- BlazorProfileValidation/
| | |-- BlazorProfileValidation.csproj
| | |-- Program.cs
| | |-- TodoState.cs
| | |-- Models/
| | | `-- ProfileModel.cs
| | |-- Services/
| | | `-- ProfileService.cs
| | |-- Validation/
| | | `-- ProfileValidator.cs
| | `-- Components/
| | |-- _Imports.razor
| | |-- App.razor
| | |-- Routes.razor
| | |-- Pages/
| | | |-- NotFound.razor
| | | `-- Todos.razor
| | `-- Shared/
| | `-- TodoList.razor
| | | `-- ProfileSettings.razor
| | |-- Shared/
| | | `-- FormTextField.razor
| | `-- Validation/
| | `-- FluentValidationBridge.razor
| `-- tests/
| `-- BlazorTodoMinimal.Tests/
| |-- BlazorTodoMinimal.Tests.csproj
| `-- TodoDashboardTests.cs
| `-- BlazorProfileValidation.Tests/
| |-- BlazorProfileValidation.Tests.csproj
| `-- ProfileSettingsTests.cs
|-- aspnet-core/
| |-- api-security-in-practice/
| | |-- ApiSecurityMinimal.slnx
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<Solution>
<Folder Name="/src/">
<Project Path="src/BlazorProfileValidation/BlazorProfileValidation.csproj" />
</Folder>
<Folder Name="/tests/">
<Project Path="tests/BlazorProfileValidation.Tests/BlazorProfileValidation.Tests.csproj" />
</Folder>
</Solution>
216 changes: 216 additions & 0 deletions blazor/forms-validation-masterclass/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
# Blazor Profile Validation Pipeline — Minimal Sample

A focused .NET 10 Interactive Server companion demonstrating manual
`EditContext` ownership, DataAnnotations, FluentValidation, backend field-error
mapping, accessible reusable inputs, dirty-state tracking, save/discard behavior,
and bUnit component tests.

## Full tutorial

[Blazor .NET 8 Forms & Validation: EditForm, FluentValidation & Server Error Handling](https://www.dotnet-guide.com/tutorials/blazor/forms-validation-masterclass/)

## Framework note

The tutorial explains Blazor forms on .NET 8.

This companion targets .NET 10 so it can use the DOTNET GUIDE repository's
current SDK and CI toolchain. The `EditContext`, `ValidationMessageStore`,
DataAnnotations, FluentValidation, field-error mapping, dirty-state,
reusable-input, and component-testing patterns demonstrated here are the same
core Blazor concepts.

## What this sample demonstrates

- a Blazor Web App using Interactive Server;
- a manually created `EditContext`;
- DataAnnotations validation;
- FluentValidation 12 rules;
- a local FluentValidation-to-EditContext bridge;
- whole-model validation on submit;
- property-specific validation on field change;
- backend-returned field errors;
- `ValidationMessageStore`;
- clearing stale backend errors after field edits;
- reusable accessible text inputs;
- `aria-invalid` and `aria-describedby`;
- dirty-state detection with `IsModified()`;
- save and discard behavior;
- `MarkAsUnmodified()` after successful save;
- eight bUnit tests;
- one ASP.NET Core unknown-route integration test.

## Why this sample does not use Blazored.FluentValidation

FluentValidation doesn't provide first-party Blazor integration.

The formerly common `Blazored.FluentValidation` adapter is archived.

This sample keeps the integration visible by using a small local component based
on:

- `EditContext.OnValidationRequested`;
- `EditContext.OnFieldChanged`;
- `ValidationMessageStore`;
- `IValidator<T>`;
- `IncludeProperties`.

The bridge supports synchronous FluentValidation rules only.

## Validation sources

```text
DataAnnotations
required, length, format

FluentValidation
conditional and cross-field rules

ProfileService
backend-only reserved username and blocked email-domain rules
```

Each source owns a separate validation-message store.

## Backend boundary

`ProfileService` is an in-process backend simulation.

It returns a dictionary of field names and messages so the component can
demonstrate backend-error mapping.

It does not make HTTP requests or deserialize RFC 7807 responses.

## Render-mode boundary

The sample uses Interactive Server.

The browser must maintain an active Blazor circuit.

## State boundary

The profile service is scoped and stores data only in process memory.

Restarting the application resets the saved profile.

## Prerequisite

- .NET 10 SDK
- a modern browser for optional manual interaction checks

## Restore, build, and test

```powershell
dotnet restore `
.\BlazorProfileValidation.slnx

dotnet build `
.\BlazorProfileValidation.slnx `
--configuration Release `
--no-restore

dotnet test `
.\BlazorProfileValidation.slnx `
--configuration Release `
--no-build
```

## Run

```powershell
dotnet run `
--project .\src\BlazorProfileValidation\BlazorProfileValidation.csproj `
--urls http://localhost:5144
```

Open:

```text
http://localhost:5144/
```

## Demonstration backend errors

Use these valid client-side values to trigger backend-only errors:

```text
Username: reserved
Email: any-address@blocked.example
```

## Testing boundary

The test suite contains:

- eight bUnit component tests;
- one ASP.NET Core integration test for direct unknown-route handling.

The local FluentValidation bridge supports explicitly declared cross-field
dependencies (Username changes revalidate DisplayName). Dependencies are
declared in the page component, not inferred automatically.

The tests do not launch a graphical browser or establish a real browser-driven
SignalR session.

## Project structure

```text
BlazorProfileValidation.slnx
README.md
src/
└── BlazorProfileValidation/
├── BlazorProfileValidation.csproj
├── Program.cs
├── Models/
│ └── ProfileModel.cs
├── Services/
│ └── ProfileService.cs
├── Validation/
│ └── ProfileValidator.cs
└── Components/
├── _Imports.razor
├── App.razor
├── Routes.razor
├── Pages/
│ ├── NotFound.razor
│ └── ProfileSettings.razor
├── Shared/
│ └── FormTextField.razor
└── Validation/
└── FluentValidationBridge.razor
tests/
└── BlazorProfileValidation.Tests/
├── BlazorProfileValidation.Tests.csproj
└── ProfileSettingsTests.cs
```

## Deliberately omitted

- third-party Blazor validation adapters;
- async validation;
- username API calls;
- debounce;
- JavaScript focus management;
- navigation guards;
- optimistic UI;
- authentication;
- databases;
- WebAssembly;
- browser automation;
- Docker;
- production persistence.

These topics remain in the complete tutorial.

## Verification

- Companion target framework: .NET 10
- Tutorial framework: .NET 8
- Render mode: Interactive Server
- FluentValidation integration: local bridge
- External services required: none
- Database required: none
- API keys required: none
- Expected tests: 9
- Last reviewed: 2026-08-02

This sample is educational and should be reviewed before production use.
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>

<ItemGroup>
<PackageReference
Include="FluentValidation.DependencyInjectionExtensions"
Version="12.1.1" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />

<meta name="viewport"
content="width=device-width, initial-scale=1" />

<base href="/" />

<HeadOutlet @rendermode="InteractiveServer" />
</head>
<body>
<Routes @rendermode="InteractiveServer" />

<script src="_framework/blazor.web.js"></script>
</body>
</html>
Loading
Loading