Repository navigation
Description
Background and Motivation
Today, Blazor's entire form validation pipeline is synchronous. EditContext.Validate() fires a synchronous OnValidationRequested event and returns bool. Developers who need async validation — such as database uniqueness checks, remote API calls, or service-based business rule validation — would need to block on async operations using .Result or .GetAwaiter().GetResult(), which is incompatible with Blazor WebAssembly and causes deadlocks on Blazor Server. Third-party libraries like FluentValidation, which has first-class async support, cannot fully integrate with Blazor's EditContext because the event model is synchronous.
Additionally, async validation APIs are being added to System.ComponentModel.DataAnnotations in the BCL (dotnet/runtime#121536); the Blazor form infrastructure needs to support these when they land.
Unlike synchronous validation, async validation introduces a period where the outcome is unknown. The UI needs to communicate this to users (e.g., spinners, disabling submit) and distinguish infrastructure failures from validation errors — requiring the framework to expose per-field pending and faulted state.
This proposal adds:
- Per-field async validation task tracking with pending/faulted state and cancellation
- Programmatic state queries (
IsValidationPending,IsValidationFaulted) for UI feedback - An async event (
OnValidationRequestedAsync) for validator components to subscribe to EditContext.ValidateAsync(CancellationToken)— an async counterpart to the existing syncValidate()- Extending
Validate()to invoke async handlers as well, throwing if any does not complete synchronously, so calls to the existing API do not silently skip async validators
Issue: dotnet/aspnetcore#7680
Proposed API
namespace Microsoft.AspNetCore.Components.Forms;
public sealed class EditContext
{
+ /// <summary>
+ /// Registers an async validation task for a specific field. If a task is already
+ /// tracked for this field, the previous task is cancelled and replaced.
+ /// </summary>
+ public void AddValidationTask(
+ in FieldIdentifier fieldIdentifier,
+ Task task,
+ CancellationTokenSource cts);
+ public bool IsValidationPending(in FieldIdentifier fieldIdentifier);
+ public bool IsValidationPending<TField>(Expression<Func<TField>> accessor);
+ public bool IsValidationPending();
+ public bool IsValidationFaulted(in FieldIdentifier fieldIdentifier);
+ public bool IsValidationFaulted<TField>(Expression<Func<TField>> accessor);
+ public bool IsValidationFaulted();
// Existing sync event (unchanged)
public event EventHandler<ValidationRequestedEventArgs>? OnValidationRequested;
+ /// <summary>
+ /// Async event raised when validation is requested. Validator components subscribe
+ /// to perform async validation (e.g., database lookups, remote API calls).
+ /// For a given validator, subscribe to either <see cref="OnValidationRequested"/> or
+ /// <see cref="OnValidationRequestedAsync"/>, not both, to avoid running validation twice
+ /// per pass. All handlers are invoked and awaited by <see cref="ValidateAsync"/>.
+ /// </summary>
+ public event Func<object, ValidationRequestedEventArgs, Task>? OnValidationRequestedAsync;
// Existing sync method — also invokes OnValidationRequestedAsync handlers and throws
// InvalidOperationException if any handler returns an incomplete Task.
public bool Validate();
+ /// <summary>
+ /// Validates this EditContext asynchronously. Cancels any pending field-level validation
+ /// tasks, invokes both sync <see cref="OnValidationRequested"/> and async
+ /// <see cref="OnValidationRequestedAsync"/> handlers, and returns whether the model is
+ /// valid. The cancellation token is surfaced to handlers via
+ /// <see cref="ValidationRequestedEventArgs.CancellationToken"/> and bounds the in-flight
+ /// pass only.
+ /// </summary>
+ public Task<bool> ValidateAsync(CancellationToken cancellationToken = default);
}
public sealed class ValidationRequestedEventArgs : EventArgs
{
+ public ValidationRequestedEventArgs(CancellationToken cancellationToken);
+ /// <summary>
+ /// Token signalled when the caller of <see cref="EditContext.ValidateAsync"/> requests
+ /// cancellation. Async handlers performing long-running work should pass it to their
+ /// downstream APIs.
+ /// </summary>
+ public CancellationToken CancellationToken { get; }
}namespace Microsoft.Extensions.Validation;
public class ValidationOptions
{
+ /// <summary>
+ /// Tries to resolve the <see cref="ValidatablePropertyInfo"/> for the specified
+ /// property of the specified type. Walks base types so inherited properties are
+ /// resolved correctly.
+ /// </summary>
+ [Experimental("ASP0029")]
+ public bool TryGetValidatablePropertyInfo(
+ Type containingType,
+ string propertyName,
+ [NotNullWhen(true)] out ValidatablePropertyInfo? validatablePropertyInfo);
}CSS classes
InputBase automatically applies "pending" and "faulted" CSS classes via FieldCssClassProvider, following the existing compound class pattern (e.g., "modified pending", "modified faulted"). Custom FieldCssClassProvider subclasses can map these states to any CSS framework classes (e.g., "is-validating", "is-warning" for Bootstrap).
API details
AddValidationTask(FieldIdentifier, Task, CancellationTokenSource) — Registers an in-flight async validation task for a specific field. If a task is already tracked for the field, the previous task's CancellationTokenSource is cancelled and disposed, and the new task replaces it. The task is automatically removed from tracking when it completes. On completion: cancellation (OperationCanceledException) is silent; any other exception marks the field as faulted. State changes trigger NotifyValidationStateChanged().
IsValidationPending — Returns true when a tracked async validation task is still running (Task.IsCompleted == false). Two per-field variants are provided: IsValidationPending(in FieldIdentifier) and IsValidationPending<TField>(Expression<Func<TField>>). The parameterless IsValidationPending() returns true only while a form-level ValidateAsync() pass is in flight.
IsValidationFaulted — Returns true when a tracked async validation task threw a non-cancellation exception. "Faulted" means the infrastructure failed to determine validity — it does not mean the field's value is invalid. The faulted state is cleared when the field is re-edited (a new task replaces the faulted one via AddValidationTask) and at the start of the next form-level pass. Two per-field variants are provided: IsValidationFaulted(in FieldIdentifier) and IsValidationFaulted<TField>(Expression<Func<TField>>). The parameterless IsValidationFaulted() reflects the most recent form-level ValidateAsync() pass only.
OnValidationRequestedAsync — Async counterpart to the existing sync OnValidationRequested. Declared as event Func<object, ValidationRequestedEventArgs, Task>?, supporting multiple subscribers via multicast. ValidateAsync() invokes all handlers via GetInvocationList() and awaits them concurrently with Task.WhenAll.
ValidateAsync(CancellationToken) — Cancels all pending field-level tasks first (form-submit validation supersedes them) and clears any lingering per-field fault flags from prior passes, fires the sync OnValidationRequested event, then fires and awaits all OnValidationRequestedAsync handlers, and returns true if the validation did not produce any error messages and did not throw. EditForm.HandleSubmitAsync() calls this instead of Validate().
Validate() — Fires OnValidationRequested synchronously and also invokes OnValidationRequestedAsync handlers; if any handler returns an incomplete Task the method throws InvalidOperationException directing the caller to ValidateAsync().
ValidationRequestedEventArgs.CancellationToken — New property and constructor on the existing event-args type. Validate() passes the shared Empty instance (no token); ValidateAsync(CancellationToken) constructs an args instance carrying the caller's token. Async handlers should observe the token when performing long-running work.
ValidationOptions.TryGetValidatablePropertyInfo(Type, string, out ValidatablePropertyInfo) — Resolves the ValidatablePropertyInfo for a named property on a given containing type, walking base types so inherited properties are resolved correctly. This is needed to support per-field async validation in Microsoft.Extensions.Validation — when DataAnnotationsValidator handles a field change, it needs to look up the validation metadata for that specific property rather than re-validating the entire object graph.
Usage Examples
Implicit submit validation. Existing forms with async validators work without code changes — EditForm internally calls ValidateAsync() instead of Validate():
<EditForm Model="@registration" OnValidSubmit="HandleValidSubmit">
<DataAnnotationsValidator />
<InputText @bind-Value="registration.Username" />
<button type="submit">Register</button>
<ValidationSummary />
</EditForm>
@code {
private Registration registration = new();
private async Task HandleValidSubmit(EditContext context)
{
// This only fires after ALL validation (including async) has passed
await UserService.CreateAsync(registration);
}
}Explicit submit validation. Developers using OnSubmit call ValidateAsync():
<EditForm Model="@registration" OnSubmit="HandleSubmit">
<DataAnnotationsValidator />
<!-- ... -->
</EditForm>
@code {
private async Task HandleSubmit(EditContext context)
{
var isValid = await context.ValidateAsync();
if (isValid)
{
await UserService.CreateAsync(registration);
}
}
}Programmatic pending/faulted state query for custom UI. The expression-based overloads provide a concise syntax matching ValidationMessage:
<div class="form-group">
<InputText @bind-Value="registration.Username" />
@if (editContext.IsValidationPending(() => registration.Username))
{
<span class="spinner">Checking availability…</span>
}
else if (editContext.IsValidationFaulted(() => registration.Username))
{
<span class="text-warning">An error occurred while validating. Please try again.</span>
}
else
{
<ValidationMessage For="() => registration.Username" />
}
</div>
<button type="submit"
disabled="@editContext.IsValidationPending()">
Register
</button>Third-party validator integration. Library authors subscribe to the async event and use AddValidationTask for field-level tracking:
public class FluentValidationValidator : ComponentBase
{
[CascadingParameter] EditContext EditContext { get; set; }
protected override void OnInitialized()
{
// Form-submit: subscribe to the async validation event
EditContext.OnValidationRequestedAsync += async (sender, args) =>
{
var validator = GetValidatorForModel(EditContext.Model);
var result = await validator.ValidateAsync(EditContext.Model);
// Populate ValidationMessageStore with results
};
// Per-field: start async validation and register for tracking/cancellation
EditContext.OnFieldChanged += (sender, args) =>
{
var cts = new CancellationTokenSource();
var task = RunFieldValidationAsync(args.FieldIdentifier, cts.Token);
EditContext.AddValidationTask(args.FieldIdentifier, task, cts);
};
}
}Alternative Designs
Behavior of the existing sync Validate(). The current proposal extends Validate() to invoke OnValidationRequestedAsync handlers and throw InvalidOperationException if any handler does not complete synchronously.
This matches how the interaction between EditForm.Validate and DataAnnotationValidator works in .NET 10. Since EditForm internally calls ValidateAsync(), most developers will not need to interact with Validate() directly.
In addition, we considered two changes (not chosen for now):
// Change A: mark Validate() obsolete
[Obsolete("Use ValidateAsync() instead.")]
public bool Validate();
// Change B: optional parameter to opt out of async handlers
var isValid = editContext.Validate(); // throws if model has async validators
var isValid = editContext.Validate(syncOnly: true); // skips async validators, no throw
var isValid = await editContext.ValidateAsync(); // full validation including asyncDeclarative UI for per-field validation state. The current proposal only provides the programmatic API (IsValidationPending, IsValidationFaulted) for displaying pending/faulted state — developers must write @if blocks to conditionally render spinners or error messages. A declarative API could reduce this boilerplate. Two options are under consideration:
-
Optional render fragment slots on
ValidationMessage.ValidationMessagecould acceptPendingContentandFaultedContentrender fragments. When async validation is pending,PendingContentis rendered instead of error messages; when faulted,FaultedContentis rendered. When neither fragment is provided, behavior is unchanged.<ValidationMessage For="() => registration.Username"> <PendingContent> <span class="spinner">Checking availability…</span> </PendingContent> <FaultedContent> <span class="text-warning">An error occurred while validating. Please try again.</span> </FaultedContent> </ValidationMessage>
Note:
ValidationMessagecurrently does not accept child content — it renders a flat list of<div class="validation-message">elements. Adding render fragments would be a change to its rendering behavior. -
Separate built-in components. New components such as
<ValidationPendingIndicator For="() => registration.Username">and<ValidationFaultedIndicator>could be introduced alongside the existing<ValidationMessage>, keeping each component's responsibility focused.
This is not part of the current proposal but could be added as a follow-up.
Risks
- BCL dependency. The async validation path for simple models (without
AddValidation()) depends onValidator.TryValidateObjectAsyncandValidator.TryValidatePropertyAsync(or similar APIs), which do not exist yet inSystem.ComponentModel.DataAnnotations(dotnet/runtime#121536). Until those ship, theMicrosoft.Extensions.Validationpath viaAddValidation()is required for async validators.
Activity
dotnet-policy-service commentedon Apr 20, 2026
Thank you for submitting this for API review. This will be reviewed by @dotnet/aspnet-api-review at the next meeting of the ASP.NET Core API Review group. Please ensure you take a look at the API review process documentation and ensure that:
github-actions commentedon Apr 20, 2026
Triage Summary
Area:
area-blazor✅ (already labeled — proposal targetsEditContextand Blazor form infrastructure inMicrosoft.AspNetCore.Components.Forms)Type:
api-proposal— well-structured formal API proposal with proposed API surface, detailed semantics, usage examples, alternatives considered, and a linked specPotential Duplicates
Notes
api-ready-for-review, indicating it has been prepared for the formal API review process.Microsoft.Extensions.Validation(ValidatableTypeInfo.GetProperty) which overlaps with thefeature-validationlabel already applied.DataAnnotationsValidatorasync path; theMicrosoft.Extensions.Validationpath viaAddValidation()is the interim solution.api-proposallabel to align with the standard label used for formal API proposals in this repo.halter73 commentedon Apr 29, 2026
Also take a look at dotnet/designs#363 which introduces the
AsyncValidationAttributetype and then updates the various validation pipelines in an async manner in the safest/most-backward-compatible way that I can currently think of. Feel free to comment on that PR and leave feedback, before we turn it into a more full-blown API proposal.oroztocil commentedon Apr 30, 2026
@halter73
The API proposal here is for the Blazor form validation UX, which is independent of the DataAnnotations API (e.g., you can use it with FluentValidation or your custom async code). As such, it is fully compatible with your proposal and any similar API shape.
The only requirement is the ability of the Microsoft.Extensions.Validation pipeline to run sync validators first, async later - which is not an API change but behavioral change (with only an indirect consequence for the API design, i.e. it needs to be possible to tell in advance if a validation attribute is sync or async). I'll add the behavioral notes to your PR.