Skip to content

RenderFragment serialization#66528

Merged
dariatiurina merged 14 commits into
dotnet:mainfrom
dariatiurina:52768-renderfragment-serialization
May 19, 2026
Merged

RenderFragment serialization#66528
dariatiurina merged 14 commits into
dotnet:mainfrom
dariatiurina:52768-renderfragment-serialization

Conversation

@dariatiurina

@dariatiurina dariatiurina commented Apr 29, 2026

Copy link
Copy Markdown
Contributor

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

@github-actions github-actions Bot added the area-blazor Includes: Blazor, Razor Components label Apr 29, 2026
@dariatiurina dariatiurina self-assigned this Apr 29, 2026
@dariatiurina dariatiurina added this to the 11.0-preview5 milestone Apr 29, 2026
@dariatiurina dariatiurina marked this pull request as ready for review April 29, 2026 17:04
@dariatiurina dariatiurina requested a review from a team as a code owner April 29, 2026 17:04
Copilot AI review requested due to automatic review settings April 29, 2026 17:04

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 SSRRenderModeBoundary to allow RenderFragment parameters and replace them with SerializedRenderFragment before marker serialization.
  • Updates Server and WebAssembly parameter deserializers to detect SerializedRenderFragment and rehydrate it back into a live RenderFragment, 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.

Comment thread src/Components/Shared/src/RenderFragmentSerializer.cs Outdated
Comment thread src/Components/Shared/src/RenderFragmentSerializer.cs Outdated
Comment thread src/Components/Shared/src/RenderFragmentSerializer.cs
Comment thread src/Components/Shared/src/RenderFragmentSerializer.cs Outdated
Comment thread src/Components/Shared/src/RenderFragmentSerializer.cs Outdated
Comment thread src/Components/Shared/src/RenderFragmentSerializer.cs
dariatiurina and others added 3 commits April 30, 2026 12:16
Co-authored-by: Copilot <copilot@github.com>
Comment thread src/Components/Shared/src/RenderFragmentSerializer.cs Outdated
Comment thread src/Components/Shared/src/RenderFragmentSerializer.cs Outdated
Comment thread src/Components/Shared/src/RenderFragmentSerializer.cs Outdated
}

[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)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use JsonSerializerContext

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 15 out of 15 changed files in this pull request and generated 4 comments.

Comment thread src/Components/Endpoints/src/Rendering/SSRRenderModeBoundary.cs
Comment thread src/Components/Components/src/Rendering/RenderTreeBuilder.cs
Comment thread src/Components/Components/src/Rendering/RenderTreeBuilder.cs
Comment thread src/Components/Shared/src/RenderFragmentSerializer.cs Outdated
Comment thread src/Components/Components/src/Rendering/RenderTreeBuilder.cs
Comment thread src/Components/Endpoints/src/Rendering/SSRRenderModeBoundary.cs Outdated
Comment thread src/Components/Endpoints/src/Rendering/SSRRenderModeBoundary.cs Outdated

@oroztocil oroztocil left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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!

Comment thread src/Components/Shared/src/RenderFragmentSerializer.cs
Comment thread src/Components/Shared/src/RenderFragmentSerializer.cs Outdated
Comment thread src/Components/Shared/src/RenderFragmentSerializer.cs Outdated
Comment thread src/Components/Shared/src/RenderFragmentSerializer.cs Outdated
Comment thread src/Components/Shared/src/RenderFragmentSerializer.cs Outdated
Comment thread src/Components/Shared/src/RenderFragmentSerializer.cs
Comment thread src/Components/Shared/src/RenderFragmentSerializer.cs

public IReadOnlyDictionary<int, RenderFragmentCapture> ChildCaptures => _childCaptures;

public void Invoke(RenderTreeBuilder builder)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?)

@dariatiurina dariatiurina May 19, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/Components/Shared/src/RenderFragmentSerializer.cs Outdated
Comment thread src/Components/Shared/src/RenderFragmentSerializer.cs Outdated
Comment thread src/Components/Endpoints/src/Rendering/SSRRenderModeBoundary.cs Outdated
Comment thread src/Components/Endpoints/src/Rendering/SSRRenderModeBoundary.cs
Comment thread src/Components/Shared/src/RenderFragmentSerializer.cs Outdated
{
var start = builder.GetFrames().Count;
_original(builder);
var end = builder.GetFrames().Count;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Since this call, GetFrames always returns the same "value", right? Can we omit the subsequent GetFrames calls and reuse what we got here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_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">

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is expected, because we serialize results of the execution of RenderFragments. But I agree that we need to document this.

Comment thread src/Components/Endpoints/test/RenderFragmentSerializerTest.cs Outdated
Comment thread src/Components/Shared/src/RenderFragmentSerializer.cs Outdated
@dariatiurina dariatiurina enabled auto-merge (squash) May 19, 2026 19:29
@dariatiurina dariatiurina merged commit 654900e into dotnet:main May 19, 2026
25 checks passed
@dariatiurina dariatiurina deleted the 52768-renderfragment-serialization branch May 19, 2026 19:37
@dariatiurina

Copy link
Copy Markdown
Contributor Author

/backport to release/11.0-preview5

@github-actions

Copy link
Copy Markdown
Contributor

Started backporting to release/11.0-preview5 (link to workflow run)

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Blazor] Support serializing render fragments from SSR

5 participants