Skip to content

Commit d4b8ba6

Browse files
[Blazor] Dispatch inbound activity handlers in Blazor Web apps (#53185) (#53313)
# Dispatch inbound activity handlers in Blazor Web apps Fixes an issue where inbound activity handlers were not getting invoked in Blazor Web apps. ## Description Inbound activity handlers were a new feature introduced in .NET 8 to allow Blazor Server apps to monitor circuit activity and make services available to Blazor components that wouldn't otherwise be accessible. However, we also introduced a new "Blazor Web" app model that didn't incorporate this new feature correctly. This PR fixes the issue by: * Building the inbound activity pipeline in Blazor Web apps just after circuit handlers are retrieved * Invoking inbound activity handlers in web root component updates Fixes #51934 ## Customer Impact Multiple customers have indicated being affected by this issue. Since this is a new feature in .NET 8, customers expect it to work in Blazor Web apps, which we recommend for new projects. Without this fix, the functionality only works in traditional Blazor Server apps. ## Regression? - [ ] Yes - [X] No This is a new feature in .NET 8. ## Risk - [ ] High - [ ] Medium - [X] Low The fix is simple and applies to a well-tested area of the framework, so our E2E tests are likely to catch issues with this change. ## Verification - [X] Manual (required) - [X] Automated ## Packaging changes reviewed? - [ ] Yes - [ ] No - [X] N/A
1 parent 303c6f9 commit d4b8ba6

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;
@@ -735,11 +735,10 @@ internal Task UpdateRootComponents(
735735

736736
return Renderer.Dispatcher.InvokeAsync(async () =>
737737
{
738-
var webRootComponentManager = Renderer.GetOrCreateWebRootComponentManager();
739738
var shouldClearStore = false;
739+
var shouldWaitForQuiescence = false;
740740
var operations = operationBatch.Operations;
741741
var batchId = operationBatch.BatchId;
742-
Task[]? pendingTasks = null;
743742
try
744743
{
745744
if (Descriptors.Count > 0)
@@ -752,6 +751,7 @@ internal Task UpdateRootComponents(
752751
if (_isFirstUpdate)
753752
{
754753
_isFirstUpdate = false;
754+
shouldWaitForQuiescence = true;
755755
if (store != null)
756756
{
757757
shouldClearStore = true;
@@ -763,6 +763,7 @@ internal Task UpdateRootComponents(
763763

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

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

811-
if (pendingTasks != null)
812-
{
813-
await Task.WhenAll(pendingTasks);
814-
}
780+
await PerformRootComponentOperations(operations, shouldWaitForQuiescence);
815781

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

@@ -837,6 +803,58 @@ internal Task UpdateRootComponents(
837803
});
838804
}
839805

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

1162+
[Fact]
1163+
public void InteractiveServerRootComponent_CanAccessCircuitContext()
1164+
{
1165+
Navigate($"{ServerPathBase}/interactivity/circuit-context");
1166+
1167+
Browser.Equal("True", () => Browser.FindElement(By.Id("has-circuit-context")).Text);
1168+
}
1169+
11621170
private void BlockWebAssemblyResourceLoad()
11631171
{
11641172
((IJavaScriptExecutor)Browser).ExecuteScript("sessionStorage.setItem('block-load-boot-resource', 'true')");

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)