Skip to content

Commit a19c70e

Browse files
[Blazor] Dispatch inbound activity handlers in Blazor Web apps (#53185)
1 parent 8dd1b6d commit a19c70e

File tree

4 files changed

+95
-39
lines changed

4 files changed

+95
-39
lines changed

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

Lines changed: 57 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ internal partial class CircuitHost : IAsyncDisposable
2222
private readonly CircuitOptions _options;
2323
private readonly RemoteNavigationManager _navigationManager;
2424
private readonly ILogger _logger;
25-
private readonly Func<Func<Task>, Task> _dispatchInboundActivity;
25+
private Func<Func<Task>, Task> _dispatchInboundActivity;
2626
private CircuitHandler[] _circuitHandlers;
2727
private bool _initialized;
2828
private bool _isFirstUpdate = true;
@@ -734,11 +734,10 @@ internal Task UpdateRootComponents(
734734

735735
return Renderer.Dispatcher.InvokeAsync(async () =>
736736
{
737-
var webRootComponentManager = Renderer.GetOrCreateWebRootComponentManager();
738737
var shouldClearStore = false;
738+
var shouldWaitForQuiescence = false;
739739
var operations = operationBatch.Operations;
740740
var batchId = operationBatch.BatchId;
741-
Task[]? pendingTasks = null;
742741
try
743742
{
744743
if (Descriptors.Count > 0)
@@ -751,6 +750,7 @@ internal Task UpdateRootComponents(
751750
if (_isFirstUpdate)
752751
{
753752
_isFirstUpdate = false;
753+
shouldWaitForQuiescence = true;
754754
if (store != null)
755755
{
756756
shouldClearStore = true;
@@ -762,6 +762,7 @@ internal Task UpdateRootComponents(
762762

763763
// Retrieve the circuit handlers at this point.
764764
_circuitHandlers = [.. _scope.ServiceProvider.GetServices<CircuitHandler>().OrderBy(h => h.Order)];
765+
_dispatchInboundActivity = BuildInboundActivityDispatcher(_circuitHandlers, Circuit);
765766
await OnCircuitOpenedAsync(cancellation);
766767
await OnConnectionUpAsync(cancellation);
767768

@@ -773,44 +774,9 @@ internal Task UpdateRootComponents(
773774
throw new InvalidOperationException($"The first set of update operations must always be of type {nameof(RootComponentOperationType.Add)}");
774775
}
775776
}
776-
777-
pendingTasks = new Task[operations.Length];
778-
}
779-
780-
for (var i = 0; i < operations.Length; i++)
781-
{
782-
var operation = operations[i];
783-
switch (operation.Type)
784-
{
785-
case RootComponentOperationType.Add:
786-
var task = webRootComponentManager.AddRootComponentAsync(
787-
operation.SsrComponentId,
788-
operation.Descriptor.ComponentType,
789-
operation.Marker.Value.Key,
790-
operation.Descriptor.Parameters);
791-
if (pendingTasks != null)
792-
{
793-
pendingTasks[i] = task;
794-
}
795-
break;
796-
case RootComponentOperationType.Update:
797-
// We don't need to await component updates as any unhandled exception will be reported and terminate the circuit.
798-
_ = webRootComponentManager.UpdateRootComponentAsync(
799-
operation.SsrComponentId,
800-
operation.Descriptor.ComponentType,
801-
operation.Marker.Value.Key,
802-
operation.Descriptor.Parameters);
803-
break;
804-
case RootComponentOperationType.Remove:
805-
webRootComponentManager.RemoveRootComponent(operation.SsrComponentId);
806-
break;
807-
}
808777
}
809778

810-
if (pendingTasks != null)
811-
{
812-
await Task.WhenAll(pendingTasks);
813-
}
779+
await PerformRootComponentOperations(operations, shouldWaitForQuiescence);
814780

815781
await Client.SendAsync("JS.EndUpdateRootComponents", batchId);
816782

@@ -836,6 +802,58 @@ internal Task UpdateRootComponents(
836802
});
837803
}
838804

805+
private async ValueTask PerformRootComponentOperations(
806+
RootComponentOperation[] operations,
807+
bool shouldWaitForQuiescence)
808+
{
809+
var webRootComponentManager = Renderer.GetOrCreateWebRootComponentManager();
810+
var pendingTasks = shouldWaitForQuiescence
811+
? new Task[operations.Length]
812+
: null;
813+
814+
// The inbound activity pipeline needs to be awaited because it populates
815+
// the pending tasks used to wait for quiescence.
816+
await HandleInboundActivityAsync(() =>
817+
{
818+
for (var i = 0; i < operations.Length; i++)
819+
{
820+
var operation = operations[i];
821+
switch (operation.Type)
822+
{
823+
case RootComponentOperationType.Add:
824+
var task = webRootComponentManager.AddRootComponentAsync(
825+
operation.SsrComponentId,
826+
operation.Descriptor.ComponentType,
827+
operation.Marker.Value.Key,
828+
operation.Descriptor.Parameters);
829+
if (pendingTasks != null)
830+
{
831+
pendingTasks[i] = task;
832+
}
833+
break;
834+
case RootComponentOperationType.Update:
835+
// We don't need to await component updates as any unhandled exception will be reported and terminate the circuit.
836+
_ = webRootComponentManager.UpdateRootComponentAsync(
837+
operation.SsrComponentId,
838+
operation.Descriptor.ComponentType,
839+
operation.Marker.Value.Key,
840+
operation.Descriptor.Parameters);
841+
break;
842+
case RootComponentOperationType.Remove:
843+
webRootComponentManager.RemoveRootComponent(operation.SsrComponentId);
844+
break;
845+
}
846+
}
847+
848+
return Task.CompletedTask;
849+
});
850+
851+
if (pendingTasks != null)
852+
{
853+
await Task.WhenAll(pendingTasks);
854+
}
855+
}
856+
839857
private static partial class Log
840858
{
841859
// 100s used for lifecycle stuff

src/Components/test/E2ETest/ServerRenderingTests/InteractivityTest.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1138,6 +1138,14 @@ public void NavigationManagerCanRefreshSSRPageWhenServerInteractivityEnabled()
11381138
Browser.Equal("GET", () => Browser.Exists(By.Id("method")).Text);
11391139
}
11401140

1141+
[Fact]
1142+
public void InteractiveServerRootComponent_CanAccessCircuitContext()
1143+
{
1144+
Navigate($"{ServerPathBase}/interactivity/circuit-context");
1145+
1146+
Browser.Equal("True", () => Browser.FindElement(By.Id("has-circuit-context")).Text);
1147+
}
1148+
11411149
private void BlockWebAssemblyResourceLoad()
11421150
{
11431151
// Force a WebAssembly resource cache miss so that we can fall back to using server interactivity

src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsStartup.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
using Components.TestServer.RazorComponents;
99
using Components.TestServer.RazorComponents.Pages.Forms;
1010
using Components.TestServer.Services;
11+
using Microsoft.AspNetCore.Components.Server.Circuits;
1112
using Microsoft.AspNetCore.Mvc;
1213

1314
namespace TestServer;
@@ -35,6 +36,10 @@ public void ConfigureServices(IServiceCollection services)
3536
services.AddHttpContextAccessor();
3637
services.AddSingleton<AsyncOperationService>();
3738
services.AddCascadingAuthenticationState();
39+
40+
var circuitContextAccessor = new TestCircuitContextAccessor();
41+
services.AddSingleton<CircuitHandler>(circuitContextAccessor);
42+
services.AddSingleton(circuitContextAccessor);
3843
}
3944

4045
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
@page "/interactivity/circuit-context"
2+
@rendermode RenderMode.InteractiveServer
3+
@inject TestCircuitContextAccessor CircuitContextAccessor
4+
5+
<h1>Circuit context</h1>
6+
7+
<p>
8+
Has circuit context: <span id="has-circuit-context">@_hasCircuitContext</span>
9+
</p>
10+
11+
@code {
12+
private bool _hasCircuitContext;
13+
14+
protected override async Task OnAfterRenderAsync(bool firstRender)
15+
{
16+
if (firstRender)
17+
{
18+
await Task.Yield();
19+
20+
_hasCircuitContext = CircuitContextAccessor.HasCircuitContext;
21+
22+
StateHasChanged();
23+
}
24+
}
25+
}

0 commit comments

Comments
 (0)