Skip to content

Commit c8115ed

Browse files
[release/8.0] [Blazor] Defer enhanced component updates until reconnection circuit completes (#51242)
# Defer enhanced component updates until reconnection circuit completes Ensures that enhanced updates to interactive server components get deferred until the circuit is reconnected. ## Description This PR includes two changes: 1. Takes circuit "connected-ness" into account when determining if enhanced component updates can be applied 2. Waits until any given root component's disposal gets acknowledged by .NET before the browser stops tracking it for SSR updates Fixes #51082 ## Customer Impact This change impacts Blazor Web scenarios where interactive server components exist on the page. With this fix, it's more likely that a customer with an unstable connection will be able to continue using the app without refreshing the page. In particular: 1. If a developer creates a custom, less intrusive reconnection dialog that allows the user to keep interacting with the app, a user-induced page navigation during reconnection won't bring the app to an unusable state 2. If the user quickly navigates between pages while the circuit is unstable, there won't be console errors caused by attempting to interactively update a component that was removed from the page ## Regression? - [ ] Yes - [X] No These fixes only affect "Blazor Web" scenarios, which are new in .NET 8. ## Risk - [ ] High - [X] Medium - [X] Low This is a moderately-sized change. However, we have existing automated tests that verify the scenarios that this code is likely to impact, and we've added new tests to help ensure that these fixes introduce the above stability improvements. ## Verification - [X] Manual (required) - [X] Automated ## Packaging changes reviewed? - [ ] Yes - [ ] No - [X] N/A
1 parent 7ffeb43 commit c8115ed

28 files changed

+358
-151
lines changed

src/Components/Server/src/Circuits/CircuitHost.cs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -726,7 +726,7 @@ private async Task TryNotifyClientErrorAsync(IClientProxy client, string error,
726726
}
727727

728728
internal Task UpdateRootComponents(
729-
CircuitRootComponentOperation[] operations,
729+
RootComponentOperationBatch operationBatch,
730730
ProtectedPrerenderComponentApplicationStore store,
731731
IServerComponentDeserializer serverComponentDeserializer,
732732
CancellationToken cancellation)
@@ -737,6 +737,8 @@ internal Task UpdateRootComponents(
737737
{
738738
var webRootComponentManager = Renderer.GetOrCreateWebRootComponentManager();
739739
var shouldClearStore = false;
740+
var operations = operationBatch.Operations;
741+
var batchId = operationBatch.BatchId;
740742
Task[]? pendingTasks = null;
741743
try
742744
{
@@ -785,7 +787,7 @@ internal Task UpdateRootComponents(
785787
var task = webRootComponentManager.AddRootComponentAsync(
786788
operation.SsrComponentId,
787789
operation.Descriptor.ComponentType,
788-
operation.Descriptor.Key,
790+
operation.Marker.Value.Key,
789791
operation.Descriptor.Parameters);
790792
if (pendingTasks != null)
791793
{
@@ -797,7 +799,7 @@ internal Task UpdateRootComponents(
797799
_ = webRootComponentManager.UpdateRootComponentAsync(
798800
operation.SsrComponentId,
799801
operation.Descriptor.ComponentType,
800-
operation.Descriptor.Key,
802+
operation.Marker.Value.Key,
801803
operation.Descriptor.Parameters);
802804
break;
803805
case RootComponentOperationType.Remove:
@@ -811,6 +813,8 @@ internal Task UpdateRootComponents(
811813
await Task.WhenAll(pendingTasks);
812814
}
813815

816+
await Client.SendAsync("JS.EndUpdateRootComponents", batchId);
817+
814818
Log.UpdateRootComponentsSucceeded(_logger);
815819
}
816820
catch (Exception ex)

src/Components/Server/src/Circuits/CircuitRootComponentOperation.cs

Lines changed: 0 additions & 13 deletions
This file was deleted.

src/Components/Server/src/Circuits/IServerComponentDeserializer.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,5 @@ internal interface IServerComponentDeserializer
1010
bool TryDeserializeComponentDescriptorCollection(
1111
string serializedComponentRecords,
1212
out List<ComponentDescriptor> descriptors);
13-
bool TryDeserializeRootComponentOperations(string serializedComponentOperations, [NotNullWhen(true)] out CircuitRootComponentOperation[]? operationsWithDescriptors);
13+
bool TryDeserializeRootComponentOperations(string serializedComponentOperations, [NotNullWhen(true)] out RootComponentOperationBatch? operationBatch);
1414
}

src/Components/Server/src/Circuits/ServerComponentDeserializer.cs

Lines changed: 23 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,12 @@ public bool TryDeserializeWebRootComponentDescriptor(ComponentMarker record, [No
151151
return false;
152152
}
153153

154+
if (record.Key != serverComponent.Key)
155+
{
156+
Log.InvalidRootComponentKey(_logger);
157+
return false;
158+
}
159+
154160
// We're seeing a different invocation ID than we did the last time we processed a component.
155161
// There are two possibilities:
156162
// [1] A new invocation has started, in which case we should stop accepting components from the previous one.
@@ -193,7 +199,7 @@ public bool TryDeserializeWebRootComponentDescriptor(ComponentMarker record, [No
193199
serverComponent.ParameterDefinitions.AsReadOnly(),
194200
serverComponent.ParameterValues.AsReadOnly());
195201

196-
result = new(componentType, serverComponent.Key, webRootComponentParameters);
202+
result = new(componentType, webRootComponentParameters);
197203
return true;
198204
}
199205

@@ -285,61 +291,59 @@ private bool TryDeserializeServerComponent(ComponentMarker record, out ServerCom
285291
return (componentDescriptor, serverComponent);
286292
}
287293

288-
public bool TryDeserializeRootComponentOperations(string serializedComponentOperations, [NotNullWhen(true)] out CircuitRootComponentOperation[]? operations)
294+
public bool TryDeserializeRootComponentOperations(string serializedComponentOperations, [NotNullWhen(true)] out RootComponentOperationBatch? result)
289295
{
290296
int[]? seenComponentIdsStorage = null;
291297
try
292298
{
293-
var result = JsonSerializer.Deserialize<RootComponentOperation[]>(
299+
result = JsonSerializer.Deserialize<RootComponentOperationBatch>(
294300
serializedComponentOperations,
295301
ServerComponentSerializationSettings.JsonSerializationOptions);
302+
var operations = result.Operations;
296303

297-
operations = new CircuitRootComponentOperation[result.Length];
298-
299-
Span<int> seenSsrComponentIds = result.Length <= 128
300-
? stackalloc int[result.Length]
301-
: (seenComponentIdsStorage = ArrayPool<int>.Shared.Rent(result.Length)).AsSpan(0, result.Length);
304+
Span<int> seenSsrComponentIds = operations.Length <= 128
305+
? stackalloc int[operations.Length]
306+
: (seenComponentIdsStorage = ArrayPool<int>.Shared.Rent(operations.Length)).AsSpan(0, operations.Length);
302307
var currentSsrComponentIdIndex = 0;
303-
for (var i = 0; i < result.Length; i++)
308+
for (var i = 0; i < operations.Length; i++)
304309
{
305-
var operation = result[i];
310+
var operation = operations[i];
306311
if (seenSsrComponentIds[0..currentSsrComponentIdIndex].Contains(operation.SsrComponentId))
307312
{
308313
Log.InvalidRootComponentOperation(_logger, operation.Type, message: "Duplicate component ID.");
309-
operations = null;
314+
result = null;
310315
return false;
311316
}
312317

313318
seenSsrComponentIds[currentSsrComponentIdIndex++] = operation.SsrComponentId;
314319

315320
if (operation.Type == RootComponentOperationType.Remove)
316321
{
317-
operations[i] = new(operation, null);
318322
continue;
319323
}
320324

321325
if (operation.Marker == null)
322326
{
323327
Log.InvalidRootComponentOperation(_logger, operation.Type, message: "Missing marker.");
324-
operations = null;
328+
result = null;
325329
return false;
326330
}
327331

328332
if (!TryDeserializeWebRootComponentDescriptor(operation.Marker.Value, out var descriptor))
329333
{
330-
operations = null;
334+
result = null;
331335
return false;
332336
}
333337

334-
operations[i] = new(operation, descriptor);
338+
operation.Descriptor = descriptor;
335339
}
336340

337341
return true;
338342
}
339343
catch (Exception ex)
340344
{
341345
Log.FailedToProcessRootComponentOperations(_logger, ex);
342-
operations = null;
346+
result = null;
343347
return false;
344348
}
345349
finally
@@ -388,5 +392,8 @@ private static partial class Log
388392

389393
[LoggerMessage(12, LogLevel.Debug, "Failed to parse root component operations", EventName = nameof(FailedToProcessRootComponentOperations))]
390394
public static partial void FailedToProcessRootComponentOperations(ILogger logger, Exception exception);
395+
396+
[LoggerMessage(13, LogLevel.Debug, "The provided root component key was not valid.", EventName = nameof(InvalidRootComponentKey))]
397+
public static partial void InvalidRootComponentKey(ILogger logger);
391398
}
392399
}

src/Components/Server/src/Circuits/WebRootComponentDescriptor.cs

Lines changed: 0 additions & 16 deletions
This file was deleted.

src/Components/Server/src/Microsoft.AspNetCore.Components.Server.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@
6262
<Compile Include="$(ComponentsSharedSourceRoot)src\ElementReferenceJsonConverter.cs" />
6363
<Compile Include="$(ComponentsSharedSourceRoot)src\ComponentParametersTypeCache.cs" />
6464
<Compile Include="$(ComponentsSharedSourceRoot)src\RootComponentOperation.cs" />
65+
<Compile Include="$(ComponentsSharedSourceRoot)src\RootComponentOperationBatch.cs" />
6566
<Compile Include="$(ComponentsSharedSourceRoot)src\RootComponentOperationType.cs" />
6667
<Compile Include="$(ComponentsSharedSourceRoot)src\RootComponentTypeCache.cs" />
6768
<Compile Include="$(ComponentsSharedSourceRoot)src\DefaultAntiforgeryStateProvider.cs" LinkBase="Forms" />

src/Components/Server/test/Circuits/CircuitHostTest.cs

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33

44
using System.Globalization;
55
using System.Reflection;
6-
using System.Runtime.InteropServices.Marshalling;
76
using System.Text.Json;
87
using Microsoft.AspNetCore.Components.Endpoints;
98
using Microsoft.AspNetCore.Components.Rendering;
@@ -593,15 +592,14 @@ private async Task AddComponentAsync<TComponent>(CircuitHost circuitHost, int ss
593592
Type = RootComponentOperationType.Add,
594593
SsrComponentId = ssrComponentId,
595594
Marker = CreateMarker(typeof(TComponent), ssrComponentId.ToString(CultureInfo.InvariantCulture), parameters, componentKey),
595+
Descriptor = new(
596+
componentType: typeof(TComponent),
597+
parameters: CreateWebRootComponentParameters(parameters)),
596598
};
597-
var addDescriptor = new WebRootComponentDescriptor(
598-
componentType: typeof(TComponent),
599-
key: addOperation.Marker.Value.Key,
600-
parameters: CreateWebRootComponentParameters(parameters));
601599

602600
// Add component
603601
await circuitHost.UpdateRootComponents(
604-
[new(addOperation, addDescriptor)], null, CreateDeserializer(), CancellationToken.None);
602+
new() { Operations = [addOperation] }, null, CreateDeserializer(), CancellationToken.None);
605603
}
606604

607605
private async Task UpdateComponentAsync<TComponent>(CircuitHost circuitHost, int ssrComponentId, Dictionary<string, object> parameters = null, string componentKey = "")
@@ -611,15 +609,14 @@ private async Task UpdateComponentAsync<TComponent>(CircuitHost circuitHost, int
611609
Type = RootComponentOperationType.Update,
612610
SsrComponentId = ssrComponentId,
613611
Marker = CreateMarker(typeof(TComponent), ssrComponentId.ToString(CultureInfo.InvariantCulture), parameters, componentKey),
612+
Descriptor = new(
613+
componentType: typeof(TComponent),
614+
parameters: CreateWebRootComponentParameters(parameters)),
614615
};
615-
var updateDescriptor = new WebRootComponentDescriptor(
616-
componentType: typeof(TComponent),
617-
key: updateOperation.Marker.Value.Key,
618-
parameters: CreateWebRootComponentParameters(parameters));
619616

620617
// Update component
621618
await circuitHost.UpdateRootComponents(
622-
[new(updateOperation, updateDescriptor)], null, CreateDeserializer(), CancellationToken.None);
619+
new() { Operations = [updateOperation] }, null, CreateDeserializer(), CancellationToken.None);
623620
}
624621

625622
private async Task RemoveComponentAsync(CircuitHost circuitHost, int ssrComponentId)
@@ -632,7 +629,7 @@ private async Task RemoveComponentAsync(CircuitHost circuitHost, int ssrComponen
632629

633630
// Remove component
634631
await circuitHost.UpdateRootComponents(
635-
[new(removeOperation, null)], null, CreateDeserializer(), CancellationToken.None);
632+
new() { Operations = [removeOperation] }, null, CreateDeserializer(), CancellationToken.None);
636633
}
637634

638635
private ProtectedPrerenderComponentApplicationStore CreateStore()
@@ -857,7 +854,7 @@ public bool TryDeserializeComponentDescriptorCollection(string serializedCompone
857854
return true;
858855
}
859856

860-
public bool TryDeserializeRootComponentOperations(string serializedComponentOperations, out CircuitRootComponentOperation[] operationBatch)
857+
public bool TryDeserializeRootComponentOperations(string serializedComponentOperations, out RootComponentOperationBatch operationBatch)
861858
{
862859
operationBatch = default;
863860
return true;

src/Components/Server/test/Circuits/ComponentHubTest.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,7 @@ public bool TryDeserializeComponentDescriptorCollection(string serializedCompone
170170
return true;
171171
}
172172

173-
public bool TryDeserializeRootComponentOperations(string serializedComponentOperations, out CircuitRootComponentOperation[] operationsWithDescriptors)
173+
public bool TryDeserializeRootComponentOperations(string serializedComponentOperations, out RootComponentOperationBatch operationsWithDescriptors)
174174
{
175175
operationsWithDescriptors = default;
176176
return true;

src/Components/Server/test/Circuits/ServerComponentDeserializerTest.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -412,14 +412,11 @@ public void UpdateRootComponents_TryDeserializeRootComponentOperationsReturnsFal
412412
Marker = CreateMarker(typeof(DynamicallyAddedComponent)),
413413
};
414414

415-
var operationsJson = JsonSerializer.Serialize(
416-
new[] { operation, other },
417-
ServerComponentSerializationSettings.JsonSerializationOptions);
418-
415+
var batchJson = SerializeRootComponentOperationBatch(new() { Operations = [operation, other] });
419416
var deserializer = CreateServerComponentDeserializer();
420417

421418
// Act
422-
var result = deserializer.TryDeserializeRootComponentOperations(operationsJson, out var parsed);
419+
var result = deserializer.TryDeserializeRootComponentOperations(batchJson, out var parsed);
423420

424421
// Assert
425422
Assert.False(result);
@@ -431,6 +428,9 @@ private string SerializeComponent(string assembly, string type) =>
431428
new ServerComponent(0, null, assembly, type, Array.Empty<ComponentParameter>(), Array.Empty<object>(), Guid.NewGuid()),
432429
ServerComponentSerializationSettings.JsonSerializationOptions);
433430

431+
private string SerializeRootComponentOperationBatch(RootComponentOperationBatch batch)
432+
=> JsonSerializer.Serialize(batch, ServerComponentSerializationSettings.JsonSerializationOptions);
433+
434434
private ServerComponentDeserializer CreateServerComponentDeserializer()
435435
{
436436
return new ServerComponentDeserializer(

src/Components/Shared/src/RootComponentOperation.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
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.Text.Json.Serialization;
5+
46
namespace Microsoft.AspNetCore.Components;
57

68
internal sealed class RootComponentOperation
@@ -13,4 +15,18 @@ internal sealed class RootComponentOperation
1315

1416
// The marker that was initially rendered to the page.
1517
public ComponentMarker? Marker { get; set; }
18+
19+
// Describes additional information about the component.
20+
// This property may get populated by .NET after JSON deserialization.
21+
[JsonIgnore]
22+
public WebRootComponentDescriptor? Descriptor { get; set; }
23+
}
24+
25+
internal sealed class WebRootComponentDescriptor(
26+
Type componentType,
27+
WebRootComponentParameters parameters)
28+
{
29+
public Type ComponentType { get; } = componentType;
30+
31+
public WebRootComponentParameters Parameters { get; } = parameters;
1632
}

0 commit comments

Comments
 (0)