แก้ไข

ASP.NET Core Blazor client-side form validation in static SSR

This article explains how Blazor adds live client-side validation to forms that use static server-side rendering (static SSR). The browser validates individual fields as the user edits them and validates the full form before it's submitted. If the client-side check passes, the form is submitted and validated again on the server.

Forms that use an interactive render mode don't use the static SSR client-side validation feature described in this article. Both their per-field validation and full-form submit validation run in .NET through the form's EditContext.

How client-side validation works

When a static SSR form contains a DataAnnotationsValidator component, Blazor renders the form's validation rules into the page and enforces them using JavaScript before the form is submitted. The user sees validation errors without a round trip to the server.

Client-side validation activates automatically when the following conditions are met:

For the built-in set of validation attributes, no JavaScript configuration, additional package, or service registration is required. The section Custom client-side validation rules describes how to add support for custom validation attributes.

Important

Client-side validation is a user experience improvement, not an authoritative validation pass. It can be bypassed by disabling or modifying the browser's JavaScript execution. Server-side validation runs after the form is posted and remains authoritative. Never rely on client-side validation to protect data integrity.

Supported validation attributes

The following System.ComponentModel.DataAnnotations attributes are enforced client-side, matching the server-side data annotations behavior:

Validation attributes that don't appear in this list, including custom ValidationAttribute-derived attributes, aren't enforced client-side by default. They continue to run server-side after the form is submitted. To supply a client-side rule for a custom attribute, see the Custom client-side validation rules section.

Validation timing

A field is validated when its value is committed. For text inputs (<input> elements), this occurs when the field loses focus. Checkboxes and dropdown lists are validated immediately after selection.

After a field has shown a validation error or after the form has been submitted at least once, the field is validated again on every keystroke so that corrections are reflected immediately.

Submitting the form validates every tracked field. If any field is invalid, the submission is blocked and focus moves to the first invalid field.

Validation messages, localization, and accessibility

Client-side validation uses ValidationMessage<TValue> to display messages for individual fields and ValidationSummary to display messages for the whole form, which is the same way that interactive validation reports messages to users.

When validation localization is configured, error messages are localized on the server as the page is rendered, so client-side validation displays the same localized strings as the server-side experience. Localization requires Microsoft.Extensions.Validation. For more information, see Validation in ASP.NET Core.

ARIA attributes on input elements and validation message containers are managed by Blazor automatically, so assistive technologies announce validation errors without additional configuration.

Validation state CSS classes

The client-side validation engine applies the same CSS classes as Blazor's interactive validation, which are shown in the following table.

Element Classes
Input valid or invalid, plus modified once the user edits the field
Validation message validation-message
Validation summary validation-summary-errors or validation-summary-valid

Client-side validation also calls the browser's Constraint Validation API, so the standard CSS pseudo-classes :valid and :invalid reflect each input's current validation state.

Opt out of client-side validation

The feature can be disabled at multiple levels. Server-side validation is unaffected by any of the options in this section.

Opt out for a single form

Set the DataAnnotationsValidator component's DisableClientValidation parameter to true:

<DataAnnotationsValidator DisableClientValidation="true" />

Opt out for the entire app

Set DisableClientValidation on RazorComponentsServiceOptions when Razor components services are registered in the Program file:

builder.Services.AddRazorComponents(options =>
{
    options.DisableClientValidation = true;
});

The global option takes precedence. When it's set to true, forms don't emit client-side validation rules.

Opt out for a single submit button

Use the standard HTML formnovalidate attribute on the button. The form is posted without a client-side check, and server-side validation still runs after the post:

<button type="submit" formnovalidate>Save draft</button>

This can be used to implement a "save draft" or "back" button that don't require a completely valid form for the submit to succeed.

Custom client-side validation rules

Custom validation attributes continue to run on the server but don't have a client-side implementation by default. Enforcing a custom rule in the browser involves two steps: emitting the rule from the .NET attribute and registering a matching JavaScript validator. The server-side implementation remains authoritative.

Emit the rule from .NET

Implement IClientValidationRuleProvider on the validation attribute and return one or more ClientValidationRule instances. The rule's Name identifies the client-side validator, and Parameters supplies values the validator needs.

The following StartsWithAttribute validates server-side in IsValid and contributes a startswith client-side rule with a prefix parameter:

using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Components.Forms;

namespace BlazorSample.Validation;

// A custom validation attribute that also contributes a client-side rule for static SSR
// forms. The rule name ('startswith') must match the validator registered on the client
// with Blazor.formValidation.addValidator.
public sealed class StartsWithAttribute : ValidationAttribute, IClientValidationRuleProvider
{
    private readonly string prefix;

    public StartsWithAttribute(string prefix)
    {
        this.prefix = prefix;
        ErrorMessage = $"The value must start with '{prefix}'.";
    }

    protected override ValidationResult? IsValid(object? value,
        ValidationContext validationContext)
    {
        // Leave empty values to RequiredAttribute, matching the client-side validator.
        if (value is not string text || text.Length == 0)
        {
            return ValidationResult.Success;
        }

        if (!text.StartsWith(prefix, StringComparison.Ordinal))
        {
            return new ValidationResult(ErrorMessage, [validationContext.MemberName!]);
        }

        return ValidationResult.Success;
    }

