Improve Blazor SSR client-side form validation#67324
Draft
oroztocil wants to merge 11 commits into
Draft
Conversation
Open
3 tasks
…ed nav, fix tests
b57c88c to
b5c921f
Compare
3 tasks
3 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Reworks the .NET Preview 5 client-side validation implementation for Blazor SSR forms. From a developer's point of view the feature is unchanged (add
<DataAnnotationsValidator />to a static SSREditFormand validation runs in the browser), but the design underneath is substantially simpler and better layered: validation rules are delivered through a single inert per-form carrier element instead ofdata-val-*attributes on every input, all render-mode-specific code is out of the coreFormsassembly, and the public surface shrinks to a small, purposeful set of types.Contributes to #51040. Supersedes the Preview 5 implementation.
Background and motivation
Preview 5 shipped an initial implementation that emitted jQuery-unobtrusive-style
data-val-*attributes onto every input, generated by a publicIClientValidationServiceliving in the coreMicrosoft.AspNetCore.Components.Formsassembly and read on the client by a DOM scanner. Follow-up architectural review raised three concerns this PR addresses:Formsassembly, where it does not belong.data-val-*approach spread validation metadata across many attributes on many elements and coupled the input components to the client-validation wire format.What changed
Wire format: one inert carrier element per form
Instead of
data-val-*attributes on each input, each static-SSR form emits a single custom element carrying all of the form's rules as JSON in adata-rulesattribute:Because the payload is in an attribute (not element content), the element renders nothing and needs no CSS to stay hidden. The JSON is encoder-escaped by the serializer and HTML-attribute-encoded by Blazor, so it is safe in the page and round-trips through
getAttribute+JSON.parse.Assembly layering
The feature is split across three assemblies along the existing
Endpoints->Web->Formsreference direction:FormsEnableClientValidationflag onDataAnnotationsValidator, and writing the activation signal intoEditContext.Properties. No render logic, no dedicated types.WebEditForm/InputBaseintegration:InputBaseregisters rendered fields into an internalRenderedFieldRegistry; the internalClientValidationDatacomponent emits the carrier; theClientValidationProviderabstraction is the DI seam.ClientValidationProviderpublic; rest internalEndpointsDataAnnotationsClientValidationProvider, its reflection/MEV cache, the typed descriptors, and the JSON serializer. Maps DataAnnotations attributes to rules for server-validated fields only.ClientValidationRule+IClientValidationAdapterpublic; rest internalInput*components are untouched beyond a single gated registration call inInputBase; there is no per-input rule logic anywhere.The provider returns a
RenderFragmentClientValidationProvider.RenderClientValidationRules(...)returns aRenderFragment?that renders the carrier element (ornullwhen there is nothing to emit). TheWeb-sideClientValidationDatacomponent simply renders that fragment; it never sees the descriptor types or the serializer, both of which are internal toEndpoints. This keeps the wire format fully owned byEndpointsand off theWeb(and therefore WebAssembly) surface.Client ingestion: form-associated custom element
blazor.web.jsdefines<blazor-client-validation-data>as a form-associated custom element. ItsconnectedCallbackreadsdata-rules, finds the owning form viaattachInternals().form, registers each field with the validation engine, and setsnovalidateon the form;disconnectedCallbackunregisters. This replaces the Preview 5 DOM scanner (querySelectorAll('[data-val=true]')), which is removed.How it works
flowchart LR subgraph Server["Server (static SSR)"] DAV["DataAnnotationsValidator<br/>writes activation flag<br/>keyed by its own type"] IB["InputBase<br/>registers rendered field + name<br/>(AssignedRenderMode is null)"] CVD["ClientValidationData (internal)<br/>checks activation + provider,<br/>renders provider fragment"] Prov["DataAnnotationsClientValidationProvider<br/>DataAnnotations to rules -> JSON<br/>(only server-validated fields)"] DAV --> IB --> CVD Prov --> CVD end subgraph HTML["Rendered HTML"] Carrier["blazor-client-validation-data<br/>data-rules='{...}'"] end subgraph Client["blazor.web.js"] FACE["Form-associated custom element<br/>connectedCallback / reconcile()"] Engine["ValidationEngine + validators<br/>ErrorDisplay + EventManager"] FACE --> Engine end CVD --> Carrier Carrier --> FACE Engine -->|"blocks invalid submit,<br/>shows messages"| HTMLDataAnnotationsValidatorwrites the activation flag intoEditContext.Properties[typeof(DataAnnotationsValidator)]whenEnableClientValidationistrue(the default).AssignedRenderMode is null), eachInputBaseregisters itsFieldIdentifierand rendered HTMLnameinto a per-formRenderedFieldRegistry.ClientValidationData(rendered byEditFormafter itsChildContent, so it initializes after the inputs register) checks the activation flag, resolves the optionalClientValidationProvider, reads the registry, and renders the provider's fragment.DataAnnotationsClientValidationProviderbuilds rules from the model's DataAnnotations attributes and returns a fragment that emits the carrier, emitting client rules only for fields the server would also validate (see Security below).Public API
Public surface after this PR:
Everything else is internal:
ClientValidationDataandRenderedFieldRegistry(Web);ClientValidationFormDescriptor,ClientValidationFieldDescriptor,ClientValidationDataSerializer,ClientValidationCache, andDataAnnotationsClientValidationProvider(Endpoints).Removed from Preview 5 (superseded):
IClientValidationService, theAddClientValidationDI extension, and the oldobject?-parameterClientValidationRule(withWithParameter). The Preview 5 types lived inForms; the reworked feature exposes no client-validation types fromFormsat all.Behavior and scope
<DataAnnotationsValidator />to a static-SSREditFormactivates client validation.EnableClientValidation="false"opts out.connectedCallbackdoes not re-fire) and strips the JS-addednovalidate. A per-carrierreconcile(), run onenhancedload, re-applies rules when a reused carrier's payload changed and re-assertsnovalidate. Covered by E2E for form-to-form, round-trip, form-to-no-form, and no-form-to-form transitions.Utf8JsonWriter(default encoder escapes<,>,&,') combined with Blazor's HTML attribute encoding, so a hostile validation message cannot break out of the carrier element or its attribute.Testing
paramsomission, HTML escaping), theClientValidationDatacomponent (renders the provider's fragment only when activated, the registry is populated, and a provider is registered), andInputBasefield registration.reconcile(), error display, and event handling.ClientValidationsuite covers basic validation, all built-in attributes, localization, custom validators, radio groups, multiple forms, enhanced-navigation transitions,formnovalidate, and server-rendered message interop.