Clean-up Public API for RF serialization#67489
Conversation
There was a problem hiding this comment.
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 mutatesRenderTreeFrame.AttributeValueFieldin-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. |
|
|
||
| [UnsafeAccessor(UnsafeAccessorKind.Field, Name = "AttributeValueField")] | ||
| private static extern ref object GetAttributeValueField(ref RenderTreeFrame frame); |
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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.
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 theRenderFragmentserialization feature working without adding any new public surface toRenderTreeBuilder.Background
To serialize
RenderFragmentcontent across render mode boundaries,RenderFragmentCaptureswaps each nestedRenderFragmentdelegate 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 publicSetAttributeValuemethod onRenderTreeBuilderto 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
RenderFragmentCapturealready gets arefto the exact frame in the live buffer (ref var attrFrame = ref frames.Array[j]), andRenderTreeFrameis 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.RenderTreeFrame.AttributeValueproperty is get-only — there is no public setter.RenderTreeFrame.AttributeValueField, isinternalto the coreMicrosoft.AspNetCore.Componentsassembly.RenderFragmentCaptureis shared source that compiles intoMicrosoft.AspNetCore.Components.Endpoints,.Server, and.WebAssembly— none of which can see that internal field.So even with a writable
ref RenderTreeFramein hand,attrFrame.AttributeValueField = valuedoes not compile across the assembly boundary (CS0122), andattrFrame.AttributeValue = valuedoes 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:RenderTreeBuilder(the originalSetAttributeValue) — but that permanently widens the public surface with a low-level, easily-misused mutation method.InternalsVisibleTofor the three runtime assemblies — but that broadly exposes all of core's internals to them and causes unrelatedprotected internaloverride ripple across those projects.[UnsafeAccessor]— resolves the internalAttributeValueFieldby name at the runtime layer and returns aref objectwe can assign through, with no public API and noInternalsVisibleTo.This PR takes option 3.
Changes
public void SetAttributeValue(int frameIndex, object? value)fromRenderTreeBuilderand itsPublicAPI.Unshipped.txtentry.RenderFragmentCapture.WrapNestedFragmentsnow writes the wrapper delegate into the live frame buffer via an[UnsafeAccessor]-generatedrefaccessor overRenderTreeFrame.AttributeValueField, instead of callingbuilder.SetAttributeValue(...).Details
Name = "AttributeValueField"), so a future rename of that field would surface as aMissingFieldExceptionat first call rather than a compile-time error. The accessor sits next to the only call site to keep that coupling visible.frames.Arrayis 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
RenderFragmentserialization tests.Fixes #66828