Skip to content

Clean-up Public API for RF serialization#67489

Open
dariatiurina wants to merge 1 commit into
dotnet:mainfrom
dariatiurina:66828-fix-api-serialization
Open

Clean-up Public API for RF serialization#67489
dariatiurina wants to merge 1 commit into
dotnet:mainfrom
dariatiurina:66828-fix-api-serialization

Conversation

@dariatiurina

@dariatiurina dariatiurina commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Clean-up Public API for RF serialization

Summary

Removes the newly added public RenderTreeBuilder.SetAttributeValue(int, object?) API and instead performs the same in-place frame mutation through an [UnsafeAccessor]. This keeps the RenderFragment serialization feature working without adding any new public surface to RenderTreeBuilder.

Background

To serialize RenderFragment content across render mode boundaries, RenderFragmentCapture swaps each nested RenderFragment delegate in the live render buffer with a capture wrapper, so that when the nested component invokes its parameter, control flows through the wrapper and its frames get recorded. The original implementation (#66528) exposed a public SetAttributeValue method on RenderTreeBuilder to perform that swap. This PR removes that public API.

Why a mutation accessor (or a public API) is needed — we can't write the value directly

RenderFragmentCapture already gets a ref to the exact frame in the live buffer (ref var attrFrame = ref frames.Array[j]), and RenderTreeFrame is a mutable struct, so the slot itself is writable. The blocker is accessibility, not ref-ness: there is no member we are allowed to assign through from this file.

  • The public RenderTreeFrame.AttributeValue property is get-only — there is no public setter.
  • The writable backing field, RenderTreeFrame.AttributeValueField, is internal to the core Microsoft.AspNetCore.Components assembly.
  • RenderFragmentCapture is shared source that compiles into Microsoft.AspNetCore.Components.Endpoints, .Server, and .WebAssembly — none of which can see that internal field.

So even with a writable ref RenderTreeFrame in hand, attrFrame.AttributeValueField = value does not compile across the assembly boundary (CS0122), and attrFrame.AttributeValue = value does not compile because the property has no setter (CS0200). Bridging that gap requires some mechanism that has access to the internal field. The options are:

  1. A public API on RenderTreeBuilder (the original SetAttributeValue) — but that permanently widens the public surface with a low-level, easily-misused mutation method.
  2. InternalsVisibleTo for the three runtime assemblies — but that broadly exposes all of core's internals to them and causes unrelated protected internal override ripple across those projects.
  3. [UnsafeAccessor] — resolves the internal AttributeValueField by name at the runtime layer and returns a ref object we can assign through, with no public API and no InternalsVisibleTo.

This PR takes option 3.

Changes

  • Removed public void SetAttributeValue(int frameIndex, object? value) from RenderTreeBuilder and its PublicAPI.Unshipped.txt entry.
  • RenderFragmentCapture.WrapNestedFragments now writes the wrapper delegate into the live frame buffer via an [UnsafeAccessor]-generated ref accessor over RenderTreeFrame.AttributeValueField, instead of calling builder.SetAttributeValue(...).

Details

  • The accessor binds to the field by name (Name = "AttributeValueField"), so a future rename of that field would surface as a MissingFieldException at first call rather than a compile-time error. The accessor sits next to the only call site to keep that coupling visible.
  • Behavior is unchanged: frames.Array is the live builder buffer, so the write is observed by the renderer exactly as before.

Testing

No test changes. The behavior is identical to the previous public-API implementation and is covered by the existing RenderFragment serialization tests.

Fixes #66828

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 removes the previously-added public RenderTreeBuilder.SetAttributeValue(int, object?) API (introduced to support RenderFragment serialization) and replaces that functionality with a local [UnsafeAccessor]-based write to RenderTreeFrame.AttributeValueField from the shared RenderFragmentCapture implementation. The goal is to keep RenderFragment serialization working without permanently expanding the public RenderTreeBuilder surface area.

Changes:

  • Replaced the call to RenderTreeBuilder.SetAttributeValue(...) with an [UnsafeAccessor] ref accessor that mutates RenderTreeFrame.AttributeValueField in-place.
  • Removed RenderTreeBuilder.SetAttributeValue(...) from the Components assembly implementation.
  • Removed the corresponding entry from PublicAPI.Unshipped.txt.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated no comments.

File Description
src/Components/Shared/src/RenderFragmentCapture.cs Swaps nested RenderFragment delegates in the live frame buffer via [UnsafeAccessor] instead of a public builder API.
src/Components/Components/src/Rendering/RenderTreeBuilder.cs Deletes the public SetAttributeValue method (and its argument/frame validation).
src/Components/Components/src/PublicAPI.Unshipped.txt Removes the unshipped public API declaration for SetAttributeValue.
@dariatiurina dariatiurina marked this pull request as ready for review June 30, 2026 13:28
@dariatiurina dariatiurina requested a review from a team as a code owner June 30, 2026 13:28
Comment on lines +92 to +94

[UnsafeAccessor(UnsafeAccessorKind.Field, Name = "AttributeValueField")]
private static extern ref object GetAttributeValueField(ref RenderTreeFrame frame);

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.

Hmm. I think I understand now. The fields are internal and the properties are get only. Isn't it? And the RTF is not constructible outside the assembly because everything else is internal. This is "cheating" as in it's piercing through the encapsulation and making this dependency implicit.

I'll take a look at this and see if we can come up with something different. If not, we might want to just retain the public API (and this discussion is what we should include on the description).

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.

Yes, I forgot this part of the implementation on the API review. But yes, the biggest problem is that we do not change RF itself, but the AttributeValueField, which is private.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

3 participants