Skip to content

[release/8.0-preview7] [Blazor] Improvements to the interaction between SSR and interactive rendering #49557

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jul 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,10 @@ public ServerComponentSerializer(IDataProtectionProvider dataProtectionProvider)
.CreateProtector(ServerComponentSerializationSettings.DataProtectionProviderPurpose)
.ToTimeLimitedDataProtector();

public ServerComponentMarker SerializeInvocation(ServerComponentInvocationSequence invocationId, Type type, ParameterView parameters, bool prerendered)
public ServerComponentMarker SerializeInvocation(ServerComponentInvocationSequence invocationId, Type type, ParameterView parameters, string key, bool prerendered)
{
var (sequence, serverComponent) = CreateSerializedServerComponent(invocationId, type, parameters);
return prerendered ? ServerComponentMarker.Prerendered(sequence, serverComponent) : ServerComponentMarker.NonPrerendered(sequence, serverComponent);
return prerendered ? ServerComponentMarker.Prerendered(sequence, serverComponent, key) : ServerComponentMarker.NonPrerendered(sequence, serverComponent, key);
}

private (int sequence, string payload) CreateSerializedServerComponent(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ namespace Microsoft.AspNetCore.Components.Endpoints;
// See the details of the component serialization protocol in WebAssemblyComponentDeserializer.cs on the Components solution.
internal sealed class WebAssemblyComponentSerializer
{
public static WebAssemblyComponentMarker SerializeInvocation(Type type, ParameterView parameters, bool prerendered)
public static WebAssemblyComponentMarker SerializeInvocation(Type type, ParameterView parameters, string? key, bool prerendered)
{
var assembly = type.Assembly.GetName().Name ?? throw new InvalidOperationException("Cannot prerender components from assemblies with a null name");
var typeFullName = type.FullName ?? throw new InvalidOperationException("Cannot prerender component types with a null name");
Expand All @@ -19,8 +19,8 @@ public static WebAssemblyComponentMarker SerializeInvocation(Type type, Paramete
var serializedDefinitions = Convert.ToBase64String(JsonSerializer.SerializeToUtf8Bytes(definitions, WebAssemblyComponentSerializationSettings.JsonSerializationOptions));
var serializedValues = Convert.ToBase64String(JsonSerializer.SerializeToUtf8Bytes(values, WebAssemblyComponentSerializationSettings.JsonSerializationOptions));

return prerendered ? WebAssemblyComponentMarker.Prerendered(assembly, typeFullName, serializedDefinitions, serializedValues) :
WebAssemblyComponentMarker.NonPrerendered(assembly, typeFullName, serializedDefinitions, serializedValues);
return prerendered ? WebAssemblyComponentMarker.Prerendered(assembly, typeFullName, serializedDefinitions, serializedValues, key) :
WebAssemblyComponentMarker.NonPrerendered(assembly, typeFullName, serializedDefinitions, serializedValues, key);
}

internal static void AppendPreamble(TextWriter writer, WebAssemblyComponentMarker record)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,14 @@ private static void HandleNavigationAfterResponseStarted(TextWriter writer, stri
protected override void WriteComponentHtml(int componentId, TextWriter output)
=> WriteComponentHtml(componentId, output, allowBoundaryMarkers: true);

private void WriteComponentHtml(int componentId, TextWriter output, bool allowBoundaryMarkers)
protected override void RenderChildComponent(TextWriter output, ref RenderTreeFrame componentFrame)
{
var componentId = componentFrame.ComponentId;
var sequenceAndKey = new SequenceAndKey(componentFrame.Sequence, componentFrame.ComponentKey);
WriteComponentHtml(componentId, output, allowBoundaryMarkers: true, sequenceAndKey);
}

private void WriteComponentHtml(int componentId, TextWriter output, bool allowBoundaryMarkers, SequenceAndKey sequenceAndKey = default)
{
_visitedComponentIdsInCurrentStreamingBatch?.Add(componentId);

Expand All @@ -198,9 +205,8 @@ private void WriteComponentHtml(int componentId, TextWriter output, bool allowBo
// It may be better to use a custom element like <blazor-component ...>[prerendered]<blazor-component>
// so it's easier for the JS code to react automatically whenever this gets inserted or updated during
// streaming SSR or progressively-enhanced navigation.

var (serverMarker, webAssemblyMarker) = componentState.Component is SSRRenderModeBoundary boundary
? boundary.ToMarkers(_httpContext)
? boundary.ToMarkers(_httpContext, sequenceAndKey.Sequence, sequenceAndKey.Key)
: default;

if (serverMarker.HasValue)
Expand Down Expand Up @@ -246,4 +252,5 @@ private void WriteComponentHtml(int componentId, TextWriter output, bool allowBo
}

private readonly record struct ComponentIdAndDepth(int ComponentId, int Depth);
private readonly record struct SequenceAndKey(int Sequence, object? Key);
}
43 changes: 40 additions & 3 deletions src/Components/Endpoints/src/Rendering/SSRRenderModeBoundary.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections.Concurrent;
using System.Globalization;
using System.Security.Cryptography;
using System.Text;
using Microsoft.AspNetCore.Components.Rendering;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Http;
Expand All @@ -14,11 +18,14 @@ namespace Microsoft.AspNetCore.Components.Endpoints;
/// </summary>
internal class SSRRenderModeBoundary : IComponent
{
private static readonly ConcurrentDictionary<Type, string> _componentTypeNameHashCache = new();

private readonly Type _componentType;
private readonly IComponentRenderMode _renderMode;
private readonly bool _prerender;
private RenderHandle _renderHandle;
private IReadOnlyDictionary<string, object?>? _latestParameters;
private string? _markerKey;

public SSRRenderModeBoundary(Type componentType, IComponentRenderMode renderMode)
{
Expand Down Expand Up @@ -92,8 +99,12 @@ private void Prerender(RenderTreeBuilder builder)
builder.CloseComponent();
}

public (ServerComponentMarker?, WebAssemblyComponentMarker?) ToMarkers(HttpContext httpContext)
public (ServerComponentMarker?, WebAssemblyComponentMarker?) ToMarkers(HttpContext httpContext, int sequence, object? key)
{
// We expect that the '@key' and sequence number shouldn't change for a given component instance,
// so we lazily compute the marker key once.
_markerKey ??= GenerateMarkerKey(sequence, key);

var parameters = _latestParameters is null
? ParameterView.Empty
: ParameterView.FromDictionary((IDictionary<string, object?>)_latestParameters);
Expand All @@ -106,15 +117,41 @@ private void Prerender(RenderTreeBuilder builder)
var serverComponentSerializer = httpContext.RequestServices.GetRequiredService<ServerComponentSerializer>();

var invocationId = EndpointHtmlRenderer.GetOrCreateInvocationId(httpContext);
serverMarker = serverComponentSerializer.SerializeInvocation(invocationId, _componentType, parameters, _prerender);
serverMarker = serverComponentSerializer.SerializeInvocation(invocationId, _componentType, parameters, _markerKey, _prerender);
}

WebAssemblyComponentMarker? webAssemblyMarker = null;
if (_renderMode is WebAssemblyRenderMode or AutoRenderMode)
{
webAssemblyMarker = WebAssemblyComponentSerializer.SerializeInvocation(_componentType, parameters, _prerender);
webAssemblyMarker = WebAssemblyComponentSerializer.SerializeInvocation(_componentType, parameters, _markerKey, _prerender);
}

return (serverMarker, webAssemblyMarker);
}

private string GenerateMarkerKey(int sequence, object? key)
{
var componentTypeNameHash = _componentTypeNameHashCache.GetOrAdd(_componentType, ComputeComponentTypeNameHash);
return $"{componentTypeNameHash}:{sequence}:{(key as IFormattable)?.ToString(null, CultureInfo.InvariantCulture)}";
}

private static string ComputeComponentTypeNameHash(Type componentType)
{
if (componentType.FullName is not { } typeName)
{
throw new InvalidOperationException($"An invalid component type was used in {nameof(SSRRenderModeBoundary)}.");
}

var typeNameLength = typeName.Length;
var typeNameBytes = typeNameLength < 1024
? stackalloc byte[typeNameLength]
: new byte[typeNameLength];

Encoding.UTF8.GetBytes(typeName, typeNameBytes);

Span<byte> typeNameHashBytes = stackalloc byte[SHA1.HashSizeInBytes];
SHA1.HashData(typeNameBytes, typeNameHashBytes);

return Convert.ToHexString(typeNameHashBytes);
}
}
2 changes: 2 additions & 0 deletions src/Components/Server/src/Circuits/CircuitFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,12 +64,14 @@ public async ValueTask<CircuitHost> CreateCircuitHostAsync(
var appLifetime = scope.ServiceProvider.GetRequiredService<ComponentStatePersistenceManager>();
await appLifetime.RestoreStateAsync(store);

var serverComponentDeserializer = scope.ServiceProvider.GetRequiredService<IServerComponentDeserializer>();
var jsComponentInterop = new CircuitJSComponentInterop(_options);
var renderer = new RemoteRenderer(
scope.ServiceProvider,
_loggerFactory,
_options,
client,
serverComponentDeserializer,
_loggerFactory.CreateLogger<RemoteRenderer>(),
jsRuntime,
jsComponentInterop);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Diagnostics.CodeAnalysis;

namespace Microsoft.AspNetCore.Components.Server;

internal interface IServerComponentDeserializer
{
bool TryDeserializeComponentDescriptorCollection(
string serializedComponentRecords,
out List<ComponentDescriptor> descriptors);

bool TryDeserializeSingleComponentDescriptor(ServerComponentMarker record, [NotNullWhen(true)] out ComponentDescriptor? result);
}
86 changes: 86 additions & 0 deletions src/Components/Server/src/Circuits/RemoteRenderer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@

using System.Collections.Concurrent;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
using System.Text.Json;
using Microsoft.AspNetCore.Components.RenderTree;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.SignalR;
Expand All @@ -21,6 +23,7 @@ internal partial class RemoteRenderer : WebRenderer

private readonly CircuitClientProxy _client;
private readonly CircuitOptions _options;
private readonly IServerComponentDeserializer _serverComponentDeserializer;
private readonly ILogger _logger;
internal readonly ConcurrentQueue<UnacknowledgedRenderBatch> _unacknowledgedRenderBatches = new ConcurrentQueue<UnacknowledgedRenderBatch>();
private long _nextRenderId = 1;
Expand All @@ -39,13 +42,15 @@ public RemoteRenderer(
ILoggerFactory loggerFactory,
CircuitOptions options,
CircuitClientProxy client,
IServerComponentDeserializer serverComponentDeserializer,
ILogger logger,
RemoteJSRuntime jsRuntime,
CircuitJSComponentInterop jsComponentInterop)
: base(serviceProvider, loggerFactory, jsRuntime.ReadJsonSerializerOptions(), jsComponentInterop)
{
_client = client;
_options = options;
_serverComponentDeserializer = serverComponentDeserializer;
_logger = logger;

ElementReferenceContext = jsRuntime.ElementReferenceContext;
Expand All @@ -59,12 +64,90 @@ public Task AddComponentAsync(Type componentType, ParameterView parameters, stri
return RenderRootComponentAsync(componentId, parameters);
}

protected override int GetWebRendererId() => (int)WebRendererId.Server;

protected override void AttachRootComponentToBrowser(int componentId, string domElementSelector)
{
var attachComponentTask = _client.SendAsync("JS.AttachComponent", componentId, domElementSelector);
_ = CaptureAsyncExceptions(attachComponentTask);
}

protected override void UpdateRootComponents(string operationsJson)
{
var operations = JsonSerializer.Deserialize<IEnumerable<RootComponentOperation<ServerComponentMarker>>>(
operationsJson,
ServerComponentSerializationSettings.JsonSerializationOptions);

foreach (var operation in operations)
{
switch (operation.Type)
{
case RootComponentOperationType.Add:
AddRootComponent(operation);
break;
case RootComponentOperationType.Update:
UpdateRootComponent(operation);
break;
case RootComponentOperationType.Remove:
RemoveRootComponent(operation);
break;
}
}

return;

void AddRootComponent(RootComponentOperation<ServerComponentMarker> operation)
{
if (operation.SelectorId is not { } selectorId)
{
Log.InvalidRootComponentOperation(_logger, operation.Type, message: "Missing selector ID.");
return;
}

if (!_serverComponentDeserializer.TryDeserializeSingleComponentDescriptor(operation.Marker, out var descriptor))
{
throw new InvalidOperationException("Failed to deserialize a component descriptor when adding a new root component.");
}

_ = AddComponentAsync(descriptor.ComponentType, descriptor.Parameters, selectorId.ToString(CultureInfo.InvariantCulture));
}

void UpdateRootComponent(RootComponentOperation<ServerComponentMarker> operation)
{
if (operation.ComponentId is not { } componentId)
{
Log.InvalidRootComponentOperation(_logger, operation.Type, message: "Missing component ID.");
return;
}

var componentState = GetComponentState(componentId);

if (!_serverComponentDeserializer.TryDeserializeSingleComponentDescriptor(operation.Marker, out var descriptor))
{
throw new InvalidOperationException("Failed to deserialize a component descriptor when updating an existing root component.");
}

if (descriptor.ComponentType != componentState.Component.GetType())
{
Log.InvalidRootComponentOperation(_logger, operation.Type, message: "Component type mismatch.");
return;
}

_ = RenderRootComponentAsync(componentId, descriptor.Parameters);
}

void RemoveRootComponent(RootComponentOperation<ServerComponentMarker> operation)
{
if (operation.ComponentId is not { } componentId)
{
Log.InvalidRootComponentOperation(_logger, operation.Type, message: "Missing component ID.");
return;
}

this.RemoveRootComponent(componentId);
}
}

protected override void ProcessPendingRender()
{
if (_unacknowledgedRenderBatches.Count >= _options.MaxBufferedUnacknowledgedRenderBatches)
Expand Down Expand Up @@ -388,6 +471,9 @@ public static void CompletingBatchWithoutError(ILogger logger, long batchId, Tim

[LoggerMessage(107, LogLevel.Debug, "The queue of unacknowledged render batches is full.", EventName = "FullUnacknowledgedRenderBatchesQueue")]
public static partial void FullUnacknowledgedRenderBatchesQueue(ILogger logger);

[LoggerMessage(108, LogLevel.Debug, "The root component operation of type '{OperationType}' was invalid: {Message}", EventName = "InvalidRootComponentOperation")]
public static partial void InvalidRootComponentOperation(ILogger logger, RootComponentOperationType operationType, string message);
}
}

Expand Down
Loading