diff --git a/src/Components/Server/src/Circuits/CircuitHost.cs b/src/Components/Server/src/Circuits/CircuitHost.cs index 84649d0f7a9e..7acea8c63fa3 100644 --- a/src/Components/Server/src/Circuits/CircuitHost.cs +++ b/src/Components/Server/src/Circuits/CircuitHost.cs @@ -22,7 +22,7 @@ internal partial class CircuitHost : IAsyncDisposable private readonly CircuitOptions _options; private readonly RemoteNavigationManager _navigationManager; private readonly ILogger _logger; - private readonly Func, Task> _dispatchInboundActivity; + private Func, Task> _dispatchInboundActivity; private CircuitHandler[] _circuitHandlers; private bool _initialized; private bool _isFirstUpdate = true; @@ -734,11 +734,10 @@ internal Task UpdateRootComponents( return Renderer.Dispatcher.InvokeAsync(async () => { - var webRootComponentManager = Renderer.GetOrCreateWebRootComponentManager(); var shouldClearStore = false; + var shouldWaitForQuiescence = false; var operations = operationBatch.Operations; var batchId = operationBatch.BatchId; - Task[]? pendingTasks = null; try { if (Descriptors.Count > 0) @@ -751,6 +750,7 @@ internal Task UpdateRootComponents( if (_isFirstUpdate) { _isFirstUpdate = false; + shouldWaitForQuiescence = true; if (store != null) { shouldClearStore = true; @@ -762,6 +762,7 @@ internal Task UpdateRootComponents( // Retrieve the circuit handlers at this point. _circuitHandlers = [.. _scope.ServiceProvider.GetServices().OrderBy(h => h.Order)]; + _dispatchInboundActivity = BuildInboundActivityDispatcher(_circuitHandlers, Circuit); await OnCircuitOpenedAsync(cancellation); await OnConnectionUpAsync(cancellation); @@ -773,44 +774,9 @@ internal Task UpdateRootComponents( throw new InvalidOperationException($"The first set of update operations must always be of type {nameof(RootComponentOperationType.Add)}"); } } - - pendingTasks = new Task[operations.Length]; - } - - for (var i = 0; i < operations.Length; i++) - { - var operation = operations[i]; - switch (operation.Type) - { - case RootComponentOperationType.Add: - var task = webRootComponentManager.AddRootComponentAsync( - operation.SsrComponentId, - operation.Descriptor.ComponentType, - operation.Marker.Value.Key, - operation.Descriptor.Parameters); - if (pendingTasks != null) - { - pendingTasks[i] = task; - } - break; - case RootComponentOperationType.Update: - // We don't need to await component updates as any unhandled exception will be reported and terminate the circuit. - _ = webRootComponentManager.UpdateRootComponentAsync( - operation.SsrComponentId, - operation.Descriptor.ComponentType, - operation.Marker.Value.Key, - operation.Descriptor.Parameters); - break; - case RootComponentOperationType.Remove: - webRootComponentManager.RemoveRootComponent(operation.SsrComponentId); - break; - } } - if (pendingTasks != null) - { - await Task.WhenAll(pendingTasks); - } + await PerformRootComponentOperations(operations, shouldWaitForQuiescence); await Client.SendAsync("JS.EndUpdateRootComponents", batchId); @@ -836,6 +802,58 @@ internal Task UpdateRootComponents( }); } + private async ValueTask PerformRootComponentOperations( + RootComponentOperation[] operations, + bool shouldWaitForQuiescence) + { + var webRootComponentManager = Renderer.GetOrCreateWebRootComponentManager(); + var pendingTasks = shouldWaitForQuiescence + ? new Task[operations.Length] + : null; + + // The inbound activity pipeline needs to be awaited because it populates + // the pending tasks used to wait for quiescence. + await HandleInboundActivityAsync(() => + { + for (var i = 0; i < operations.Length; i++) + { + var operation = operations[i]; + switch (operation.Type) + { + case RootComponentOperationType.Add: + var task = webRootComponentManager.AddRootComponentAsync( + operation.SsrComponentId, + operation.Descriptor.ComponentType, + operation.Marker.Value.Key, + operation.Descriptor.Parameters); + if (pendingTasks != null) + { + pendingTasks[i] = task; + } + break; + case RootComponentOperationType.Update: + // We don't need to await component updates as any unhandled exception will be reported and terminate the circuit. + _ = webRootComponentManager.UpdateRootComponentAsync( + operation.SsrComponentId, + operation.Descriptor.ComponentType, + operation.Marker.Value.Key, + operation.Descriptor.Parameters); + break; + case RootComponentOperationType.Remove: + webRootComponentManager.RemoveRootComponent(operation.SsrComponentId); + break; + } + } + + return Task.CompletedTask; + }); + + if (pendingTasks != null) + { + await Task.WhenAll(pendingTasks); + } + } + private static partial class Log { // 100s used for lifecycle stuff diff --git a/src/Components/test/E2ETest/ServerRenderingTests/InteractivityTest.cs b/src/Components/test/E2ETest/ServerRenderingTests/InteractivityTest.cs index d16ae8cf226b..cf894004da30 100644 --- a/src/Components/test/E2ETest/ServerRenderingTests/InteractivityTest.cs +++ b/src/Components/test/E2ETest/ServerRenderingTests/InteractivityTest.cs @@ -1133,6 +1133,14 @@ public void NavigationManagerCanRefreshSSRPageWhenServerInteractivityEnabled() Browser.Equal("GET", () => Browser.Exists(By.Id("method")).Text); } + [Fact] + public void InteractiveServerRootComponent_CanAccessCircuitContext() + { + Navigate($"{ServerPathBase}/interactivity/circuit-context"); + + Browser.Equal("True", () => Browser.FindElement(By.Id("has-circuit-context")).Text); + } + private void BlockWebAssemblyResourceLoad() { ((IJavaScriptExecutor)Browser).ExecuteScript("sessionStorage.setItem('block-load-boot-resource', 'true')"); diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsStartup.cs b/src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsStartup.cs index dead36b67e18..be68ef2ffd7e 100644 --- a/src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsStartup.cs +++ b/src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsStartup.cs @@ -8,6 +8,7 @@ using Components.TestServer.RazorComponents; using Components.TestServer.RazorComponents.Pages.Forms; using Components.TestServer.Services; +using Microsoft.AspNetCore.Components.Server.Circuits; using Microsoft.AspNetCore.Mvc; namespace TestServer; @@ -35,6 +36,10 @@ public void ConfigureServices(IServiceCollection services) services.AddHttpContextAccessor(); services.AddSingleton(); services.AddCascadingAuthenticationState(); + + var circuitContextAccessor = new TestCircuitContextAccessor(); + services.AddSingleton(circuitContextAccessor); + services.AddSingleton(circuitContextAccessor); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/Interactivity/CircuitContextPage.razor b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/Interactivity/CircuitContextPage.razor new file mode 100644 index 000000000000..801358ec69f1 --- /dev/null +++ b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/Interactivity/CircuitContextPage.razor @@ -0,0 +1,25 @@ +@page "/interactivity/circuit-context" +@rendermode RenderMode.InteractiveServer +@inject TestCircuitContextAccessor CircuitContextAccessor + +

Circuit context

+ +

+ Has circuit context: @_hasCircuitContext +

+ +@code { + private bool _hasCircuitContext; + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + { + await Task.Yield(); + + _hasCircuitContext = CircuitContextAccessor.HasCircuitContext; + + StateHasChanged(); + } + } +}