Skip to content

Add .NET support for client-side validation for Blazor SSR#66441

Draft
oroztocil wants to merge 3 commits intomainfrom
oroztocil/validation-client-side-dotnet
Draft

Add .NET support for client-side validation for Blazor SSR#66441
oroztocil wants to merge 3 commits intomainfrom
oroztocil/validation-client-side-dotnet

Conversation

@oroztocil
Copy link
Copy Markdown
Member

Description

This PR adds support for rendering client-side validation metadata from .NET validation attributes on Blazor SSR forms. This is the .NET-side part of the overall client-side form validation feature for Blazor SSR. It generates data-val-* HTML attributes from System.ComponentModel.DataAnnotations validation attributes so the companion JS library (in a separate PR) can enforce them client-side.

Contributes to #51040
Companion PR to #66420

What this enables

When a Blazor SSR form uses <DataAnnotationsValidator />, input components are automatically rendered with data-val-* attributes derived from model validation attributes ([Required], [Range], [EmailAddress], etc.). The companion JS library reads these attributes and provides instant, in-browser validation feedback without a server round-trip.

Client validation is activated automatically for SSR forms - no additional configuration is needed beyond the standard <DataAnnotationsValidator /> component that developers already use.

How it works

flowchart LR
    subgraph Server["Server (.NET)"]
        Model["<b>Model</b><br/>[Required]<br/>[Range] etc."]
        Service["<b>DefaultClient<br/>ValidationService</b><br/>reflection + caching"]
        Components["<b>InputBase</b> / <b>InputText</b><br/><b>InputRadioGroup</b><br/><b>ValidationMessage</b><br/><b>ValidationSummary</b>"]
        Model --> Service
        Service -->|"generates<br/>data-val-* dict"| Components
    end

    subgraph HTML["Rendered HTML"]
        Input["input data-val='true'<br/>data-val-required='...'"]
        Msg["div data-valmsg-for='...'"]
        Summary["div data-valmsg-summary='true'"]
    end

    Components --> Input
    Components --> Msg
    Components --> Summary
Loading
  1. DataAnnotationsValidator detects SSR mode (AssignedRenderMode is null) and stores the IClientValidationService into EditContext.Properties. This acts as a signal for child components to render client validation attributes.

  2. InputBase (and all derived components like InputText, InputNumber, etc.) reads the service from EditContext.Properties on each render, calls GetClientValidationAttributes(fieldIdentifier), and merges the resulting data-val-* attributes into AdditionalAttributes.

  3. InputRadioGroup / InputRadio - the group component extracts data-val-* attributes and passes them to child radio buttons via InputRadioContext. All radio buttons in the group receive the attributes; the JS library deduplicates by tracking one radio per name group.

  4. ValidationMessage renders data-valmsg-for="{fieldName}" and data-valmsg-replace="true" so the JS library can find and update the error message element.

  5. ValidationSummary renders data-valmsg-summary="true" so the JS library can find and populate the summary container.

sequenceDiagram
    participant DAV as DataAnnotationsValidator
    participant EC as EditContext.Properties
    participant IB as InputBase / InputText
    participant DCVS as DefaultClientValidationService

    Note over DAV: SSR render mode only
    DAV->>EC: Store IClientValidationService

    IB->>EC: Read IClientValidationService
    IB->>DCVS: GetClientValidationAttributes(fieldIdentifier)
    DCVS-->>IB: { "data-val": "true", "data-val-required": "..." }
    IB->>IB: Merge into AdditionalAttributes (first-wins)
Loading

Key design decisions

SSR-only activation. Client validation attributes are only rendered when AssignedRenderMode is null (static SSR). Interactive Blazor components use EditContext for server-side validation - adding data-val-* attributes would be redundant and could conflict with interactive validation state.

EditContext.Properties as communication channel. Rather than adding a new cascading parameter or modifying InputBase's parameter signature (which would be a breaking change), the IClientValidationService is stored in EditContext.Properties. This is an existing extensibility mechanism that doesn't require changes to component contracts.

First-wins attribute merging. When InputBase merges data-val-* attributes into AdditionalAttributes, it uses TryAdd (first-wins). This means developer-specified attributes take precedence - if a developer manually sets data-val-required in AdditionalAttributes, the generated value is not overwritten.

Singleton with caching. DefaultClientValidationService is registered as a singleton and caches attribute dictionaries per (Type, FieldName). Validation attributes are read from reflection once and reused across all requests.

Supported validation attributes

