Skip to content

[API Proposal] Client-side validation for Blazor static SSR forms #67800

Description

@oroztocil

Background and Motivation

Blazor forms rendered with static server-side rendering (SSR) validate only on the server: the user fills the form, submits, the request round-trips, and errors come back on the next render. This proposal adds APIs needed to support client-side validation that provides instant, no-round-trip feedback to static SSR forms similar to validation in the interactive render modes.

The feature is enabled by adding <DataAnnotationsValidator /> to an EditForm on a static SSR page. This activates client-side validation based on DataAnnotations attributes used in the form model. The framework serializes the form's validation rules into the page, and a small validation engine shipped inside blazor.web.js enforces them in the browser (blocking invalid submits and showing messages) prior to the existing server-side validation. Rules are emitted only for fields the server would also validate, so the client never rejects a value the server would accept. With JavaScript disabled the form still validates on the server exactly as before.

Contributes to #51040.

Proposed API

1. Enabled by default, opt-out per form

 namespace Microsoft.AspNetCore.Components.Forms;

 public class DataAnnotationsValidator : ComponentBase
 {
+    // Opts the surrounding form in or out of client-side validation. Default: true.
+    [Parameter] public bool EnableClientValidation { get; set; }
 }

Client-side validation is on by default whenever a DataAnnotationsValidator is present in a static SSR form. Set EnableClientValidation="false" to keep server-only validation for that form.

2. Supporting custom validation attributes

A custom ValidationAttribute opts into client-side validation by implementing IClientValidationAdapter and returning one or more ClientValidationRules. A rule names a JavaScript validator (registered via Blazor.formValidation.addValidator) and carries the string parameters that validator needs. The framework attaches the attribute's resolved (and, when a localizer is configured, localized) error message, so adapters describe only the rule shape, not the message.

 namespace Microsoft.AspNetCore.Components.Forms;

+// Implemented by a ValidationAttribute to contribute client-side validation rules.
+public interface IClientValidationAdapter
+{
+    IEnumerable<ClientValidationRule> GetClientValidationRules();
+}

+public sealed class ClientValidationRule
+{
+    public ClientValidationRule(string name, IReadOnlyDictionary<string, string>? parameters = null);
+    public string Name { get; }
+    public IReadOnlyDictionary<string, string>? Parameters { get; }
+}

3. Client-side validation rule source

ClientValidationProvider is the extension seam that supplies a form's client-validation metadata. The built-in provider that maps DataAnnotations attributes is internal. Apps that source rules from a different system can register their own provider in DI. It returns a RenderFragment that emits the metadata into the form, or null when there is nothing to emit, and receives the fields that actually rendered (so it only emits rules for present inputs).

 namespace Microsoft.AspNetCore.Components.Forms.ClientValidation;

+public abstract class ClientValidationProvider
+{
+    public abstract RenderFragment? RenderClientValidationRules(
+        EditContext editContext,
+        IReadOnlyDictionary<FieldIdentifier, string> renderedFields);
+}

4. JavaScript API: Blazor.formValidation

Once a page contains a client-validated form, blazor.web.js exposes a small validation service on the global Blazor object. addValidator registers a custom validator by name (matching a ClientValidationRule.Name). validateField and validateForm run validation programmatically.

// window.Blazor.formValidation
interface ValidationService {
  // Register a custom validator. `name` matches a ClientValidationRule.Name from an IClientValidationAdapter.
  addValidator(name: string, validator: Validator): void;

  // Validate a single field, returns true when valid.
  validateField(element: HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement): boolean;

  // Validate every tracked field in the form, returns true when all fields are valid.
  validateForm(form: HTMLFormElement): boolean;
}

type Validator = (context: ValidationContext) => ValidationResult;

interface ValidationContext {
  value: string | null | undefined;                                  // the field's current value
  element: HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement;
  params: Record<string, string>;                                    // the rule's Parameters
}

interface ValidationResult {
  success: boolean;
  message?: string;                                                  // optional per-call override of the rule's message
}

Usage Examples

Zero-config: DataAnnotations on a static SSR form

No configuration beyond the usual AddRazorComponents(). The feature is enabled by adding the existing DataAnnotationsValidator component into the form, same as with interactive validation.

<EditForm Model="Model" FormName="signup" method="post">
    <DataAnnotationsValidator />
    <InputText @bind-Value="Model.Email" />
    <ValidationMessage For="() => Model.Email" />
    <button type="submit">Sign up</button>
</EditForm>

@code {
    [SupplyParameterFromForm] public SignupModel Model { get; set; } = new();

    public class SignupModel
    {
        [Required, EmailAddress] public string Email { get; set; } = "";
    }
}

Typing an invalid email now shows the message in the browser and blocks submit, with no server round-trip. With JavaScript disabled, the same rules still run on the server.

Disable client-side validation for a form

<EditForm Model="Model">
    <DataAnnotationsValidator EnableClientValidation="false" />
    ...
</EditForm>

A custom validation attribute with a client-side rule

The attribute implements IClientValidationAdapter (server logic plus client rule metadata), and a matching JavaScript validator is registered using Blazor.formValidation.addValidator().

public sealed class StartsWithAttribute(string prefix) : ValidationAttribute, IClientValidationAdapter
{
    public override bool IsValid(object? value)
        => value is not string s || s.StartsWith(prefix, StringComparison.Ordinal);

    public IEnumerable<ClientValidationRule> GetClientValidationRules()
        => [new ClientValidationRule("startswith", new Dictionary<string, string> { ["prefix"] = prefix })];
}
[StartsWith("ABC-", ErrorMessage = "Code must start with 'ABC-'.")]
public string Code { get; set; } = "";
<script>
  Blazor.formValidation.addValidator('startswith', ctx =>
    ({ success: !ctx.value || ctx.value.startsWith(ctx.params.prefix) }));
</script>

Validating from JavaScript

const input = document.querySelector('#email');

Blazor.formValidation.validateField(input);       // true / false, and updates the field's message
Blazor.formValidation.validateForm(input.form);   // validate the whole form

Alternative design

Make wire protocol part of public contract

The JSON-based protocol between the Blazor SSR backend and the JS validation code is currently effectively internal. The payload describing the rules is produced by a method with an opaque return value:

public abstract class ClientValidationProvider
{
    public abstract RenderFragment? RenderClientValidationRules(
        EditContext editContext,
        IReadOnlyDictionary<FieldIdentifier, string> renderedFields);
}

We could instead make it return some user-understandable, strongly-typed representation, and invite users to write custom ClientValidationProvider implementations against our protocol:

public abstract class ClientValidationProvider
{
    public abstract FormRules? GetFormRules(
        EditContext editContext,
        IReadOnlyDictionary<FieldIdentifier, string> renderedFields);
}

The proposed design uses the opaque approach in order 1) minimize the increase in public API surface, and 2) intentionally signal that the "wire" protocol is ours to develop. Users who would want to implement their own source of client-side validation rules (i.e., not just adapters for their custom validation attributes) are responsible for either matching the format expected by the shipped JS code, or integrating their own JS code that supports whatever wire protocol they implement.

Metadata

Metadata

Assignees

Labels

api-approvedAPI was approved in API review, it can be implementedapi-proposalapi-ready-for-reviewAPI is ready for formal API review - https://github.com/dotnet/apireviewsarea-blazorIncludes: Blazor, Razor Components

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions