Make Blazor component descriptor serialization Native AOT and trimming compatible
Maintainers usually reply within 1 day
Nobody has claimed this yet.
Assessment
- Difficulty
- 5/5
- Estimated time
- Over a week
- Newbie friendliness
- 25/100
Research direction
Start with the linked BlazorSerializer contract in issue #70407 and the Blazor Native AOT epic in #70364, then trace the current root-type cache and descriptor writer and reader mentioned in this issue. Map how component and parameter identities, selected serializer options, and existing validation flow through those entry points. Done means supported descriptors resolve through known mappings and preserve concrete parameter values across the described activation and update scenarios.
Written by the indexing model from the issue text.
Description
Make Blazor component descriptor serialization Native AOT and trimming compatible
Summary
Use the proposed BlazorSerializer and BlazorSerializerOptions contract to serialize and restore component descriptor parameter values, and resolve both component identities and concrete parameter identities through known string-to-type mappings, preferably generated at compile time.
A descriptor identifies the root component to activate or update and carries its parameter values. Component identity maps to an eligible component Type consumed by the existing component runtime. Each concrete parameter identity maps to a value Type consumed by the shared serializer. The component instance itself is not deserialized as an application model.
This is Serialization item 3 of the Blazor Native AOT epic. Component discovery supplies supported component definitions and their mapping contributions; descriptor consumers must use those mappings when writing and reading descriptors. Application values use the selected application's serialization definitions. A host may register multiple applications, each with its own selected configuration.
Proposal: API names below are proposed or drawn from the shared proposal, not approved APIs. Declaration syntax, identity encoding/versioning, and shared host-binding contracts remain open.
Goals
- Restore supported descriptors for initial activation, root updates, and applicable circuit root-marker restoration without arbitrary assembly loading, type-name resolution, or member discovery in strict execution.
- Preserve concrete root-parameter types and supported data, including an
Employeepassed to aPersonparameter, ordinary null, and typed values whose representation is null. - Reuse application serialization definitions through
BlazorSerializerOptions, including compatible generated STJ definitions and byte-provider definitions, without a second descriptor-specific provider registration. - Isolate multiple mapped applications: each application's endpoints and associated circuits retain its component mappings, parameter mappings, and finalized serializer selection.
- Preserve supported defaults for ordinary upgrades without new warnings or required declarations, along with existing Server protection/validation, browser-safe carriers, supported captured content, and error-reporting conventions.
Non-goals
- Define how discovery produces component metadata, or redesign component construction, injection, assignment, or lifecycle execution. Consuming known component mappings is required here; generating/discovering component definitions remains a separate integration boundary.
- Integrate JS interop serialization or add dedicated DynamicComponent support.
- Redesign persistent state, session, or TempData value serialization. Shared initialization must precede their immediate serialization callbacks, but their representations and lifecycle rules are separate.
- Replace descriptor, renderer, or host protocols wholesale, or permit application providers to override framework envelope fields.
- Serialize arbitrary delegates or configure nested-model polymorphism. Preserve currently supported captured render content without expanding it.
Proposed solution
There are two logical identity-mapping roles:
| Mapping | Input and output | Consumer |
|---|---|---|
| Component identity | Known string identity to eligible root component Type, with matching writer identity |
Existing component runtime |
| Concrete parameter identity | Known string identity to supported concrete value Type, with matching writer identity |
BlazorSerializer with selected options |
These roles may share registry storage or generation machinery, but remain independently eligible. A type is not an allowed root component merely because it has a serializer definition, appears in the parameter registry, or implements IComponent. Physical sharing versus separate registry stores remains open.
Both roles are public interface contracts implemented by source-generated application/library code: proposed names are IComponentTypeRegistry and IComponentParameterTypeRegistry. Generated implementation classes and framework caches may remain internal. Public contracts, rather than access to internal framework registry classes, provide the cross-assembly boundary.
Component mappings compose supported application and library component definitions, including non-routable roots introduced by interactive boundaries. Parameter mappings include statically established parameter types and explicitly declared additional concrete runtime selections. The additional declarations are application-wide, through an attribute or equivalent mechanism whose syntax remains open. They ensure both the known mapping and the proper serialization descriptor are available on every applicable receiving side; preserving a type name alone is insufficient.
For non-null parameter values, writers select the concrete type using the actual value's runtime type. Readers resolve the known identity to Type, then select its matching definition through the application's options. A broad resolver is not an enumeration of all allowed incoming descriptor types. Missing support must not silently use a declared base type, lose derived data, or invoke unsafe discovery.
Blazor owns the descriptor envelope and all component identities, sequence numbers, invocation IDs, keys, routing fields, and transport validation. Providers own only supported application-value slots. The shared internal STJ bridge preserves definition selection throughout those slots; its byte-provider Base64 carrier is distinct from Server protection/transport or WebAssembly HTML-comment Base64. Native STJ definitions do not become provider blobs simply because they occur in a descriptor.
flowchart TD
Host[Application-aware host selection] --> Components[Known component mappings]
Host --> Parameters[Known parameter mappings]
Host --> Options[BlazorSerializerOptions]
Writer[Descriptor writer] --> Components
Writer --> Parameters
Writer --> Serializer[BlazorSerializer]
Serializer --> Options
Writer --> Envelope[Framework envelope and transport]
Envelope --> Reader[Descriptor reader]
Reader --> Components
Reader --> Parameters
Reader --> Serializer
Reader --> Runtime[Existing component runtime]
Scenarios and corresponding design
1. Resolve the component type in a descriptor
A root component does not need application parameters:
<StatusPanel @rendermode="InteractiveServer" />
The selected application's mappings identify the supported StatusPanel type. Before this change, the current root-type cache resolves assembly/type-name fields using runtime discovery. The strict/generated path instead resolves the descriptor identity through known mappings and hands the type to the existing component runtime.
sequenceDiagram
participant G as Generated component contribution
participant M as Component mappings
participant W as Descriptor writer
participant D as Descriptor reader
participant C as Existing component runtime
G->>M: Contribute supported application and library roots
W->>M: Get identity for component Type
M-->>W: Known string identity
W-->>D: Framework descriptor
D->>D: Apply protocol validation
D->>M: Resolve component identity
M-->>D: Eligible Type or no match
D->>C: Deliver known component Type and empty parameters
Outcome: Supported routable and non-routable roots resolve without type-name discovery. Unknown or ineligible identities fail before activation; no-match does not proceed to the runtime. Discovery owns producing the component definitions, not an exemption from resolving component identities in this consumer.
2. Pass an ordinary parameter across a Server boundary
Existing component usage remains unchanged:
<CustomerSummary Customer="customer"
@rendermode="InteractiveServer" />
The same code retains supported defaults. When the application supplies a supported definition through its selected options, descriptor writes and reads use that definition.
sequenceDiagram
participant W as Descriptor writer
participant M as Component mappings
participant R as Parameter mappings
participant B as BlazorSerializer
participant D as Descriptor reader
participant C as Existing component runtime
W->>M: Get component identity
W->>R: Get concrete parameter identity
W->>B: Write concrete value with selected options
W-->>D: Protected framework descriptor
D->>D: Apply existing validation
D->>M: Resolve component Type
D->>R: Resolve concrete value Type
D->>B: Read bounded value with selected options
D->>C: Deliver component Type and ParameterView
Outcome: Both identities resolve through their appropriate known mapping. The existing runtime still constructs the component, assigns parameters, and runs its lifecycle.
3. Reuse an application serializer provider
Application configuration uses the shared contract:
options.TypeInfoResolvers.Add(new MessagePackResolver(options));
MessagePackResolver is an illustrative provider integration from the shared proposal, not a shipping framework class. Before descriptor adoption, configuring this shared resolver alone does not wire the existing descriptor serializers. After adoption, it handles the supported known parameter types assigned to it.
sequenceDiagram
participant D as Descriptor value consumer
participant B as BlazorSerializer
participant O as Selected options
participant P as Selected provider
D->>B: Write known concrete Type
B->>O: Select matching definition
B->>P: Write provider bytes
P->>B: Delegate supported child with captured options
B-->>D: Complete value in framework slot
D->>B: Read using matching Type and options
B->>P: Read decoded provider bytes
B-->>D: Restored value
Outcome: No second descriptor-specific serializer registration is required. An existing STJ context implementing IBlazorTypeInfoResolver through application-authored integration participates through the same options. This feature does not generate that interface implementation. Nested provider and framework handling must remain effective; a selected provider failure is not retried through reflection.
4. Preserve a declared additional runtime type
A component declares a base-type parameter:
[Parameter]
public Person? Person { get; set; }
The caller supplies a value that may contain an Employee:
<PersonSummary Person="selectedPerson"
@rendermode="InteractiveServer" />
For strict/generated execution, the application explicitly declares Employee as an additional supported root-parameter type through the application-wide declaration mechanism. The declaration's syntax remains open; it must establish both mapping eligibility and serialization support.
sequenceDiagram
participant G as Generated contribution
participant R as Parameter mappings
participant W as Descriptor writer
participant B as BlazorSerializer
participant D as Parameter reader
G->>R: Include explicitly declared Employee
W->>R: Get identity for actual runtime Type
R-->>W: Employee identity
W->>B: Write as Employee
W-->>D: Concrete identity and complete value
D->>R: Resolve Employee identity
R-->>D: typeof(Employee)
D->>B: Read as Employee
Outcome: Derived data and concrete type are retained. Undeclared additional runtime types or unavailable definitions fail clearly in strict execution, rather than silently using Person. Ordinary statically established parameter types do not require manual declarations. This does not configure nested-model polymorphism.
5. Round-trip null and typed encoded-null values
An application may pass an ordinary null:
<PersonSummary Person="@((Person?)null)"
@rendermode="InteractiveServer" />
Separately, a non-null concrete value may have a supported serializer representation of null.
sequenceDiagram
participant W as Descriptor writer
participant D as Parameter reader
participant R as Parameter mappings
participant B as BlazorSerializer
alt CLR value is null
W-->>D: Null without concrete identity
D->>D: Restore null under framework rule
else Non-null value encodes as null
W-->>D: Concrete identity and encoded null
D->>R: Resolve concrete Type
D->>B: Read null representation with selected definition
end
Outcome: Untyped null stays distinct from a typed encoded-null value. The latter must execute its concrete definition, including existing supported union behavior.
6. Restore WebAssembly and Auto parameters
The same supported model may cross a browser-runtime boundary:
<CustomerSummary Customer="customer"
@rendermode="InteractiveWebAssembly" />
Changing the render mode to InteractiveAuto supports either applicable receiving runtime. Server and client need compatible component mappings, parameter mappings, and serialization definitions; they do not share a live options object.
sequenceDiagram
participant W as WebAssembly descriptor writer
participant H as Client host setup
participant M as Client component mappings
participant D as Client parameter reader
participant R as Client parameter mappings
participant B as BlazorSerializer
participant C as Existing component runtime
W-->>H: HTML-safe descriptor data
H->>H: Retain raw data until configuration exists
H->>M: Resolve known component identity
M-->>H: Eligible client component Type
H->>D: Restore initial parameter values
D->>R: Resolve concrete value identity
D->>B: Read with selected client options
H->>C: Deliver resolved component and restored parameters
Outcome: Initial and later reads use configured client selection, not an early static default. Non-routable library components require client component mappings too. Missing or incompatible support fails explicitly. HTML-comment transport remains safe even when values contain comment delimiters.
7. Isolate multiple mapped applications and later updates
Two applications in one host may use different definitions for the same CLR model. Each application has one selected configuration for its endpoints and associated circuits, typically sourced through endpoint metadata or an equivalent application-aware mechanism.
sequenceDiagram
participant H as Initial connection endpoint metadata
participant U as ComponentHub
participant F as CircuitFactory
participant D as Circuit descriptor readers
U->>H: Read application, options and both registry roles
H-->>U: Selected application configuration
U->>D: Restore applicable initial descriptors with selection
U->>F: Create scope with application association
F->>F: Initialize options and both mapping roles
U->>D: Later roots or updates for associated circuit
D->>D: Retain owning application's selection
Outcome: Initial roots, roots supplied after an empty start, updates, reconnects, and applicable restored markers retain the application's selection. A component known only to another application is not implicitly eligible. Concurrent or nested calls cannot change another operation's options, provider caches, mapping eligibility, or mutable operation state.
8. Compose library types and supported captured content
Libraries contribute supported component definitions, ordinary parameter models, and serialization definitions. Applications include additional runtime-selected library parameter types in their application-wide declarations. Conflicting identities must fail rather than select the first loaded contribution.
sequenceDiagram
participant G as Application and library contributions
participant M as Component mapping composition
participant R as Parameter mapping composition
participant F as RenderFragmentSerializer
participant B as BlazorSerializer
G->>M: Contribute supported component identities
G->>R: Contribute supported value identities
M->>M: Validate component-role conflicts
R->>R: Validate parameter-role conflicts
F->>B: Write or read supported nested application values
Outcome: Component and parameter contributions compose without assembly scanning and preserve their role-specific eligibility. Existing captured content keeps its framework DTOs and restrictions; nested component identities use component mappings and nested application values use parameter definitions. Arbitrary delegates and unsupported templated content do not become serializable.
9. Reject invalid or unsupported descriptors
Server restoration retains data protection, expiration, marker-key validation, invocation-ID checks, and sequence/reuse checks under existing rules.
sequenceDiagram
participant D as Descriptor readers
participant M as Component mappings
participant R as Parameter mappings
participant B as BlazorSerializer
participant E as Existing rejection or exception surface
D->>D: Validate envelope, definitions and counts
alt Invalid framework descriptor
D->>E: Reject under existing rules
else Unknown or ineligible component
D->>M: Resolve component identity
M-->>D: No eligible match
D->>E: Report unsupported root component
else Unknown parameter identity
D->>R: Resolve concrete identity
R-->>D: No match
D->>E: Report unsupported value type
else Missing definition or invalid value
D->>B: Read selected concrete definition
B-->>D: Explicit failure
D->>E: Report without reflection retry
end
Outcome: Server logging/rejection and WebAssembly exception conventions remain explicit. Errors identify the relevant component or parameter and identity where safe, without logging sensitive payload values. Unknown identities, wrong-role entries, incomplete definitions, mismatched counts, malformed values, and provider failures cannot yield success-shaped defaults.
Registry and serializer contracts
The following public interface declarations express the mapping roles supplied by generated application/library code. Interface-based extensibility is selected; exact names/signatures remain proposed. The declarations do not select separate physical stores or final registration APIs.
public interface IComponentTypeRegistry
{
Type? ResolveType(string identity);
string? GetIdentity(Type componentType);
}
public interface IComponentParameterTypeRegistry
{
Type? ResolveType(string identity);
string? GetIdentity(Type type);
}
Implementations provide stable known mappings after configuration finalization without requiring a public dictionary or framework registry constructor. Generated code may use direct lookups or precomputed tables; a single implementation may implement both interfaces while preserving role eligibility. Framework wrappers/caches may remain internal and owned by the selected application.
Conflicting identity-to-type or type-to-identity contributions must fail explicitly. Proposed composition rules allow identical duplicate contributions to coalesce and use one canonical writer identity per type per role. Missing lookups return no match and require caller-side failure reporting. A type present in both roles must meet both requirements independently. The lookup-only interfaces do not enumerate contributions, so whole-registry validation cannot be assumed. Generated composition can validate known entries; validation of independently registered implementations and the final registration/composition mechanism remain open.
Component contributions come from supported discovery definitions. Parameter contributions include statically established types plus explicit additional runtime declarations. Generation/configuration must establish the required serialization support; emitting only a string-to-type token does not suffice. Generated code implements the public interfaces across assembly boundaries without accessing internal framework types; registration, composition, and final package placement remain open.
Value execution reuses the relevant proposed shared operations:
public static class BlazorSerializer
{
public static void Serialize(
IBufferWriter<byte> destination, object? value,
Type type, BlazorSerializerOptions options);
public static object? Deserialize(
ReadOnlySequence<byte> input, Type type,
BlazorSerializerOptions options);
}
public sealed class BlazorSerializerOptions
{
public IList<IBlazorTypeInfoResolver> TypeInfoResolvers { get; }
public bool IsReadOnly { get; }
public void MakeReadOnly();
public BlazorTypeInfo GetTypeInfo(Type type);
}
These are API-reference excerpts from the shared proposal, not implementations or replacement declarations for its complete surface. No new public provider protocol is added.
The existing assembly/full-name descriptor fields are not an approved new registry encoding. Adapting those fields to the logical string key, retaining or changing the wire representation, version compatibility, and canonicalization remain open. No name parser, assembly load, or Type.GetType fallback is implied in strict execution.
Host and application integration
| Surface | Required integration |
|---|---|
| SSR initialization | Select the mapped application's configuration, typically from endpoint metadata; initialize access to serializer options and both mapping roles before value consumers or immediate state-restoration callbacks. |
| Server writers | Use selected component identities and parameter definitions without capturing request-selected state in the existing singleton writer. |
| Server readers | Replace strict component name-based discovery and parameter name-based discovery with their selected known mappings, retaining framework protocol validation. |
| Hub/circuit creation | Read the owning application's configuration from endpoint metadata on the initial connection before initial descriptor restoration, then carry that selection into circuit initialization, including empty starts. |
| Subsequent roots/reconnects/restoration | Retain or re-establish the owning application under the compatible configuration policy; reject cross-application updates rather than switch configurations. |
| WebAssembly startup | Supply configured client selection before initial component/parameter resolution and value restoration; both initial and later reads must stop using configuration-independent static dispatch. |
| Captured content | Preserve framework representation and restrictions while resolving supported nested component and value identities through their respective roles. |
| Existing runtime | Receive the resolved component Type and ParameterView; construction, injection, assignment, and lifecycle remain unchanged. |
Assume all information needed to identify the owning application and select serializer options and both registry roles is available in endpoint metadata readable on the initial connection. ComponentHub.StartCircuit reads this selection before restoring initial descriptors and passes it into CircuitFactory for the circuit to retain. The existing pre-circuit descriptor reads do not, by themselves, require splitting framework validation from value restoration or moving restoration into the circuit scope.
WebAssemblyHostBuilder currently restores registered root parameters during early builder initialization before application DI exists. The design proposes retaining raw descriptors until configured client services are available. Observable RootComponents behavior must be preserved or any necessary behavior change explicitly resolved before implementation.
DI exposes the selected application's configuration; one host-wide options registration cannot represent multiple independent applications. The host supplies the required application association through the initial connection's endpoint metadata; recovering the original SSR endpoint or defining another association protocol is not part of this proposal. Selected configuration must also precede state-restoration callbacks that deserialize immediately, not merely precede rendering.
Finalized options are reusable configuration, not an active runtime or STJ operation stack. Providers that capture options and their native formatter caches belong to that configuration; shallow copies must not retain providers bound to another options instance. Scope-bound runtime association and concurrent/reentrant operation state must remain isolated without global mutable selection.
- Dominant language
- C#
- Stars
- 38.5k
- Forks
- 11.6k
- Avg merge
- 3d 1h
- Merged PRs (30d)
- 289
Getting set up
Starts the project's dev container in your browser, under your own GitHub account.
- No Dockerfile or Docker Compose file
- Has a pull request template
- Read the contributing guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from dotnet/aspnetcore
-
area-networking
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
dotnet/aspnetcore#70545 · 1 comment ·
Maintainers usually reply within 1 day
-
area-infrastructure
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
dotnet/aspnetcore#70524 · 1 comment ·
Maintainers usually reply within 1 day
-
[OpenTelemetry] Hosting replaces an Activity.DisplayName that the app set, when a route matchesOpenarea-hosting Regression
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
dotnet/aspnetcore#70516 · 1 comment · 1 reaction ·
Maintainers usually reply within 1 day
-
[SSE] ServerSentEventsResult.WrapEvents is missing [EnumeratorCancellation], so RequestAborted never reaches the app's generator on the IAsyncEnumerable<T> overloadPossibly taken @bharatprajapati-coder claimed this 1 day ago. Openarea-minimal
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
dotnet/aspnetcore#70419 · 2 comments ·
Maintainers usually reply within 1 day
-
[Blazor] DefaultInMemoryCircuitPersistenceProvider never disposes the retention CancellationTokenSource when a persist is rejected at capacityPossibly taken @Vinoth2562000 claimed this 1 day ago. Openarea-blazor
Difficulty 2/5 1-3 hours Newbie friendliness 77/100
dotnet/aspnetcore#70418 · 1 comment · 1 assignee ·
Maintainers usually reply within 1 day
All issues in dotnet/aspnetcore
Similar issues
-
bug emitter:client:csharp
Difficulty 2/5 1-3 hours Newbie friendliness 84/100
microsoft/typespec#12572 · 1 comment · 1 reaction ·
Maintainers usually reply within 1 day
-
needs-area-label
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
Maintainers usually reply within 2 days
-
AWS Lambda: KeyNotFoundException drops HTTP-triggered transactions when allowAllHeaders is disabledOpenbug
Difficulty 2/5 1-3 hours Newbie friendliness 70/100
newrelic/newrelic-dotnet-agent#3949 · 1 comment ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 87/100
Maintainers usually reply within 1 day
-
priority:P2
Difficulty 2/5 1-3 hours Newbie friendliness 80/100