    public IEnumerable<ClientValidationRule> GetClientValidationRules()
    {
        yield return new ClientValidationRule(
            "startswith",
            new Dictionary<string, string> { ["prefix"] = prefix });
    }
}

Apply the attribute to the model in the usual way:

using System.ComponentModel.DataAnnotations;

namespace BlazorSample.Validation;

// Model for the static SSR client-side validation sample. The StartsWith attribute
// contributes a client-side rule enforced in the browser before the form is submitted.
public class ShipModel
{
    [Required]
    [StartsWith("NCC-")]
    public string? Registry { get; set; }
}

Register the JavaScript validator

In JavaScript, call Blazor.formValidation.addValidator(name, validator) to associate a rule name with a validator function. The name must exactly match ClientValidationRule.Name, including casing. Registrations are app-wide, and registering the same name again replaces the previous validator.

Warning

If no JavaScript validator is registered for an emitted rule name, the rule is skipped in the browser. Server-side validation still runs when the form is posted.

Register custom validators once from app startup code. Choose the registration location based on how Blazor starts, as described in the following table.

Blazor startup Registration location
Automatic startup (default) A script immediately after blazor.web.js
Manual Blazor.start() The continuation returned by Blazor.start()
Either startup style A JavaScript initializer's afterWebStarted callback

A JavaScript initializer works with either startup style. In a file named {ASSEMBLY NAME}.lib.module.js in the app's wwwroot folder:

// A JavaScript initializer works with automatic or manual Blazor startup. The validation service
// is available when afterWebStarted runs, even if the current page contains no validated form.
export function afterWebStarted(blazor) {
  blazor.formValidation.addValidator('startswith', (context) => {
    const value = context.value;

    // An empty value is valid. Use [Required] to require a value.
    if (!value) {
      return { success: true };
    }

    return { success: value.startsWith(context.params.prefix) };
  });
}

With automatic startup, an app-specific validator script can instead be loaded immediately after blazor.web.js:

<script src="@Assets["_framework/blazor.web.js"]"></script>
<script src="@Assets["js/custom-validation.js"]"></script>

The second script can call Blazor.formValidation.addValidator directly.

With manual startup, define the registration in an app script:

function registerCustomValidators(blazor) {
  blazor.formValidation.addValidator('startswith', (context) => {
    const value = context.value;

    if (!value) {
      return { success: true };
    }

    return { success: value.startsWith(context.params.prefix) };
  });
}

// Automatic startup initializes formValidation before this script runs.
// Manual startup calls registerCustomValidators after Blazor.start completes.
if (Blazor.formValidation) {
  registerCustomValidators(Blazor);
}

Load the scripts with automatic startup disabled, and register the validators after Blazor.start() completes:

<script src="@Assets["_framework/blazor.web.js"]" autostart="false"></script>
<script src="@Assets["js/custom-validation.js"]"></script>
<script>
    Blazor.start().then(() => {
        registerCustomValidators(Blazor);
    });
</script>

Important

Register validators from app startup code, not from a page or form component. A component script can run before Blazor starts, and scripts added by enhanced navigation aren't executed. Static SSR components also can't use IJSRuntime because they don't have an interactive .NET runtime.

Write JavaScript validator functions

The validator receives a context object with the following members, as shown in the following table.

Member Description
value The field's current value as a string, or null/undefined when there's no value.
element The validated input, select, or textarea element.
params The rule's Parameters as a string dictionary.

The validator is expected to return { success: true } when the value is valid. Return { success: false } to use the rule's server-supplied message, or you can return { success: false, message: '...' } to override the message for that call.

Empty values should normally be treated as valid by rules other than required, allowing an optional field to remain empty while still validating values that are supplied.

Implement the same rule semantics in .NET and JavaScript, including case sensitivity, normalization, and empty-value handling. If the implementations differ, the browser and the authoritative server-side validation can produce different results.

Validate form on demand

The Blazor.formValidation API also exposes JavaScript methods for validating on demand, as the following table shows.

Method Description
validateField(element) Validates a single field element and updates its error display. Returns true when valid.
validateForm(form) Validates every tracked field in a form. Returns true when all fields are valid.

Limitations

  • Client-side rules are emitted only for fields included in server-side validation as well. Without Microsoft.Extensions.Validation, only top-level model properties are validated. Validating nested objects and collections requires the app to call AddValidation and the model to be discovered. For more information, see Validation in ASP.NET Core. Note that this limitation is an intentional feature to help prevent bugs where the authoritative server validation would be missing due to misconfiguration.
  • Inputs added to an existing form by a later streaming rendering update don't receive client-side validation. A form delivered in a single streamed batch is covered normally.
  • Only the attributes listed in Supported validation attributes have built-in client-side implementations. For example, a RangeAttribute with a non-numeric operand type is only enforced on the server. Other attributes require a custom client-side validation rule.
  • Custom JavaScript validators are synchronous. Rules that require a network call or other asynchronous work must run on the server or use asynchronous validation with an interactive render mode. For more information, see ASP.NET Core Blazor advanced form validation.

Additional resources