Skip to content
1 change: 1 addition & 0 deletions src/Components/Components/src/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ abstract Microsoft.AspNetCore.Components.CascadingParameterSubscription.Dispose(
abstract Microsoft.AspNetCore.Components.CascadingParameterSubscription.GetCurrentValue() -> object?
Microsoft.AspNetCore.Components.CascadingParameterSubscription
Microsoft.AspNetCore.Components.CascadingParameterSubscription.CascadingParameterSubscription() -> void
Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder.SetAttributeValue(int frameIndex, object? value) -> void
static Microsoft.AspNetCore.Components.NavigationManagerExtensions.GetUriWithHash(this Microsoft.AspNetCore.Components.NavigationManager! navigationManager, string! hash) -> string!
Microsoft.AspNetCore.Components.NavigationOptions.RelativeToCurrentUri.get -> bool
Microsoft.AspNetCore.Components.NavigationOptions.RelativeToCurrentUri.init -> void
Expand Down
29 changes: 29 additions & 0 deletions src/Components/Components/src/Rendering/RenderTreeBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -777,6 +777,35 @@ internal bool InsertAttributeExpensive(int insertAtIndex, int sequence, string a
public ArrayRange<RenderTreeFrame> GetFrames() =>
_entries.ToRange();

/// <summary>
/// Replaces the attribute value of an existing attribute frame at the specified index.
/// This is used to update attribute values in-place after frames have been appended,
/// for example when wrapping <see cref="RenderFragment"/> delegates during serialization.
/// </summary>
/// <param name="frameIndex">The zero-based index of the attribute frame whose value should be replaced.</param>
/// <param name="value">The new attribute value.</param>
/// <exception cref="ArgumentOutOfRangeException">Thrown when <paramref name="frameIndex"/> is outside the range of appended frames.</exception>
/// <exception cref="InvalidOperationException">Thrown when the frame at <paramref name="frameIndex"/> is not of type <see cref="RenderTreeFrameType.Attribute"/>.</exception>
public void SetAttributeValue(int frameIndex, object? value)
{
var frames = _entries.Buffer;
var count = _entries.Count;

if ((uint)frameIndex >= (uint)count)
{
throw new ArgumentOutOfRangeException(nameof(frameIndex));
}

ref var frame = ref frames[frameIndex];
if (frame.FrameTypeField != RenderTreeFrameType.Attribute)
{
throw new InvalidOperationException(
$"The frame at index {frameIndex} is of type '{frame.FrameTypeField}', not '{RenderTreeFrameType.Attribute}'.");
}

frame.AttributeValueField = value;
}

internal void AssertTreeIsValid(IComponent component)
{
if (_openElementIndices.Count > 0)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@
<Compile Include="$(ComponentsSharedSourceRoot)src\HotReloadManager.cs" LinkBase="HotReload" />
<Compile Include="$(ComponentsSharedSourceRoot)src\Reflection\PropertyGetter.cs" />
<Compile Include="$(ComponentsSharedSourceRoot)src\Reflection\PropertySetter.cs" />
<Compile Include="$(ComponentsSharedSourceRoot)src\RenderFragmentSerializer.cs" />
<Compile Include="$(ComponentsSharedSourceRoot)src\RenderFragmentCapture.cs" />
<Compile Include="$(ComponentsSharedSourceRoot)src\ComponentParametersTypeCache.cs" />

<Compile Include="$(SharedSourceRoot)PropertyHelper\**\*.cs" />

Expand Down
75 changes: 65 additions & 10 deletions src/Components/Endpoints/src/Rendering/SSRRenderModeBoundary.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Components.Rendering;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using static Microsoft.AspNetCore.Internal.LinkerFlags;

namespace Microsoft.AspNetCore.Components.Endpoints;
Expand All @@ -27,8 +29,10 @@ internal class SSRRenderModeBoundary : IComponent
private readonly bool _prerender;
private RenderHandle _renderHandle;
private IReadOnlyDictionary<string, object?>? _latestParameters;
private Dictionary<string, RenderFragmentCapture>? _topLevelCaptures;
private ComponentMarkerKey? _markerKey;
private readonly HttpContext _httpContext;
private ILogger? _renderFragmentSerializationLogger;

public IComponentRenderMode RenderMode { get; }

Expand Down Expand Up @@ -108,6 +112,24 @@ public Task SetParametersAsync(ParameterView parameters)

ValidateParameters(_latestParameters);

if (_prerender)
{
// Replace each top-level RenderFragment parameter with a capture wrapper
// so that when the component invokes the fragment during prerendering,
// the output frames are captured for later serialization.
var parametersDict = (Dictionary<string, object?>)_latestParameters;
foreach (var name in parametersDict.Keys.ToArray())
{
if (parametersDict[name] is RenderFragment rf)
{
var capture = new RenderFragmentCapture(rf);
_topLevelCaptures ??= new();
_topLevelCaptures[name] = capture;
parametersDict[name] = (RenderFragment)capture.Invoke;
}
}
}

if (RenderMode is InteractiveWebAssemblyRenderMode)
{
// Preload WebAssembly assets when using WebAssembly (not Auto) mode
Expand Down Expand Up @@ -150,13 +172,10 @@ private void ValidateParameters(IReadOnlyDictionary<string, object?> latestParam
{
throw new InvalidOperationException($"Cannot pass RenderFragment<T> parameter '{name}' to component '{_componentType.Name}' with rendermode '{RenderMode.GetType().Name}'. Templated content can't be passed across a rendermode boundary, because it is arbitrary code and cannot be serialized.");
}
else
else if (value is not RenderFragment)
{
// TODO: Ideally we *should* support RenderFragment (the non-generic version) by prerendering it
// However it's very nontrivial since it means we have to execute it within the current renderer
// somehow without actually emitting its result directly, wait for quiescence, and then prerender
// the output into a separate buffer so we can serialize it in a special way.
// A prototype implementation is at https://github.com/dotnet/aspnetcore/commit/ed330ff5b143974d9060828a760ad486b1d386ac
// Non-RenderFragment delegates (event handlers, etc.) can't cross render mode boundaries.
// RenderFragment is allowed and will be serialized in ToMarker().
throw new InvalidOperationException($"Cannot pass the parameter '{name}' to component '{_componentType.Name}' with rendermode '{RenderMode.GetType().Name}'. This is because the parameter is of the delegate type '{value.GetType()}', which is arbitrary code and cannot be serialized.");
}
}
Expand All @@ -181,9 +200,14 @@ public ComponentMarker ToMarker(HttpContext httpContext, int sequence, object? c
// so we lazily compute the marker key once.
_markerKey ??= GenerateMarkerKey(sequence, componentKey);

var parameters = _latestParameters is null
// Build a serialization-safe copy of parameters, replacing RenderFragment delegates with DTOs
_renderFragmentSerializationLogger ??= httpContext.RequestServices
.GetRequiredService<ILoggerFactory>()
.CreateLogger(typeof(RenderFragmentSerializer));

var serializableParameters = _latestParameters is null
? ParameterView.Empty
: ParameterView.FromDictionary((IDictionary<string, object?>)_latestParameters);
: BuildSerializableParameterView(_latestParameters, _renderFragmentSerializationLogger);

var marker = RenderMode switch
{
Expand All @@ -200,17 +224,48 @@ public ComponentMarker ToMarker(HttpContext httpContext, int sequence, object? c
var serverComponentSerializer = httpContext.RequestServices.GetRequiredService<ServerComponentSerializer>();

var invocationId = EndpointHtmlRenderer.GetOrCreateInvocationId(httpContext);
serverComponentSerializer.SerializeInvocation(ref marker, invocationId, _componentType, parameters);
serverComponentSerializer.SerializeInvocation(ref marker, invocationId, _componentType, serializableParameters);
}

if (RenderMode is InteractiveWebAssemblyRenderMode or InteractiveAutoRenderMode)
{
WebAssemblyComponentSerializer.SerializeInvocation(ref marker, _componentType, parameters);
WebAssemblyComponentSerializer.SerializeInvocation(ref marker, _componentType, serializableParameters);
}

return marker;
}

private ParameterView BuildSerializableParameterView(
IReadOnlyDictionary<string, object?> latestParameters,
ILogger logger)
{
var dict = new Dictionary<string, object?>(latestParameters.Count);
foreach (var (name, value) in latestParameters)
{
if (value is RenderFragment)
{
if (_topLevelCaptures is null || !_topLevelCaptures.TryGetValue(name, out var capture))
{
// If we didn't wrap the RenderFragment in a capture, it means prerendering is disabled.
// If the capture is null, then fragment was conditionally rendered and didn't execute. In either case we can't serialize it.
throw new InvalidOperationException(
$"Cannot serialize RenderFragment parameter '{name}' for component '{_componentType.Name}', because the RenderFragment was not executed. It can be due to disabled prerendering or conditional rendering.");
}

dict[name] = new SerializedRenderFragment
{
Nodes = RenderFragmentSerializer.SerializeFrames(capture, logger, _componentType.Name)
};
}
else
{
dict[name] = value;
}
}

return ParameterView.FromDictionary(dict);
}

private ComponentMarkerKey GenerateMarkerKey(int sequence, object? componentKey)
{
var componentTypeNameHash = _componentTypeNameHashCache.GetOrAdd(_componentType, TypeNameHash.Compute);
Expand Down
Loading
Loading