Skip to content

[API Proposal] Async form validation support in Blazor #66381

Closed

Description

@oroztocil

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 sync Validate()
  • 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 async

Declarative 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:

  1. Optional render fragment slots on ValidationMessage. ValidationMessage could accept PendingContent and FaultedContent render fragments. When async validation is pending, PendingContent is rendered instead of error messages; when faulted, FaultedContent is 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: ValidationMessage currently 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.

  2. 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 on Validator.TryValidateObjectAsync and Validator.TryValidatePropertyAsync (or similar APIs), which do not exist yet in System.ComponentModel.DataAnnotations (dotnet/runtime#121536). Until those ship, the Microsoft.Extensions.Validation path via AddValidation() is required for async validators.

Activity

  1. added
    api-suggestionEarly API idea and discussion, it is NOT ready for implementation
    area-blazorIncludes: Blazor, Razor Components
    api-ready-for-reviewAPI is ready for formal API review - https://github.com/dotnet/apireviews
    feature-validationIssues related to model validation in minimal and controller-based APIs
    on Apr 20, 2026
  2. dotnet-policy-service commented on Apr 20, 2026

    @dotnet-policy-service
    Contributor

    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:

    • The PR contains changes to the reference-assembly that describe the API change. Or, you have included a snippet of reference-assembly-style code that illustrates the API change.
    • The PR describes the impact to users, both positive (useful new APIs) and negative (breaking changes).
    • Someone is assigned to "champion" this change in the meeting, and they understand the impact and design of the change.
  3. github-actions commented on Apr 20, 2026

    @github-actions
    Contributor

    Triage Summary

    Area: area-blazor ✅ (already labeled — proposal targets EditContext and Blazor form infrastructure in Microsoft.AspNetCore.Components.Forms)
    Type: api-proposal — well-structured formal API proposal with proposed API surface, detailed semantics, usage examples, alternatives considered, and a linked spec

    Potential Duplicates

    • Blazor form async validation enhancements #7680 — "Async Validation support for EditContext" (related — explicitly acknowledged by the author as the original feature request that this proposal addresses; not a duplicate but the motivating predecessor)
    • No other duplicates found

    Notes

    • The issue is already labeled api-ready-for-review, indicating it has been prepared for the formal API review process.
    • The proposal also touches Microsoft.Extensions.Validation (ValidatableTypeInfo.GetProperty) which overlaps with the feature-validation label already applied.
    • Depends on a BCL addition (dotnet/runtime#121536) for the DataAnnotationsValidator async path; the Microsoft.Extensions.Validation path via AddValidation() is the interim solution.
    • Adding api-proposal label to align with the standard label used for formal API proposals in this repo.

    Generated by Issue Triage Agent for dotnet/aspnetcore for issue #66381 · ● 182.4K · ◷

  4. added this to the 11.0-preview5 milestone on Apr 20, 2026
  5. removed
    api-suggestionEarly API idea and discussion, it is NOT ready for implementation
    on Apr 20, 2026
  6. halter73 commented on Apr 29, 2026

    @halter73
    Member

    Also take a look at dotnet/designs#363 which introduces the AsyncValidationAttribute type 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.

  7. oroztocil commented on Apr 30, 2026

    @oroztocil
    MemberAuthor

    @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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    api-proposalapi-ready-for-reviewAPI is ready for formal API review - https://github.com/dotnet/apireviewsarea-blazorIncludes: Blazor, Razor Componentsfeature-validationIssues related to model validation in minimal and controller-based APIs

    Type

    No type

    Projects

    No projects

      Milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions