Skip to content

[release/11.0-preview5] RenderFragment serialization#66754

Merged
wtgodbe merged 14 commits into
release/11.0-preview5from
backport/pr-66528-to-release/11.0-preview5
May 20, 2026
Merged

[release/11.0-preview5] RenderFragment serialization#66754
wtgodbe merged 14 commits into
release/11.0-preview5from
backport/pr-66528-to-release/11.0-preview5

Conversation

@github-actions

@github-actions github-actions Bot commented May 20, 2026

Copy link
Copy Markdown
Contributor

Backport of #66528 to release/11.0-preview5

/cc @dariatiurina

RenderFragment serialization

Summary

Enables non-generic RenderFragment parameters (e.g., ChildContent) to cross interactive render mode boundaries (Server and WebAssembly). Previously, passing a RenderFragment to a component with @rendermode threw an InvalidOperationException because delegates cannot be serialized. This PR introduces a serialization/deserialization mechanism that converts RenderFragment content 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 params
Loading

By centralizing the wrapping logic in RenderFragmentCapture, the same infrastructure can be reused anywhere RenderFragment serialization is needed (e.g. CacheBoundary).

Serialization rules

Frame type Serialized as Notes
Element tag + attrs + children Keys preserved with KeyTypeName/KeyTypeAssembly
Text / Markup content string
Component FullName + Assembly + params Keys preserved with KeyTypeName/KeyTypeAssembly
Region Transparent — children inlined
Delegates / EventCallback Skipped Warning logged
RenderFragment<T> Skipped Warning logged
Ref captures / render modes / named events Skipped Warning logged
Typed attribute values Value + TypeName/TypeAssembly Correct round-trip through JSON

Key lifecycle phases

  1. ValidationSSRRenderModeBoundary.ValidateParameters now allows RenderFragment delegates through; RenderFragment<T> and other delegate types are still rejected.

  2. Capture — In SetParametersAsync, each top-level RenderFragment parameter is wrapped with a RenderFragmentCapture decorator and stored in a per-boundary _topLevelCaptures dictionary. When the wrapper is invoked during prerendering, it records the produced render tree frames into its own buffer. Nested RenderFragment parameters on components inside a fragment are recursively wrapped via RenderFragmentCapture.WrapNestedFragments, which uses the new RenderTreeBuilder.SetAttributeValue API to replace the delegate values in-place in the live render buffer.

  3. SerializeRenderFragmentSerializer.SerializeFrames walks the captured frame span and converts it into RenderTreeNode DTOs. It preserves element/component keys with KeyTypeName/KeyTypeAssembly for correct round-trip through JSON, attribute values with TypeName/TypeAssembly, and component type names via FullName + Assembly. Nested RenderFragment parameters resolve via the parent capture's ChildCaptures lookup (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.

  4. TransportBuildSerializableParameterView produces a copy of the parameter dictionary in which each RenderFragment value is replaced with a SerializedRenderFragment DTO. The DTOs travel inside the existing component marker JSON.

  5. Deserialize — Both Server (ComponentParameterDeserializer) and WebAssembly (WebAssemblyComponentParameterDeserializer) detect when a parameter's type name matches SerializedRenderFragment (in the Microsoft.AspNetCore.Components.Endpoints assembly) and call RenderFragmentSerializer.Deserialize to reconstruct a live RenderFragment delegate that replays the serialized nodes into the interactive component's render tree builder. Nested serialized fragments inside component parameters are recursively rehydrated.

Changes

  • New RenderFragmentSerializer shared class (src/Components/Shared/src/RenderFragmentSerializer.cs): Core serialization/deserialization logic, plus supporting types:

    • SerializedRenderFragment — DTO wrapper containing List<RenderTreeNode>
    • RenderTreeNode / RenderTreeAttribute — JSON-serializable representation of the render tree
    • 6 structured log message categories for skipped frame types (event handlers, element/component refs, render modes, named events, generic RenderFragment<T>)
  • New RenderFragmentCapture shared class (src/Components/Shared/src/RenderFragmentCapture.cs): Decorator that wraps a single RenderFragment delegate, records the frames it produces when invoked, and recursively wraps any nested RenderFragment parameters it encounters on child components.

  • New RenderTreeBuilder.SetAttributeValue public API (RenderTreeBuilder.cs / PublicAPI.Unshipped.txt): Replaces the attribute value of an existing frame at a given index. Used by RenderFragmentCapture.WrapNestedFragments to swap nested RenderFragment delegates with their capture wrappers in-place.

  • SSRRenderModeBoundary updated:

    • ValidateParameters permits RenderFragment (still rejects RenderFragment<T> and other delegates)
    • SetParametersAsync wraps top-level RenderFragment parameters with RenderFragmentCapture instances stored in _topLevelCaptures (only when Prerender=true)
    • New BuildSerializableParameterView replaces RenderFragment values with SerializedRenderFragment DTOs before marker serialization, throwing InvalidOperationException if a RenderFragment parameter is encountered without a corresponding capture (i.e., when prerendering is disabled)
    • Creates an ILogger for RenderFragmentSerializer via HttpContext.RequestServices
  • Server ComponentParameterDeserializer updated: Detects SerializedRenderFragment type name and deserializes to a live RenderFragment

  • WebAssembly WebAssemblyComponentParameterDeserializer updated: Same detection/deserialization, with [DynamicDependency] attributes added for SerializedRenderFragment, RenderTreeNode, and RenderTreeAttribute to preserve them during trimming

  • Shared sources linked into Endpoints, Server, and WebAssembly projects via <Compile Include> directives in their .csproj files

Known limitations

  • RenderFragment<T> (templated content) cannot cross render mode boundaries — skipped during serialization with a warning log.
  • Prerender must be enabledRenderFragment serialization requires the fragment to be invoked during prerendering to capture its frames. Attempting to serialize a RenderFragment parameter when the render mode has Prerender=false throws InvalidOperationException.
  • Event handlers, EventCallback, @ref captures, @rendermode directives, and @formname named events inside serialized fragments are dropped with structured warning logs; only structural markup and component declarations survive the boundary.

Testing

  • Unit tests (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).
  • SSRRenderModeBoundaryTest updated: existing tests now use a CreateHttpContext() helper that registers ILoggerFactory (needed because SSRRenderModeBoundary now creates a logger for RenderFragmentSerializer).
  • E2E tests (RenderFragmentSerializationTest.cs, 7 [Theory] scenarios × Server + WebAssembly = 14 cases): Cover simple text, nested elements, mixed content, attributes, nested RenderFragment parameters, components with typed parameters, and keyed elements crossing the boundary. New test assets: RenderFragmentInteractive.razor page, plus RenderFragmentChild.razor and TypedParameterDisplay.razor test components.

Fixes #52768

Customer Impact

New feature to test in P5.

Regression?

  • Yes
  • No

Risk

  • High
  • Medium
  • Low

New feature, no users will be impacted by changes.

Verification

  • Manual (required)
  • Automated

Packaging changes reviewed?

  • Yes
  • No
  • N/A

When servicing release/2.3

  • Make necessary changes in eng/PatchConfig.props

@github-actions github-actions Bot requested a review from a team as a code owner May 20, 2026 15:59
@ilonatommy ilonatommy added this to the 11.0-preview5 milestone May 20, 2026
@lewing lewing added the Servicing-approved Shiproom has approved the issue label May 20, 2026
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Hi @github-actions[bot]. This PR was just approved to be included in the upcoming servicing release. Somebody from the @dotnet/aspnet-build team will get it merged when the branches are open. Until then, please make sure all the CI checks pass and the PR is reviewed.

@wtgodbe

wtgodbe commented May 20, 2026

Copy link
Copy Markdown
Member

Mac machines are down right now. Change was previously validated against main - merging

@wtgodbe wtgodbe merged commit febfa18 into release/11.0-preview5 May 20, 2026
23 of 30 checks passed
@wtgodbe wtgodbe deleted the backport/pr-66528-to-release/11.0-preview5 branch May 20, 2026 19:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-blazor Includes: Blazor, Razor Components Servicing-approved Shiproom has approved the issue

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants