Skip to content

Commit 8729336

Browse files
committed
[Blazor] Improvements to the interaction between SSR and interactive rendering (#49238)
1 parent 2067158 commit 8729336

File tree

56 files changed

+2301
-328
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

56 files changed

+2301
-328
lines changed

src/Components/Endpoints/src/DependencyInjection/ServerComponentSerializer.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,10 @@ public ServerComponentSerializer(IDataProtectionProvider dataProtectionProvider)
1818
.CreateProtector(ServerComponentSerializationSettings.DataProtectionProviderPurpose)
1919
.ToTimeLimitedDataProtector();
2020

21-
public ServerComponentMarker SerializeInvocation(ServerComponentInvocationSequence invocationId, Type type, ParameterView parameters, bool prerendered)
21+
public ServerComponentMarker SerializeInvocation(ServerComponentInvocationSequence invocationId, Type type, ParameterView parameters, string key, bool prerendered)
2222
{
2323
var (sequence, serverComponent) = CreateSerializedServerComponent(invocationId, type, parameters);
24-
return prerendered ? ServerComponentMarker.Prerendered(sequence, serverComponent) : ServerComponentMarker.NonPrerendered(sequence, serverComponent);
24+
return prerendered ? ServerComponentMarker.Prerendered(sequence, serverComponent, key) : ServerComponentMarker.NonPrerendered(sequence, serverComponent, key);
2525
}
2626

2727
private (int sequence, string payload) CreateSerializedServerComponent(

src/Components/Endpoints/src/DependencyInjection/WebAssemblyComponentSerializer.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ namespace Microsoft.AspNetCore.Components.Endpoints;
88
// See the details of the component serialization protocol in WebAssemblyComponentDeserializer.cs on the Components solution.
99
internal sealed class WebAssemblyComponentSerializer
1010
{
11-
public static WebAssemblyComponentMarker SerializeInvocation(Type type, ParameterView parameters, bool prerendered)
11+
public static WebAssemblyComponentMarker SerializeInvocation(Type type, ParameterView parameters, string? key, bool prerendered)
1212
{
1313
var assembly = type.Assembly.GetName().Name ?? throw new InvalidOperationException("Cannot prerender components from assemblies with a null name");
1414
var typeFullName = type.FullName ?? throw new InvalidOperationException("Cannot prerender component types with a null name");
@@ -19,8 +19,8 @@ public static WebAssemblyComponentMarker SerializeInvocation(Type type, Paramete
1919
var serializedDefinitions = Convert.ToBase64String(JsonSerializer.SerializeToUtf8Bytes(definitions, WebAssemblyComponentSerializationSettings.JsonSerializationOptions));
2020
var serializedValues = Convert.ToBase64String(JsonSerializer.SerializeToUtf8Bytes(values, WebAssemblyComponentSerializationSettings.JsonSerializationOptions));
2121

22-
return prerendered ? WebAssemblyComponentMarker.Prerendered(assembly, typeFullName, serializedDefinitions, serializedValues) :
23-
WebAssemblyComponentMarker.NonPrerendered(assembly, typeFullName, serializedDefinitions, serializedValues);
22+
return prerendered ? WebAssemblyComponentMarker.Prerendered(assembly, typeFullName, serializedDefinitions, serializedValues, key) :
23+
WebAssemblyComponentMarker.NonPrerendered(assembly, typeFullName, serializedDefinitions, serializedValues, key);
2424
}
2525

2626
internal static void AppendPreamble(TextWriter writer, WebAssemblyComponentMarker record)

src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Streaming.cs

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,14 @@ private static void HandleNavigationAfterResponseStarted(TextWriter writer, stri
185185
protected override void WriteComponentHtml(int componentId, TextWriter output)
186186
=> WriteComponentHtml(componentId, output, allowBoundaryMarkers: true);
187187

188-
private void WriteComponentHtml(int componentId, TextWriter output, bool allowBoundaryMarkers)
188+
protected override void RenderChildComponent(TextWriter output, ref RenderTreeFrame componentFrame)
189+
{
190+
var componentId = componentFrame.ComponentId;
191+
var sequenceAndKey = new SequenceAndKey(componentFrame.Sequence, componentFrame.ComponentKey);
192+
WriteComponentHtml(componentId, output, allowBoundaryMarkers: true, sequenceAndKey);
193+
}
194+
195+
private void WriteComponentHtml(int componentId, TextWriter output, bool allowBoundaryMarkers, SequenceAndKey sequenceAndKey = default)
189196
{
190197
_visitedComponentIdsInCurrentStreamingBatch?.Add(componentId);
191198

@@ -198,9 +205,8 @@ private void WriteComponentHtml(int componentId, TextWriter output, bool allowBo
198205
// It may be better to use a custom element like <blazor-component ...>[prerendered]<blazor-component>
199206
// so it's easier for the JS code to react automatically whenever this gets inserted or updated during
200207
// streaming SSR or progressively-enhanced navigation.
201-
202208
var (serverMarker, webAssemblyMarker) = componentState.Component is SSRRenderModeBoundary boundary
203-
? boundary.ToMarkers(_httpContext)
209+
? boundary.ToMarkers(_httpContext, sequenceAndKey.Sequence, sequenceAndKey.Key)
204210
: default;
205211

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

248254
private readonly record struct ComponentIdAndDepth(int ComponentId, int Depth);
255+
private readonly record struct SequenceAndKey(int Sequence, object? Key);
249256
}

src/Components/Endpoints/src/Rendering/SSRRenderModeBoundary.cs

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using System.Collections.Concurrent;
5+
using System.Globalization;
6+
using System.Security.Cryptography;
7+
using System.Text;
48
using Microsoft.AspNetCore.Components.Rendering;
59
using Microsoft.AspNetCore.Components.Web;
610
using Microsoft.AspNetCore.Http;
@@ -14,11 +18,14 @@ namespace Microsoft.AspNetCore.Components.Endpoints;
1418
/// </summary>
1519
internal class SSRRenderModeBoundary : IComponent
1620
{
21+
private static readonly ConcurrentDictionary<Type, string> _componentTypeNameHashCache = new();
22+
1723
private readonly Type _componentType;
1824
private readonly IComponentRenderMode _renderMode;
1925
private readonly bool _prerender;
2026
private RenderHandle _renderHandle;
2127
private IReadOnlyDictionary<string, object?>? _latestParameters;
28+
private string? _markerKey;
2229

2330
public SSRRenderModeBoundary(Type componentType, IComponentRenderMode renderMode)
2431
{
@@ -92,8 +99,12 @@ private void Prerender(RenderTreeBuilder builder)
9299
builder.CloseComponent();
93100
}
94101

95-
public (ServerComponentMarker?, WebAssemblyComponentMarker?) ToMarkers(HttpContext httpContext)
102+
public (ServerComponentMarker?, WebAssemblyComponentMarker?) ToMarkers(HttpContext httpContext, int sequence, object? key)
96103
{
104+
// We expect that the '@key' and sequence number shouldn't change for a given component instance,
105+
// so we lazily compute the marker key once.
106+
_markerKey ??= GenerateMarkerKey(sequence, key);
107+
97108
var parameters = _latestParameters is null
98109
? ParameterView.Empty
99110
: ParameterView.FromDictionary((IDictionary<string, object?>)_latestParameters);
@@ -106,15 +117,41 @@ private void Prerender(RenderTreeBuilder builder)
106117
var serverComponentSerializer = httpContext.RequestServices.GetRequiredService<ServerComponentSerializer>();
107118

108119
var invocationId = EndpointHtmlRenderer.GetOrCreateInvocationId(httpContext);
109-
serverMarker = serverComponentSerializer.SerializeInvocation(invocationId, _componentType, parameters, _prerender);
120+
serverMarker = serverComponentSerializer.SerializeInvocation(invocationId, _componentType, parameters, _markerKey, _prerender);
110121
}
111122

112123
WebAssemblyComponentMarker? webAssemblyMarker = null;
113124
if (_renderMode is WebAssemblyRenderMode or AutoRenderMode)
114125
{
115-
webAssemblyMarker = WebAssemblyComponentSerializer.SerializeInvocation(_componentType, parameters, _prerender);
126+
webAssemblyMarker = WebAssemblyComponentSerializer.SerializeInvocation(_componentType, parameters, _markerKey, _prerender);
116127
}
117128

118129
return (serverMarker, webAssemblyMarker);
119130
}
131+
132+
private string GenerateMarkerKey(int sequence, object? key)
133+
{
134+
var componentTypeNameHash = _componentTypeNameHashCache.GetOrAdd(_componentType, ComputeComponentTypeNameHash);
135+
return $"{componentTypeNameHash}:{sequence}:{(key as IFormattable)?.ToString(null, CultureInfo.InvariantCulture)}";
136+
}
137+
138+
private static string ComputeComponentTypeNameHash(Type componentType)
139+
{
140+
if (componentType.FullName is not { } typeName)
141+
{
142+
throw new InvalidOperationException($"An invalid component type was used in {nameof(SSRRenderModeBoundary)}.");
143+
}
144+
145+
var typeNameLength = typeName.Length;
146+
var typeNameBytes = typeNameLength < 1024
147+
? stackalloc byte[typeNameLength]
148+
: new byte[typeNameLength];
149+
150+
Encoding.UTF8.GetBytes(typeName, typeNameBytes);
151+
152+
Span<byte> typeNameHashBytes = stackalloc byte[SHA1.HashSizeInBytes];
153+
SHA1.HashData(typeNameBytes, typeNameHashBytes);
154+
155+
return Convert.ToHexString(typeNameHashBytes);
156+
}
120157
}

src/Components/Server/src/Circuits/CircuitFactory.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,12 +64,14 @@ public async ValueTask<CircuitHost> CreateCircuitHostAsync(
6464
var appLifetime = scope.ServiceProvider.GetRequiredService<ComponentStatePersistenceManager>();
6565
await appLifetime.RestoreStateAsync(store);
6666

67+
var serverComponentDeserializer = scope.ServiceProvider.GetRequiredService<IServerComponentDeserializer>();
6768
var jsComponentInterop = new CircuitJSComponentInterop(_options);
6869
var renderer = new RemoteRenderer(
6970
scope.ServiceProvider,
7071
_loggerFactory,
7172
_options,
7273
client,
74+
serverComponentDeserializer,
7375
_loggerFactory.CreateLogger<RemoteRenderer>(),
7476
jsRuntime,
7577
jsComponentInterop);
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using System.Diagnostics.CodeAnalysis;
5+
46
namespace Microsoft.AspNetCore.Components.Server;
57

68
internal interface IServerComponentDeserializer
79
{
810
bool TryDeserializeComponentDescriptorCollection(
911
string serializedComponentRecords,
1012
out List<ComponentDescriptor> descriptors);
13+
14+
bool TryDeserializeSingleComponentDescriptor(ServerComponentMarker record, [NotNullWhen(true)] out ComponentDescriptor? result);
1115
}

src/Components/Server/src/Circuits/RemoteRenderer.cs

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33

44
using System.Collections.Concurrent;
55
using System.Diagnostics.CodeAnalysis;
6+
using System.Globalization;
67
using System.Linq;
8+
using System.Text.Json;
79
using Microsoft.AspNetCore.Components.RenderTree;
810
using Microsoft.AspNetCore.Components.Web;
911
using Microsoft.AspNetCore.SignalR;
@@ -21,6 +23,7 @@ internal partial class RemoteRenderer : WebRenderer
2123

2224
private readonly CircuitClientProxy _client;
2325
private readonly CircuitOptions _options;
26+
private readonly IServerComponentDeserializer _serverComponentDeserializer;
2427
private readonly ILogger _logger;
2528
internal readonly ConcurrentQueue<UnacknowledgedRenderBatch> _unacknowledgedRenderBatches = new ConcurrentQueue<UnacknowledgedRenderBatch>();
2629
private long _nextRenderId = 1;
@@ -39,13 +42,15 @@ public RemoteRenderer(
3942
ILoggerFactory loggerFactory,
4043
CircuitOptions options,
4144
CircuitClientProxy client,
45+
IServerComponentDeserializer serverComponentDeserializer,
4246
ILogger logger,
4347
RemoteJSRuntime jsRuntime,
4448
CircuitJSComponentInterop jsComponentInterop)
4549
: base(serviceProvider, loggerFactory, jsRuntime.ReadJsonSerializerOptions(), jsComponentInterop)
4650
{
4751
_client = client;
4852
_options = options;
53+
_serverComponentDeserializer = serverComponentDeserializer;
4954
_logger = logger;
5055

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

67+
protected override int GetWebRendererId() => (int)WebRendererId.Server;
68+
6269
protected override void AttachRootComponentToBrowser(int componentId, string domElementSelector)
6370
{
6471
var attachComponentTask = _client.SendAsync("JS.AttachComponent", componentId, domElementSelector);
6572
_ = CaptureAsyncExceptions(attachComponentTask);
6673
}
6774

75+
protected override void UpdateRootComponents(string operationsJson)
76+
{
77+
var operations = JsonSerializer.Deserialize<IEnumerable<RootComponentOperation<ServerComponentMarker>>>(
78+
operationsJson,
79+
ServerComponentSerializationSettings.JsonSerializationOptions);
80+
81+
foreach (var operation in operations)
82+
{
83+
switch (operation.Type)
84+
{
85+
case RootComponentOperationType.Add:
86+
AddRootComponent(operation);
87+
break;
88+
case RootComponentOperationType.Update:
89+
UpdateRootComponent(operation);
90+
break;
91+
case RootComponentOperationType.Remove:
92+
RemoveRootComponent(operation);
93+
break;
94+
}
95+
}
96+
97+
return;
98+
99+
void AddRootComponent(RootComponentOperation<ServerComponentMarker> operation)
100+
{
101+
if (operation.SelectorId is not { } selectorId)
102+
{
103+
Log.InvalidRootComponentOperation(_logger, operation.Type, message: "Missing selector ID.");
104+
return;
105+
}
106+
107+
if (!_serverComponentDeserializer.TryDeserializeSingleComponentDescriptor(operation.Marker, out var descriptor))
108+
{
109+
throw new InvalidOperationException("Failed to deserialize a component descriptor when adding a new root component.");
110+
}
111+
112+
_ = AddComponentAsync(descriptor.ComponentType, descriptor.Parameters, selectorId.ToString(CultureInfo.InvariantCulture));
113+
}
114+
115+
void UpdateRootComponent(RootComponentOperation<ServerComponentMarker> operation)
116+
{
117+
if (operation.ComponentId is not { } componentId)
118+
{
119+
Log.InvalidRootComponentOperation(_logger, operation.Type, message: "Missing component ID.");
120+
return;
121+
}
122+
123+
var componentState = GetComponentState(componentId);
124+
125+
if (!_serverComponentDeserializer.TryDeserializeSingleComponentDescriptor(operation.Marker, out var descriptor))
126+
{
127+
throw new InvalidOperationException("Failed to deserialize a component descriptor when updating an existing root component.");
128+
}
129+
130+
if (descriptor.ComponentType != componentState.Component.GetType())
131+
{
132+
Log.InvalidRootComponentOperation(_logger, operation.Type, message: "Component type mismatch.");
133+
return;
134+
}
135+
136+
_ = RenderRootComponentAsync(componentId, descriptor.Parameters);
137+
}
138+
139+
void RemoveRootComponent(RootComponentOperation<ServerComponentMarker> operation)
140+
{
141+
if (operation.ComponentId is not { } componentId)
142+
{
143+
Log.InvalidRootComponentOperation(_logger, operation.Type, message: "Missing component ID.");
144+
return;
145+
}
146+
147+
this.RemoveRootComponent(componentId);
148+
}
149+
}
150+
68151
protected override void ProcessPendingRender()
69152
{
70153
if (_unacknowledgedRenderBatches.Count >= _options.MaxBufferedUnacknowledgedRenderBatches)
@@ -388,6 +471,9 @@ public static void CompletingBatchWithoutError(ILogger logger, long batchId, Tim
388471

389472
[LoggerMessage(107, LogLevel.Debug, "The queue of unacknowledged render batches is full.", EventName = "FullUnacknowledgedRenderBatchesQueue")]
390473
public static partial void FullUnacknowledgedRenderBatchesQueue(ILogger logger);
474+
475+
[LoggerMessage(108, LogLevel.Debug, "The root component operation of type '{OperationType}' was invalid: {Message}", EventName = "InvalidRootComponentOperation")]
476+
public static partial void InvalidRootComponentOperation(ILogger logger, RootComponentOperationType operationType, string message);
391477
}
392478
}
393479

0 commit comments

Comments
 (0)