DefaultClientValidationService generates data-val-* attributes for these standard System.ComponentModel.DataAnnotations attributes:

.NET Attribute Generated data-val-* attributes
[Required] data-val-required
[StringLength] data-val-length, -min, -max
[MaxLength] data-val-maxlength, -max
[MinLength] data-val-minlength, -min
[Range] data-val-range, -min, -max
[RegularExpression] data-val-regex, -pattern
[Compare] data-val-equalto, -other
[EmailAddress] data-val-email
[Url] data-val-url
[Phone] data-val-phone
[CreditCard] data-val-creditcard
[FileExtensions] data-val-fileextensions, -extensions
Custom IClientValidationAdapter Developer-defined attributes

New public API surface

New namespace: Microsoft.AspNetCore.Components.Forms.ClientValidation

// Service that generates data-val-* HTML attributes from validation attributes
public interface IClientValidationService
{
    IReadOnlyDictionary<string, object>? GetClientValidationAttributes(FieldIdentifier fieldIdentifier);
}

// Extensibility interface for custom ValidationAttribute subclasses
public interface IClientValidationAdapter
{
    void AddClientValidationAttributes(in ClientValidationContext context);
}

// Context for custom adapters (ref struct for zero-allocation)
public readonly ref struct ClientValidationContext
{
    public string ErrorMessage { get; }
    public void MergeAttribute(string key, string value);
}

Existing namespace: Microsoft.AspNetCore.Components.Forms

// New parameter on DataAnnotationsValidator
public class DataAnnotationsValidator
{
    [Parameter] public bool EnableClientValidation { get; set; } = true;
}

Existing namespace: Microsoft.Extensions.DependencyInjection

// Called by AddRazorComponents automatically
// Needs to be public for dependency reasons (to avoid making new internal types public)
public static class ClientValidationServiceCollectionExtensions
{
    public static IServiceCollection AddClientValidation(this IServiceCollection services);
}

Custom validation attribute example

public class NotEqualToAttribute : ValidationAttribute, IClientValidationAdapter
{
    public string OtherProperty { get; }
    public NotEqualToAttribute(string otherProperty) => OtherProperty = otherProperty;

    protected override ValidationResult? IsValid(object? value, ValidationContext context) { /* server logic */ }

    public void AddClientValidationAttributes(in ClientValidationContext context)
    {
        context.MergeAttribute("data-val-notequalto", context.ErrorMessage);
        context.MergeAttribute("data-val-notequalto-other", "*." + OtherProperty);
    }
}

Usage

No special setup is needed for standard Blazor SSR apps. Client validation activates automatically when <DataAnnotationsValidator /> is present in an SSR form:

<EditForm Model="model" FormName="registration" method="post">
    <DataAnnotationsValidator />
    <ValidationSummary />

    <InputText @bind-Value="model.Name" />
    <ValidationMessage For="() => model.Name" />

    <InputText @bind-Value="model.Email" />
    <ValidationMessage For="() => model.Email" />

    <button type="submit">Submit</button>
</EditForm>

Rendered HTML (SSR):

<form method="post">
    <div data-valmsg-summary="true" class="validation-summary-valid">
        <ul class="validation-errors"></ul>
    </div>

    <input name="model.Name" type="text"
           data-val="true"
           data-val-required="The Name field is required." />
    <div class="validation-message"
         data-valmsg-for="model.Name"
         data-valmsg-replace="true"></div>

    <input name="model.Email" type="email"
           data-val="true"
           data-val-required="The Email field is required."
           data-val-email="The Email field is not a valid e-mail address." />
    <div class="validation-message"
         data-valmsg-for="model.Email"
         data-valmsg-replace="true"></div>

    <button type="submit">Submit</button>
</form>

To disable client validation for a specific form:

<DataAnnotationsValidator EnableClientValidation="false" />

Testing

25 unit tests covering:

  • All 12 supported validation attributes with correct data-val-* generation
  • StringLength with max-only (verifies min attribute is omitted when MinimumLength is 0)
  • Range with double values (verifies invariant culture formatting)
  • Multiple attributes on a single property
  • Properties with no validation attributes (returns null)
  • Non-existent properties (returns null)
  • [Display] and [DisplayName] integration in error messages
  • Fallback to property name when no display attribute is present
  • Result caching (same dictionary instance returned for same type+field)
  • Custom IClientValidationAdapter implementation
  • Nested model support (validation attributes on nested type properties)
  • Inherited validation attributes from base classes

