Blazor SSR Gets Client-Side Validation in .NET 11 Preview 5
If you've built Blazor Server-Side Rendering (SSR) forms, you know the pain: a user fills out a form, hits submit, the form posts to the server, the server runs validation, and only then does the user see the "This field is required" message next to the empty email field.
That round-trip latency adds up. It breaks the immediacy users expect from modern web apps.
.NET 11 Preview 5 fixes this. Blazor SSR forms now get instant, in-browser validation feedback — no server required. The server renders your validation rules as metadata, and Blazor's JavaScript enforces them client-side. Same DataAnnotationsValidator component you already use. Zero code changes needed.
Let's break down how it works.
Before .NET 11: The SSR Validation Gap
In .NET 8 and 9, Blazor SSR rendered HTML on the server and sent it down. Validation only ran server-side — on form submission. If a field was invalid, the whole form posted to the server, came back with validation messages, and re-rendered.
Interactive Blazor modes (Server, WebAssembly, Auto) had instant client-side validation because an active SignalR circuit or WASM runtime ran the validation logic locally. But SSR mode — the simplest, most performant option — was left out.
The result? Developers who chose SSR Blazor for its simplicity had to choose between:
- Accepting the laggy validation UX
- Adding a second JavaScript validation library (and maintaining two validation rulesets)
- Re-architecting to use an interactive render mode
None of these are great options.
What Changed in .NET 11 Preview 5
The .NET team shipped two PRs (#66441 and #66420) that bring unobtrusive client-side validation to Blazor SSR forms.
The key insight: The .NET model stays the single source of truth. On form render, the server serializes your DataAnnotations validation rules into HTML metadata attributes. Blazor's JavaScript reads those attributes and applies them client-side — the same approach ASP.NET MVC has used for years with jquery.validate.unobtrusive.
How It Works
Server renders metadata — When an
<EditForm>with<DataAnnotationsValidator />renders in SSR mode, the server emitsdata-val-*attributes for each field's validation rules (required, string length, regex, compare, email, etc.)Client enforces — Blazor's built-in JavaScript intercepts form submission and validates fields against the metadata. If a field fails, it shows the
ValidationMessageimmediately — no POST needed.Server re-validates on submit — When the form does post to the server (after passing client-side validation), the server validates again as a safety net. Defense in depth.
What You Need
Nothing. The feature is enabled by default for all SSR forms that include the <DataAnnotationsValidator /> component. Both enhanced forms (Enhance) and non-enhanced forms work.
Code Example: Registration Form (SSR)
Here's a standard Blazor SSR registration form with DataAnnotationsValidator. This exact code works with or without client-side validation in .NET 11 Preview 5:
@page "/register"
@using System.ComponentModel.DataAnnotations
<EditForm Model="Model" Enhance FormName="registration" OnValidSubmit="HandleValidSubmit">
<DataAnnotationsValidator />
<div>
<label for="Email">Email</label>
<InputText @bind-Value="Model!.Email" id="Email" />
<ValidationMessage For="() => Model!.Email" />
</div>
<div>
<label for="Password">Password</label>
<InputText @bind-Value="Model!.Password" id="Password" type="password" />
<ValidationMessage For="() => Model!.Password" />
</div>
<div>
<label for="ConfirmPassword">Confirm Password</label>
<InputText @bind-Value="Model!.ConfirmPassword" id="ConfirmPassword" type="password" />
<ValidationMessage For="() => Model!.ConfirmPassword" />
</div>
<button type="submit">Register</button>
</EditForm>
@code {
[SupplyParameterFromForm]
private RegistrationModel? Model { get; set; }
protected override void OnInitialized() => Model ??= new();
private void HandleValidSubmit()
{
// Form is valid — process registration
}
}
public class RegistrationModel
{
[Required]
[EmailAddress]
public string? Email { get; set; }
[Required]
[StringLength(100, MinimumLength = 8)]
public string? Password { get; set; }
[Required]
[Compare("Password")]
[Display(Name = "Confirm Password")]
public string? ConfirmPassword { get; set; }
}
In .NET 11 Preview 5, when this form renders, the server emits something like:
<input type="email" id="Email"
data-val="true"
data-val-required="The Email field is required."
data-val-email="The Email field is not a valid email address." />
<span data-valmsg-for="Email" data-valmsg-replace="true"></span>
<input type="password" id="Password"
data-val="true"
data-val-required="The Password field is required."
data-val-length="The Password must be at least 8 characters."
data-val-length-max="100"
data-val-length-min="8" />
The JS intercepts the submit, runs these rules on field blur and on submit, and the validation messages appear instantly.
What About Enhanced Forms?
Blazor SSR enhanced forms (those with the Enhance attribute on <EditForm>) use form enhancement — the form posts via fetch and patches the DOM with the response. In previous releases, the validation experience was:
- User fills form
- User clicks submit
- Form posts via fetch
- Server validates
- Response patches the DOM with validation errors
With .NET 11 Preview 5, enhanced forms also get client-side validation as a first pass — the form validates locally before the fetch even fires. This means:
- Before .NET 11 P5: Submit → fetch (latency) → validation feedback
- After .NET 11 P5: Instant validation → submit → fetch → server re-validation
Non-enhanced forms work identically — just without the fetch patching.
Bonus: Async Form Validation
Preview 5 also ships the building blocks for async validation in Blazor forms. You can now register per-field async validation tasks via EditContext.AddValidationTask:
@implements IDisposable
<EditForm EditContext="editContext" OnSubmit="HandleSubmit">
<InputText @bind-Value="model.Username" />
@if (editContext.IsValidationPending(() => model.Username))
{
<span>Checking availability...</span>
}
<ValidationMessage For="() => model.Username" />
<button type="submit">Register</button>
</EditForm>
@code {
[Inject] public UserService Users { get; set; } = default!;
private readonly RegistrationModel model = new();
private EditContext editContext = default!;
private ValidationMessageStore messages = default!;
protected override void OnInitialized()
{
editContext = new EditContext(model);
messages = new ValidationMessageStore(editContext);
editContext.OnFieldChanged += (_, e) =>
{
if (e.FieldIdentifier.FieldName == nameof(model.Username))
{
var cts = new CancellationTokenSource();
editContext.AddValidationTask(e.FieldIdentifier,
CheckAsync(e.FieldIdentifier, model.Username, cts.Token), cts);
}
};
}
private async Task CheckAsync(FieldIdentifier field, string value, CancellationToken ct)
{
messages.Clear(field);
if (await Users.IsUsernameTakenAsync(value, ct))
{
messages.Add(field, "Username is taken.");
}
editContext.NotifyValidationStateChanged();
}
private async Task HandleSubmit() => await editContext.ValidateAsync();
public void Dispose() => messages?.Clear();
}
The framework tracks pending tasks, cancels superseded ones (when the user types again), and exposes IsValidationPending(field) and IsValidationFaulted(field) for the UI to show spinner states.
Note: The full built-in async validation experience will land in a later preview once the new async DataAnnotations APIs ship. But the foundation is here now.
Bonus: Localized Validation Errors
Preview 5 also adds first-class localization support for validation messages in Blazor forms and Minimal API endpoints:
builder.Services.AddValidation()
.AddValidationLocalization<ValidationMessages>();
This resolves localized strings from RESX files (or a custom IStringLocalizerFactory) using the ErrorMessage values as keys. You can also configure a programmatic strategy:
builder.Services.AddValidation()
.AddValidationLocalization<ValidationMessages>(options =>
{
options.ErrorMessageKeyProvider = ctx =>
ctx.Attribute.ErrorMessage ?? $"{ctx.Attribute.GetType().Name}_Error";
});
Now your [Required] attribute automatically picks up the right language without changing a line of Razor code.
Migration Guide
If you have existing Blazor SSR forms in a .NET 8, 9, or 10 project:
-
Upgrade the SDK to .NET 11 Preview 5+ (
dotnet --versionshould show11.0.100-preview.5.*) -
Set
LangVersionto preview in your.csproj:
<PropertyGroup>
<TargetFramework>net11.0</TargetFramework>
<LangVersion>preview</LangVersion>
</PropertyGroup>
-
That's it. If you use
<DataAnnotationsValidator />in your SSR forms, client-side validation is now active. Test by tabbing off a required field without filling it — you should see the validation message instantly.
Existing Enhanced Forms
Enhanced forms (Enhance) get the same client-side validation automatically. No changes needed.
What This Means for Blazor Developers
This is a meaningful step forward for Blazor SSR. The biggest critique of SSR forms was the validation UX gap compared to interactive modes. With client-side validation, SSR Blazor forms now feel as responsive as traditional JavaScript frameworks — without requiring JavaScript interop, a separate validation library, or giving up the simplicity of static server rendering.
Who benefits most:
- SSR-first Blazor apps — Public-facing sites, content-heavy apps, and pages where interactivity overhead isn't justified
- Progressively enhanced forms — Forms that work without JavaScript but feel instant with it
- Teams migrating from MVC/Razor Pages — The unobtrusive validation pattern will feel familiar
- Multi-lingual apps — Localized validation messages are now a built-in concern, not an afterthought
Summary
| Feature | .NET 10 | .NET 11 Preview 5 |
|---|---|---|
| SSR client-side validation | ❌ Server-only | ✅ Instant in-browser |
| Async form validation | ❌ | ✅ Building blocks shipped |
| Localized validation | Partial | ✅ First-class via AddValidationLocalization
|
| QuickGrid SSR | ❌ | ✅ Sort/page without interactivity |
| SupplyParameterFromSession | ❌ | ✅ Session-backed component state |
| Dev setup | DevServer | ✅ New Gateway dev server |
.NET 11 Preview 5 turns Blazor SSR into a viable option for form-heavy applications by closing the validation UX gap. The .NET model stays the source of truth, your existing DataAnnotationsValidator components work unchanged, and the result is instant visual feedback for your users.
Have you tried Blazor SSR in production? Does client-side validation change the calculus for your next project? Drop a comment — I'd love to hear your take.
This post was written with .NET 11 Preview 5. Features may change before the final release.
Top comments (0)