RenderFragment serialization#66528
Conversation
There was a problem hiding this comment.
Pull request overview
This PR enables RenderFragment component parameters (e.g., ChildContent) to cross interactive render mode boundaries by serializing the fragment’s render tree into a JSON-friendly DTO and reconstructing a RenderFragment on the interactive side.
Changes:
- Introduces
RenderFragmentSerializer(shared source) to serialize/deserialize render tree frames into DTOs. - Updates
SSRRenderModeBoundaryto allowRenderFragmentparameters and replace them withSerializedRenderFragmentbefore marker serialization. - Updates Server and WebAssembly parameter deserializers to detect
SerializedRenderFragmentand rehydrate it back into a liveRenderFragment, plus adds unit/E2E coverage.
Reviewed changes
Copilot reviewed 14 out of 14 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| src/Components/Shared/src/RenderFragmentSerializer.cs | New shared serializer/DTO types for RenderFragment frame round-tripping. |
| src/Components/Endpoints/src/Rendering/SSRRenderModeBoundary.cs | Allows RenderFragment parameters and serializes them into DTOs before marker creation. |
| src/Components/Server/src/Circuits/ComponentParameterDeserializer.cs | Detects SerializedRenderFragment and deserializes to RenderFragment. |
| src/Components/WebAssembly/WebAssembly/src/Prerendering/WebAssemblyComponentParameterDeserializer.cs | Same detection/rehydration for WebAssembly prerender parameter deserialization (+ trimming hints). |
| src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.cs | Wires RenderFragmentSerializer logging. |
| src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHost.cs | Wires RenderFragmentSerializer logging. |
| src/Components/Endpoints/test/RenderFragmentSerializerTest.cs | Adds unit tests for serializer behavior and round-trips. |
| src/Components/test/E2ETest/ServerRenderingTests/RenderFragmentSerializationTest.cs | Adds E2E coverage for crossing boundary scenarios (server + wasm). |
| src/Components/test/testassets/TestContentPackage/RenderFragmentChild.razor | Test component used by E2E scenarios. |
| src/Components/test/testassets/TestContentPackage/TypedParameterDisplay.razor | Test component for typed parameter scenarios in E2E. |
| src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/RenderFragmentInteractive.razor | Test page exercising scenarios for interactive boundary crossing. |
| src/Components/Endpoints/src/Microsoft.AspNetCore.Components.Endpoints.csproj | Links shared serializer into Endpoints assembly. |
| src/Components/Server/src/Microsoft.AspNetCore.Components.Server.csproj | Links shared serializer into Server assembly. |
| src/Components/WebAssembly/WebAssembly/src/Microsoft.AspNetCore.Components.WebAssembly.csproj | Links shared serializer into WebAssembly assembly. |
| } | ||
|
|
||
| [UnconditionalSuppressMessage("Trimming", "IL2057", Justification = "Component types referenced in serialized RenderFragments are expected to be preserved by the application.")] | ||
| private static void DeserializeNodes(RenderTreeBuilder builder, List<RenderTreeNode> nodes) |
There was a problem hiding this comment.
Use JsonSerializerContext
There was a problem hiding this comment.
Feel free to skip everything labelled Low priority for now.
We should pass the JsonSerializationOptions to the RenderFragmentSerializer.Deserialize and its downstream code. The tests don't catch issues with mismatched serilazation options because they don't go through the equivalent E2E pipeline.
Other than that it looks good!
|
|
||
| public IReadOnlyDictionary<int, RenderFragmentCapture> ChildCaptures => _childCaptures; | ||
|
|
||
| public void Invoke(RenderTreeBuilder builder) |
There was a problem hiding this comment.
Is it guaranteed that this will be called only once per RenderFragmentCapture instance? If not, add a guard for re-entrancy or make it idempotent (clear _childCaptures?)
There was a problem hiding this comment.
It can in the case of multiple renders of the RenderFragment in the same Component's BuildRenderTree, but it doesn't matter, because result of executing same RenderFragment multiple times will yield same result.
| { | ||
| var start = builder.GetFrames().Count; | ||
| _original(builder); | ||
| var end = builder.GetFrames().Count; |
There was a problem hiding this comment.
nit: Since this call, GetFrames always returns the same "value", right? Can we omit the subsequent GetFrames calls and reuse what we got here?
There was a problem hiding this comment.
_original(builder); can resize original array and it will break our earlier reference. I can omit only second, but I am not sure that optimization will be that impactful.
|
|
||
| @if (Test == 5) | ||
| { | ||
| <TestContentPackage.RenderFragmentChild @rendermode="renderMode" IdSuffix="test5"> |
There was a problem hiding this comment.
The "dynamic" semantics of RenderFragment serialized over the boundary is different than if it's not.
If we would have a component
<MyPanel @rendermode="wasm">
<MyDateTime />
</MyPanel>and companion component MyDateTime as
<div>@DateTime.Now</div>With every re-render, it will show current time
On the other hand, the serialized render fragment will use the same value from initial pre-rendering on every re-render
<MyPanel @rendermode="wasm">
<div>@DateTime.Now</div>
</MyPanel>I'm not sure if this is expected and I can imagine debugging such scenario would be quite hard.
There was a problem hiding this comment.
This is expected, because we serialize results of the execution of RenderFragments. But I agree that we need to document this.
|
/backport to release/11.0-preview5 |
|
Started backporting to |
RenderFragment serialization
Summary
Enables non-generic
RenderFragmentparameters (e.g.,ChildContent) to cross interactive render mode boundaries (Server and WebAssembly). Previously, passing aRenderFragmentto a component with@rendermodethrew anInvalidOperationExceptionbecause delegates cannot be serialized. This PR introduces a serialization/deserialization mechanism that convertsRenderFragmentcontent into a JSON-serializable DTO during prerendering, allowing SSR-rendered content to be passed into interactive components.Lifecycle
%%{init: {'theme': 'neutral'}}%% sequenceDiagram participant Parent as Parent (SSR) participant Boundary as SSRRenderModeBoundary participant Serializer as RenderFragmentSerializer participant Deserializer as Param Deserializer<br/>(Server / WASM) participant Child as Interactive Child Note over Parent,Child: ── Prerender ── Parent->>Boundary: params (incl. RenderFragment) Boundary->>Boundary: Wrap top-level RenderFragments Note right of Boundary: Wraps each RenderFragment<br/>with a RenderFragmentCapture decorator Boundary->>Boundary: Prerender (invoke wrapped fragments) Note right of Boundary: Capture decorator records<br/>produced RenderTreeFrames and<br/>recursively wraps nested fragments Boundary->>Serializer: SerializeFrames(captured frames) Serializer-->>Boundary: SerializedRenderFragment DTO Boundary-->>Parent: HTML + marker (JSON with DTO) Note over Parent,Child: ── Interactive activation ── Parent-->>Deserializer: marker arrives (Server circuit / WASM boot) Deserializer->>Serializer: Deserialize(DTO nodes) Serializer-->>Deserializer: live RenderFragment delegate Deserializer->>Child: activate with hydrated paramsBy centralizing the wrapping logic in
RenderFragmentCapture, the same infrastructure can be reused anywhereRenderFragmentserialization is needed (e.g.CacheBoundary).Serialization rules
tag+attrs+childrenKeyTypeName/KeyTypeAssemblycontentstringFullName+Assembly+ paramsKeyTypeName/KeyTypeAssemblyRenderFragment<T>TypeName/TypeAssemblyKey lifecycle phases
Validation —
SSRRenderModeBoundary.ValidateParametersnow allowsRenderFragmentdelegates through;RenderFragment<T>and other delegate types are still rejected.Capture — In
SetParametersAsync, each top-levelRenderFragmentparameter is wrapped with aRenderFragmentCapturedecorator and stored in a per-boundary_topLevelCapturesdictionary. When the wrapper is invoked during prerendering, it records the produced render tree frames into its own buffer. NestedRenderFragmentparameters on components inside a fragment are recursively wrapped viaRenderFragmentCapture.WrapNestedFragments, which uses the newRenderTreeBuilder.SetAttributeValueAPI to replace the delegate values in-place in the live render buffer.Serialize —
RenderFragmentSerializer.SerializeFrameswalks the captured frame span and converts it intoRenderTreeNodeDTOs. It preserves element/component keys withKeyTypeName/KeyTypeAssemblyfor correct round-trip through JSON, attribute values withTypeName/TypeAssembly, and component type names viaFullName+Assembly. NestedRenderFragmentparameters resolve via the parent capture'sChildCaptureslookup (keyed by attribute frame index). Non-serializable frames (event handlers,EventCallback,RenderFragment<T>, element/component ref captures, render modes, named events) are skipped with structured log warnings.Transport —
BuildSerializableParameterViewproduces a copy of the parameter dictionary in which eachRenderFragmentvalue is replaced with aSerializedRenderFragmentDTO. The DTOs travel inside the existing component marker JSON.Deserialize — Both Server (
ComponentParameterDeserializer) and WebAssembly (WebAssemblyComponentParameterDeserializer) detect when a parameter's type name matchesSerializedRenderFragment(in theMicrosoft.AspNetCore.Components.Endpointsassembly) and callRenderFragmentSerializer.Deserializeto reconstruct a liveRenderFragmentdelegate that replays the serialized nodes into the interactive component's render tree builder. Nested serialized fragments inside component parameters are recursively rehydrated.Changes
New
RenderFragmentSerializershared class (src/Components/Shared/src/RenderFragmentSerializer.cs): Core serialization/deserialization logic, plus supporting types:SerializedRenderFragment— DTO wrapper containingList<RenderTreeNode>RenderTreeNode/RenderTreeAttribute— JSON-serializable representation of the render treeRenderFragment<T>)New
RenderFragmentCaptureshared class (src/Components/Shared/src/RenderFragmentCapture.cs): Decorator that wraps a singleRenderFragmentdelegate, records the frames it produces when invoked, and recursively wraps any nestedRenderFragmentparameters it encounters on child components.New
RenderTreeBuilder.SetAttributeValuepublic API (RenderTreeBuilder.cs/PublicAPI.Unshipped.txt): Replaces the attribute value of an existing frame at a given index. Used byRenderFragmentCapture.WrapNestedFragmentsto swap nestedRenderFragmentdelegates with their capture wrappers in-place.SSRRenderModeBoundaryupdated:ValidateParameterspermitsRenderFragment(still rejectsRenderFragment<T>and other delegates)SetParametersAsyncwraps top-levelRenderFragmentparameters withRenderFragmentCaptureinstances stored in_topLevelCaptures(only whenPrerender=true)BuildSerializableParameterViewreplacesRenderFragmentvalues withSerializedRenderFragmentDTOs before marker serialization, throwingInvalidOperationExceptionif aRenderFragmentparameter is encountered without a corresponding capture (i.e., when prerendering is disabled)ILoggerforRenderFragmentSerializerviaHttpContext.RequestServicesServer
ComponentParameterDeserializerupdated: DetectsSerializedRenderFragmenttype name and deserializes to a liveRenderFragmentWebAssembly
WebAssemblyComponentParameterDeserializerupdated: Same detection/deserialization, with[DynamicDependency]attributes added forSerializedRenderFragment,RenderTreeNode, andRenderTreeAttributeto preserve them during trimmingShared sources linked into Endpoints, Server, and WebAssembly projects via
<Compile Include>directives in their.csprojfilesKnown limitations
RenderFragment<T>(templated content) cannot cross render mode boundaries — skipped during serialization with a warning log.RenderFragmentserialization requires the fragment to be invoked during prerendering to capture its frames. Attempting to serialize aRenderFragmentparameter when the render mode hasPrerender=falsethrowsInvalidOperationException.EventCallback,@refcaptures,@rendermodedirectives, and@formnamenamed events inside serialized fragments are dropped with structured warning logs; only structural markup and component declarations survive the boundary.Testing
RenderFragmentSerializerTest.cs, 26 test methods): Cover serialization of text/markup/elements/components/regions, attribute and key type round-tripping, skipping of delegate/event-callback/ref/render-mode/named-event/generic-fragment frames, nested fragment deserialization, and end-to-end JSON round-trips for complex trees and typed keys (string, int, Guid).SSRRenderModeBoundaryTestupdated: existing tests now use aCreateHttpContext()helper that registersILoggerFactory(needed becauseSSRRenderModeBoundarynow creates a logger forRenderFragmentSerializer).RenderFragmentSerializationTest.cs, 7[Theory]scenarios × Server + WebAssembly = 14 cases): Cover simple text, nested elements, mixed content, attributes, nestedRenderFragmentparameters, components with typed parameters, and keyed elements crossing the boundary. New test assets:RenderFragmentInteractive.razorpage, plusRenderFragmentChild.razorandTypedParameterDisplay.razortest components.Fixes #52768