Blazor SSR E2E tests will be provided either in a follow up PR, or added to this PR once the JS library PR is merged.

@github-actions github-actions Bot added the area-blazor Includes: Blazor, Razor Components label Apr 23, 2026
@oroztocil oroztocil force-pushed the oroztocil/validation-client-side-dotnet branch from bf49a46 to 3141a7b Compare April 23, 2026 17:26
@oroztocil oroztocil requested a review from Copilot April 23, 2026 17:28
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds .NET-side support for Blazor SSR client-side validation by emitting unobtrusive data-val-* attributes from System.ComponentModel.DataAnnotations, plus message/summary hooks for a companion JS library.

Changes:

  • Introduces Microsoft.AspNetCore.Components.Forms.ClientValidation APIs and a default implementation that reflects over validation attributes and caches generated data-val-* dictionaries.
  • Activates SSR-only client validation via DataAnnotationsValidator and EditContext.Properties, and merges generated attributes into InputBase (including radio groups).
  • Updates ValidationMessage/ValidationSummary rendering to output data-valmsg-* markers, and adds unit tests for the default attribute generator.

Reviewed changes

Copilot reviewed 14 out of 14 changed files in this pull request and generated 11 comments.

Show a summary per file
File Description
src/Components/Web/src/Forms/ValidationSummary.cs Adds SSR client-validation summary container (data-valmsg-summary) rendering.
src/Components/Web/src/Forms/ValidationMessage.cs Adds SSR client-validation message placeholder (data-valmsg-for, data-valmsg-replace) rendering.
src/Components/Web/src/Forms/InputRadioGroup.cs Extracts data-val-* attributes from group and passes them to child radios.
src/Components/Web/src/Forms/InputRadioContext.cs Adds cascading storage for client validation attributes for radio children.
src/Components/Web/src/Forms/InputRadio.cs Applies group-provided data-val-* attributes to each rendered radio input.
src/Components/Web/src/Forms/InputBase.cs Merges generated data-val-* attributes into AdditionalAttributes when client validation is enabled.
src/Components/Forms/test/DefaultClientValidationServiceTest.cs Adds unit tests for attribute generation/caching/custom adapters.
src/Components/Forms/src/PublicAPI.Unshipped.txt Declares new public APIs for client validation + new EnableClientValidation parameter.
src/Components/Forms/src/DataAnnotationsValidator.cs Enables SSR-only client validation by storing IClientValidationService in EditContext.Properties.
src/Components/Forms/src/ClientValidation/IClientValidationService.cs New public service contract for generating data-val-* attributes.
src/Components/Forms/src/ClientValidation/IClientValidationAdapter.cs New public extensibility interface for custom validation attributes.
src/Components/Forms/src/ClientValidation/DefaultClientValidationService.cs Default reflection-based generator with caching for supported validation attributes.
src/Components/Forms/src/ClientValidation/ClientValidationServiceCollectionExtensions.cs DI extension to register the default client validation service.
src/Components/Forms/src/ClientValidation/ClientValidationContext.cs Context passed to custom adapters for emitting attributes.

Comment thread src/Components/Web/src/Forms/ValidationMessage.cs Outdated
Comment thread src/Components/Web/src/Forms/InputRadioGroup.cs
Comment thread src/Components/Web/src/Forms/ValidationSummary.cs Outdated
Comment thread src/Components/Forms/src/DataAnnotationsValidator.cs
Comment thread src/Components/Web/src/Forms/ValidationMessage.cs Outdated
Comment thread src/Components/Web/src/Forms/ValidationMessage.cs Outdated
Comment thread src/Components/Web/src/Forms/ValidationMessage.cs
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 14 out of 14 changed files in this pull request and generated 2 comments.

Comment on lines 70 to +75
_subscriptions?.Dispose();
_subscriptions = null;

// Clean up the client validation service reference from EditContext
CurrentEditContext?.Properties.Remove(typeof(IClientValidationService));

Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DataAnnotationsValidator unconditionally removes the IClientValidationService key from EditContext.Properties on dispose. If something else placed a service under the same key (or if multiple validators somehow target the same EditContext), this can remove a value that this instance didn’t set. Consider tracking whether this instance added the entry and/or only removing when the stored value matches the injected ClientValidationService instance.

Copilot uses AI. Check for mistakes.
Comment thread src/Components/Forms/src/PublicAPI.Unshipped.txt Outdated
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-blazor Includes: Blazor, Razor Components

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants