From 293a466d22780a65c8cfcd177e84c5b55cfffa29 Mon Sep 17 00:00:00 2001 From: Ilona Tomkowicz Date: Fri, 21 Nov 2025 12:29:25 +0100 Subject: [PATCH 01/13] First version of fix. Dysnamic JS roots were entering `GetMarkerKey` without initializing the manager, --- .../Server/src/Circuits/RemoteRenderer.cs | 10 +++++++--- .../src/Rendering/WebAssemblyRenderer.cs | 11 ++++++++--- .../E2ETest/Tests/StatePersistenceTest.cs | 13 +++++++++++++ .../persistent-state-js-root-component.html | 16 ++++++++++++++++ .../RazorComponentEndpointsStartup.cs | 2 ++ .../CounterWithPersistentState.razor | 19 +++++++++++++++++++ 6 files changed, 65 insertions(+), 6 deletions(-) create mode 100644 src/Components/test/testassets/BasicTestApp/wwwroot/persistent-state-js-root-component.html create mode 100644 src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/PersistentState/CounterWithPersistentState.razor diff --git a/src/Components/Server/src/Circuits/RemoteRenderer.cs b/src/Components/Server/src/Circuits/RemoteRenderer.cs index 8e161697abdb..654829189bb2 100644 --- a/src/Components/Server/src/Circuits/RemoteRenderer.cs +++ b/src/Components/Server/src/Circuits/RemoteRenderer.cs @@ -321,9 +321,13 @@ protected override ComponentState CreateComponentState(int componentId, ICompone internal ComponentMarkerKey GetMarkerKey(RemoteComponentState remoteComponentState) { - return remoteComponentState.ParentComponentState != null ? - default : - _webRootComponentManager!.GetRootComponentKey(remoteComponentState.ComponentId); + if (remoteComponentState.ParentComponentState != null) + { + return default; + } + + var webRootComponentManager = _webRootComponentManager ?? GetOrCreateWebRootComponentManager(); + return webRootComponentManager.GetRootComponentKey(remoteComponentState.ComponentId); } private void ProcessPendingBatch(string? errorMessageOrNull, UnacknowledgedRenderBatch entry) diff --git a/src/Components/WebAssembly/WebAssembly/src/Rendering/WebAssemblyRenderer.cs b/src/Components/WebAssembly/WebAssembly/src/Rendering/WebAssemblyRenderer.cs index 242e28ed7ba2..f280d7b0d9aa 100644 --- a/src/Components/WebAssembly/WebAssembly/src/Rendering/WebAssemblyRenderer.cs +++ b/src/Components/WebAssembly/WebAssembly/src/Rendering/WebAssemblyRenderer.cs @@ -224,9 +224,14 @@ protected override ComponentState CreateComponentState(int componentId, ICompone internal ComponentMarkerKey GetMarkerKey(WebAssemblyComponentState webAssemblyComponentState) { - return webAssemblyComponentState.ParentComponentState != null ? - default : - _webRootComponentManager!.GetRootComponentKey(webAssemblyComponentState.ComponentId); + if (webAssemblyComponentState.ParentComponentState != null) + { + return default; + } + + var webRootComponentManager = _webRootComponentManager ?? GetOrCreateWebRootComponentManager(); + + return webRootComponentManager.GetRootComponentKey(webAssemblyComponentState.ComponentId); } private static partial class Log diff --git a/src/Components/test/E2ETest/Tests/StatePersistenceTest.cs b/src/Components/test/E2ETest/Tests/StatePersistenceTest.cs index 007c7ce5de16..62bc170cf162 100644 --- a/src/Components/test/E2ETest/Tests/StatePersistenceTest.cs +++ b/src/Components/test/E2ETest/Tests/StatePersistenceTest.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Linq; using Components.TestServer.RazorComponents; using Microsoft.AspNetCore.Components.E2ETest.Infrastructure; using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures; @@ -355,6 +356,18 @@ private void RenderComponentsWithPersistentStateAndValidate( interactiveRuntime: interactiveRuntime); } + [Fact] + public void JsAddedPersistentStateRootComponentDoesNotTriggerCircuitError() + { + Navigate("subdir/persistent-state-js-root-component.html"); + + Browser.Equal("Counter", () => Browser.Exists(By.TagName("h1")).Text); + Browser.Equal("Current count: 0", () => Browser.Exists(By.CssSelector("p[role='status']")).Text); + + Browser.Click(By.CssSelector("button.btn-primary")); + Browser.Equal("Current count: 1", () => Browser.Exists(By.CssSelector("p[role='status']")).Text); + } + private void AssertPageState( string mode, string renderMode, diff --git a/src/Components/test/testassets/BasicTestApp/wwwroot/persistent-state-js-root-component.html b/src/Components/test/testassets/BasicTestApp/wwwroot/persistent-state-js-root-component.html new file mode 100644 index 000000000000..180c4ae21dc1 --- /dev/null +++ b/src/Components/test/testassets/BasicTestApp/wwwroot/persistent-state-js-root-component.html @@ -0,0 +1,16 @@ + + + + + +
+
+ + + + + diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsStartup.cs b/src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsStartup.cs index 861e0fbdf288..1eca1342bb5f 100644 --- a/src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsStartup.cs +++ b/src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsStartup.cs @@ -7,6 +7,7 @@ using System.Web; using Components.TestServer.RazorComponents; using Components.TestServer.RazorComponents.Pages.Forms; +using Components.TestServer.RazorComponents.Pages.PersistentState; using Components.TestServer.Services; using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Server.Circuits; @@ -50,6 +51,7 @@ public void ConfigureServices(IServiceCollection services) options.DisconnectedCircuitMaxRetained = 0; options.DetailedErrors = true; } + options.RootComponents.RegisterForJavaScript("persistent-state-counter"); }) .AddAuthenticationStateSerialization(options => { diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/PersistentState/CounterWithPersistentState.razor b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/PersistentState/CounterWithPersistentState.razor new file mode 100644 index 000000000000..574bcdabc595 --- /dev/null +++ b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/PersistentState/CounterWithPersistentState.razor @@ -0,0 +1,19 @@ +@page "/persistent-state/counter" +@rendermode RenderMode.InteractiveServer + +Counter + +

Counter

+ +

Current count: @currentCount

+ + + +@code { + [PersistentState] public int currentCount {get; set;} + + private void IncrementCount() + { + currentCount++; + } +} From 5d503c2399e21cf071d3fe6522710ccb4a3a1e49 Mon Sep 17 00:00:00 2001 From: Ilona Tomkowicz Date: Fri, 21 Nov 2025 12:55:36 +0100 Subject: [PATCH 02/13] Rename to clarify. --- src/Components/test/E2ETest/Tests/StatePersistenceTest.cs | 4 ++-- ...tent-state-js-root-component.html => dynamic-js-root.html} | 2 +- .../Components.TestServer/RazorComponentEndpointsStartup.cs | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) rename src/Components/test/testassets/BasicTestApp/wwwroot/{persistent-state-js-root-component.html => dynamic-js-root.html} (90%) diff --git a/src/Components/test/E2ETest/Tests/StatePersistenceTest.cs b/src/Components/test/E2ETest/Tests/StatePersistenceTest.cs index 62bc170cf162..591c8be10b03 100644 --- a/src/Components/test/E2ETest/Tests/StatePersistenceTest.cs +++ b/src/Components/test/E2ETest/Tests/StatePersistenceTest.cs @@ -357,9 +357,9 @@ private void RenderComponentsWithPersistentStateAndValidate( } [Fact] - public void JsAddedPersistentStateRootComponentDoesNotTriggerCircuitError() + public void PersistentStateIsSupportedInDynamicJSRoots() { - Navigate("subdir/persistent-state-js-root-component.html"); + Navigate("subdir/dynamic-js-root.html"); Browser.Equal("Counter", () => Browser.Exists(By.TagName("h1")).Text); Browser.Equal("Current count: 0", () => Browser.Exists(By.CssSelector("p[role='status']")).Text); diff --git a/src/Components/test/testassets/BasicTestApp/wwwroot/persistent-state-js-root-component.html b/src/Components/test/testassets/BasicTestApp/wwwroot/dynamic-js-root.html similarity index 90% rename from src/Components/test/testassets/BasicTestApp/wwwroot/persistent-state-js-root-component.html rename to src/Components/test/testassets/BasicTestApp/wwwroot/dynamic-js-root.html index 180c4ae21dc1..284a4d151a11 100644 --- a/src/Components/test/testassets/BasicTestApp/wwwroot/persistent-state-js-root-component.html +++ b/src/Components/test/testassets/BasicTestApp/wwwroot/dynamic-js-root.html @@ -9,7 +9,7 @@ diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsStartup.cs b/src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsStartup.cs index 1eca1342bb5f..eaf3694878ee 100644 --- a/src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsStartup.cs +++ b/src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsStartup.cs @@ -51,7 +51,7 @@ public void ConfigureServices(IServiceCollection services) options.DisconnectedCircuitMaxRetained = 0; options.DetailedErrors = true; } - options.RootComponents.RegisterForJavaScript("persistent-state-counter"); + options.RootComponents.RegisterForJavaScript("dynamic-js-root-counter"); }) .AddAuthenticationStateSerialization(options => { From 38b2f4f66c157e42d57225233d2e4a88f2cbe05d Mon Sep 17 00:00:00 2001 From: Ilona Tomkowicz <32700855+ilonatommy@users.noreply.github.com> Date: Fri, 21 Nov 2025 12:56:28 +0100 Subject: [PATCH 03/13] Update src/Components/test/E2ETest/Tests/StatePersistenceTest.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/Components/test/E2ETest/Tests/StatePersistenceTest.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Components/test/E2ETest/Tests/StatePersistenceTest.cs b/src/Components/test/E2ETest/Tests/StatePersistenceTest.cs index 591c8be10b03..6c35ae0c9a8d 100644 --- a/src/Components/test/E2ETest/Tests/StatePersistenceTest.cs +++ b/src/Components/test/E2ETest/Tests/StatePersistenceTest.cs @@ -1,7 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Linq; using Components.TestServer.RazorComponents; using Microsoft.AspNetCore.Components.E2ETest.Infrastructure; using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures; From d75a79499186b5c79de71df3ac1fe59a6f11b122 Mon Sep 17 00:00:00 2001 From: Ilona Tomkowicz <32700855+ilonatommy@users.noreply.github.com> Date: Fri, 21 Nov 2025 12:56:36 +0100 Subject: [PATCH 04/13] Update src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/PersistentState/CounterWithPersistentState.razor Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../Pages/PersistentState/CounterWithPersistentState.razor | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/PersistentState/CounterWithPersistentState.razor b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/PersistentState/CounterWithPersistentState.razor index 574bcdabc595..3227701f2040 100644 --- a/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/PersistentState/CounterWithPersistentState.razor +++ b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/PersistentState/CounterWithPersistentState.razor @@ -10,7 +10,7 @@ @code { - [PersistentState] public int currentCount {get; set;} + [PersistentState] public int currentCount { get; set; } private void IncrementCount() { From 6af73db4f428ea4d99bc29d0ffc91185d09566c0 Mon Sep 17 00:00:00 2001 From: Ilona Tomkowicz Date: Fri, 21 Nov 2025 13:30:47 +0100 Subject: [PATCH 05/13] Change approach: do not change `GetMarkerKey` but make sure the manager is initialized on time. --- .../Server/src/Circuits/CircuitJSComponentInterop.cs | 12 ++++++++++++ src/Components/Server/src/Circuits/RemoteRenderer.cs | 4 ++-- .../WebAssembly/src/Rendering/WebAssemblyRenderer.cs | 5 ++--- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/src/Components/Server/src/Circuits/CircuitJSComponentInterop.cs b/src/Components/Server/src/Circuits/CircuitJSComponentInterop.cs index 1896429673e7..ac1e1421c213 100644 --- a/src/Components/Server/src/Circuits/CircuitJSComponentInterop.cs +++ b/src/Components/Server/src/Circuits/CircuitJSComponentInterop.cs @@ -9,6 +9,7 @@ internal sealed class CircuitJSComponentInterop : JSComponentInterop { private readonly CircuitOptions _circuitOptions; private int _jsRootComponentCount; + private RemoteRenderer? _renderer; internal CircuitJSComponentInterop(CircuitOptions circuitOptions) : base(circuitOptions.RootComponents.JSComponents) @@ -16,6 +17,11 @@ internal CircuitJSComponentInterop(CircuitOptions circuitOptions) _circuitOptions = circuitOptions; } + internal void SetRenderer(RemoteRenderer renderer) + { + _renderer = renderer; + } + protected override int AddRootComponent(string identifier, string domElementSelector) { if (_jsRootComponentCount >= _circuitOptions.RootComponents.MaxJSRootComponents) @@ -23,6 +29,12 @@ protected override int AddRootComponent(string identifier, string domElementSele throw new InvalidOperationException($"Cannot add further JS root components because the configured limit of {_circuitOptions.RootComponents.MaxJSRootComponents} has been reached."); } + if (_renderer is null) + { + throw new InvalidOperationException("Renderer has not been set. Ensure SetRenderer is called before adding root components."); + } + _renderer.GetOrCreateWebRootComponentManager(); + var id = base.AddRootComponent(identifier, domElementSelector); _jsRootComponentCount++; return id; diff --git a/src/Components/Server/src/Circuits/RemoteRenderer.cs b/src/Components/Server/src/Circuits/RemoteRenderer.cs index 654829189bb2..13f212533eb3 100644 --- a/src/Components/Server/src/Circuits/RemoteRenderer.cs +++ b/src/Components/Server/src/Circuits/RemoteRenderer.cs @@ -50,6 +50,7 @@ public RemoteRenderer( ResourceAssetCollection resourceCollection = null) : base(serviceProvider, loggerFactory, jsRuntime.ReadJsonSerializerOptions(), jsComponentInterop) { + jsComponentInterop.SetRenderer(this); _client = client; _options = options; _serverComponentDeserializer = serverComponentDeserializer; @@ -326,8 +327,7 @@ internal ComponentMarkerKey GetMarkerKey(RemoteComponentState remoteComponentSta return default; } - var webRootComponentManager = _webRootComponentManager ?? GetOrCreateWebRootComponentManager(); - return webRootComponentManager.GetRootComponentKey(remoteComponentState.ComponentId); + return _webRootComponentManager!.GetRootComponentKey(remoteComponentState.ComponentId); } private void ProcessPendingBatch(string? errorMessageOrNull, UnacknowledgedRenderBatch entry) diff --git a/src/Components/WebAssembly/WebAssembly/src/Rendering/WebAssemblyRenderer.cs b/src/Components/WebAssembly/WebAssembly/src/Rendering/WebAssemblyRenderer.cs index f280d7b0d9aa..68128739eb33 100644 --- a/src/Components/WebAssembly/WebAssembly/src/Rendering/WebAssemblyRenderer.cs +++ b/src/Components/WebAssembly/WebAssembly/src/Rendering/WebAssemblyRenderer.cs @@ -46,6 +46,7 @@ public WebAssemblyRenderer(IServiceProvider serviceProvider, ResourceAssetCollec ElementReferenceContext = DefaultWebAssemblyJSRuntime.Instance.ElementReferenceContext; DefaultWebAssemblyJSRuntime.Instance.OnUpdateRootComponents += OnUpdateRootComponents; + GetOrCreateWebRootComponentManager(); } [UnconditionalSuppressMessage("Trimming", "IL2072", Justification = "These are root components which belong to the user and are in assemblies that don't get trimmed.")] @@ -229,9 +230,7 @@ internal ComponentMarkerKey GetMarkerKey(WebAssemblyComponentState webAssemblyCo return default; } - var webRootComponentManager = _webRootComponentManager ?? GetOrCreateWebRootComponentManager(); - - return webRootComponentManager.GetRootComponentKey(webAssemblyComponentState.ComponentId); + return _webRootComponentManager!.GetRootComponentKey(webAssemblyComponentState.ComponentId); } private static partial class Log From 3a8cfbd71c403777490fa80c5144ef3687268091 Mon Sep 17 00:00:00 2001 From: Ilona Tomkowicz Date: Fri, 21 Nov 2025 13:32:24 +0100 Subject: [PATCH 06/13] Cleanup. --- src/Components/Server/src/Circuits/RemoteRenderer.cs | 9 +++------ .../WebAssembly/src/Rendering/WebAssemblyRenderer.cs | 9 +++------ 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/src/Components/Server/src/Circuits/RemoteRenderer.cs b/src/Components/Server/src/Circuits/RemoteRenderer.cs index 13f212533eb3..05f3687ccb21 100644 --- a/src/Components/Server/src/Circuits/RemoteRenderer.cs +++ b/src/Components/Server/src/Circuits/RemoteRenderer.cs @@ -322,12 +322,9 @@ protected override ComponentState CreateComponentState(int componentId, ICompone internal ComponentMarkerKey GetMarkerKey(RemoteComponentState remoteComponentState) { - if (remoteComponentState.ParentComponentState != null) - { - return default; - } - - return _webRootComponentManager!.GetRootComponentKey(remoteComponentState.ComponentId); + return remoteComponentState.ParentComponentState != null ? + default : + _webRootComponentManager!.GetRootComponentKey(remoteComponentState.ComponentId); } private void ProcessPendingBatch(string? errorMessageOrNull, UnacknowledgedRenderBatch entry) diff --git a/src/Components/WebAssembly/WebAssembly/src/Rendering/WebAssemblyRenderer.cs b/src/Components/WebAssembly/WebAssembly/src/Rendering/WebAssemblyRenderer.cs index 68128739eb33..f528035661ba 100644 --- a/src/Components/WebAssembly/WebAssembly/src/Rendering/WebAssemblyRenderer.cs +++ b/src/Components/WebAssembly/WebAssembly/src/Rendering/WebAssemblyRenderer.cs @@ -225,12 +225,9 @@ protected override ComponentState CreateComponentState(int componentId, ICompone internal ComponentMarkerKey GetMarkerKey(WebAssemblyComponentState webAssemblyComponentState) { - if (webAssemblyComponentState.ParentComponentState != null) - { - return default; - } - - return _webRootComponentManager!.GetRootComponentKey(webAssemblyComponentState.ComponentId); + return webAssemblyComponentState.ParentComponentState != null ? + default : + _webRootComponentManager!.GetRootComponentKey(webAssemblyComponentState.ComponentId); } private static partial class Log From 482ee24ebf303e7ce5fa5bcba08b210989431c3a Mon Sep 17 00:00:00 2001 From: Ilona Tomkowicz Date: Fri, 21 Nov 2025 18:15:38 +0100 Subject: [PATCH 07/13] Add test for WebAssembly. --- .../E2ETest/Tests/StatePersistenceTest.cs | 8 ++++--- .../RazorComponentEndpointsStartup.cs | 2 +- .../Pages/CounterWithPersistentState.razor | 23 +++++++++++++++++++ .../wwwroot/dynamic-js-root.html | 0 .../ComponentWithPersistentState.razor} | 3 --- 5 files changed, 29 insertions(+), 7 deletions(-) create mode 100644 src/Components/test/testassets/Components.WasmMinimal/Pages/CounterWithPersistentState.razor rename src/Components/test/testassets/{BasicTestApp => Components.WasmMinimal}/wwwroot/dynamic-js-root.html (100%) rename src/Components/test/testassets/{Components.TestServer/RazorComponents/Pages/PersistentState/CounterWithPersistentState.razor => TestContentPackage/PersistentComponents/ComponentWithPersistentState.razor} (80%) diff --git a/src/Components/test/E2ETest/Tests/StatePersistenceTest.cs b/src/Components/test/E2ETest/Tests/StatePersistenceTest.cs index 6c35ae0c9a8d..d342d4d519ca 100644 --- a/src/Components/test/E2ETest/Tests/StatePersistenceTest.cs +++ b/src/Components/test/E2ETest/Tests/StatePersistenceTest.cs @@ -355,10 +355,12 @@ private void RenderComponentsWithPersistentStateAndValidate( interactiveRuntime: interactiveRuntime); } - [Fact] - public void PersistentStateIsSupportedInDynamicJSRoots() + [Theory] + [InlineData("ServerNonPrerendered")] + [InlineData("WebAssemblyNonPrerendered")] + public void PersistentStateIsSupportedInDynamicJSRoots(string renderMode) { - Navigate("subdir/dynamic-js-root.html"); + Navigate($"subdir/WasmMinimal/dynamic-js-root.html?renderMode={renderMode}"); Browser.Equal("Counter", () => Browser.Exists(By.TagName("h1")).Text); Browser.Equal("Current count: 0", () => Browser.Exists(By.CssSelector("p[role='status']")).Text); diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsStartup.cs b/src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsStartup.cs index eaf3694878ee..13e457f49995 100644 --- a/src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsStartup.cs +++ b/src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsStartup.cs @@ -51,7 +51,7 @@ public void ConfigureServices(IServiceCollection services) options.DisconnectedCircuitMaxRetained = 0; options.DetailedErrors = true; } - options.RootComponents.RegisterForJavaScript("dynamic-js-root-counter"); + options.RootComponents.RegisterForJavaScript("dynamic-js-root-counter"); }) .AddAuthenticationStateSerialization(options => { diff --git a/src/Components/test/testassets/Components.WasmMinimal/Pages/CounterWithPersistentState.razor b/src/Components/test/testassets/Components.WasmMinimal/Pages/CounterWithPersistentState.razor new file mode 100644 index 000000000000..f44a4a80f1ac --- /dev/null +++ b/src/Components/test/testassets/Components.WasmMinimal/Pages/CounterWithPersistentState.razor @@ -0,0 +1,23 @@ +@page "/persistent-state/counter" + + + + +@code{ + [Parameter, SupplyParameterFromQuery(Name = "renderMode")] + public string? RenderModeStr { get; set; } + + private RenderModeId _renderMode; + + protected override void OnInitialized() + { + if (!string.IsNullOrEmpty(RenderModeStr)) + { + _renderMode = RenderModeHelper.ParseRenderMode(RenderModeStr); + } + else + { + throw new ArgumentException("RenderModeStr cannot be null or empty", nameof(RenderModeStr)); + } + } +} \ No newline at end of file diff --git a/src/Components/test/testassets/BasicTestApp/wwwroot/dynamic-js-root.html b/src/Components/test/testassets/Components.WasmMinimal/wwwroot/dynamic-js-root.html similarity index 100% rename from src/Components/test/testassets/BasicTestApp/wwwroot/dynamic-js-root.html rename to src/Components/test/testassets/Components.WasmMinimal/wwwroot/dynamic-js-root.html diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/PersistentState/CounterWithPersistentState.razor b/src/Components/test/testassets/TestContentPackage/PersistentComponents/ComponentWithPersistentState.razor similarity index 80% rename from src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/PersistentState/CounterWithPersistentState.razor rename to src/Components/test/testassets/TestContentPackage/PersistentComponents/ComponentWithPersistentState.razor index 3227701f2040..d26ae13fc390 100644 --- a/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/PersistentState/CounterWithPersistentState.razor +++ b/src/Components/test/testassets/TestContentPackage/PersistentComponents/ComponentWithPersistentState.razor @@ -1,6 +1,3 @@ -@page "/persistent-state/counter" -@rendermode RenderMode.InteractiveServer - Counter

Counter

From 3ea48d7385ce773941bfa010b499e1cae704e532 Mon Sep 17 00:00:00 2001 From: Ilona Tomkowicz Date: Fri, 21 Nov 2025 18:26:42 +0100 Subject: [PATCH 08/13] Redunadnt because wasm rnderer already has OnUpdateRootComponents handle --- .../WebAssembly/WebAssembly/src/Rendering/WebAssemblyRenderer.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Components/WebAssembly/WebAssembly/src/Rendering/WebAssemblyRenderer.cs b/src/Components/WebAssembly/WebAssembly/src/Rendering/WebAssemblyRenderer.cs index f528035661ba..242e28ed7ba2 100644 --- a/src/Components/WebAssembly/WebAssembly/src/Rendering/WebAssemblyRenderer.cs +++ b/src/Components/WebAssembly/WebAssembly/src/Rendering/WebAssemblyRenderer.cs @@ -46,7 +46,6 @@ public WebAssemblyRenderer(IServiceProvider serviceProvider, ResourceAssetCollec ElementReferenceContext = DefaultWebAssemblyJSRuntime.Instance.ElementReferenceContext; DefaultWebAssemblyJSRuntime.Instance.OnUpdateRootComponents += OnUpdateRootComponents; - GetOrCreateWebRootComponentManager(); } [UnconditionalSuppressMessage("Trimming", "IL2072", Justification = "These are root components which belong to the user and are in assemblies that don't get trimmed.")] From dd9777f66bf88a64158ad9b8554da771b98e05cc Mon Sep 17 00:00:00 2001 From: Ilona Tomkowicz Date: Mon, 24 Nov 2025 15:00:24 +0100 Subject: [PATCH 09/13] Revert changes to the framework. --- .../Server/src/Circuits/CircuitJSComponentInterop.cs | 12 ------------ src/Components/Server/src/Circuits/RemoteRenderer.cs | 1 - 2 files changed, 13 deletions(-) diff --git a/src/Components/Server/src/Circuits/CircuitJSComponentInterop.cs b/src/Components/Server/src/Circuits/CircuitJSComponentInterop.cs index ac1e1421c213..1896429673e7 100644 --- a/src/Components/Server/src/Circuits/CircuitJSComponentInterop.cs +++ b/src/Components/Server/src/Circuits/CircuitJSComponentInterop.cs @@ -9,7 +9,6 @@ internal sealed class CircuitJSComponentInterop : JSComponentInterop { private readonly CircuitOptions _circuitOptions; private int _jsRootComponentCount; - private RemoteRenderer? _renderer; internal CircuitJSComponentInterop(CircuitOptions circuitOptions) : base(circuitOptions.RootComponents.JSComponents) @@ -17,11 +16,6 @@ internal CircuitJSComponentInterop(CircuitOptions circuitOptions) _circuitOptions = circuitOptions; } - internal void SetRenderer(RemoteRenderer renderer) - { - _renderer = renderer; - } - protected override int AddRootComponent(string identifier, string domElementSelector) { if (_jsRootComponentCount >= _circuitOptions.RootComponents.MaxJSRootComponents) @@ -29,12 +23,6 @@ protected override int AddRootComponent(string identifier, string domElementSele throw new InvalidOperationException($"Cannot add further JS root components because the configured limit of {_circuitOptions.RootComponents.MaxJSRootComponents} has been reached."); } - if (_renderer is null) - { - throw new InvalidOperationException("Renderer has not been set. Ensure SetRenderer is called before adding root components."); - } - _renderer.GetOrCreateWebRootComponentManager(); - var id = base.AddRootComponent(identifier, domElementSelector); _jsRootComponentCount++; return id; diff --git a/src/Components/Server/src/Circuits/RemoteRenderer.cs b/src/Components/Server/src/Circuits/RemoteRenderer.cs index 05f3687ccb21..8e161697abdb 100644 --- a/src/Components/Server/src/Circuits/RemoteRenderer.cs +++ b/src/Components/Server/src/Circuits/RemoteRenderer.cs @@ -50,7 +50,6 @@ public RemoteRenderer( ResourceAssetCollection resourceCollection = null) : base(serviceProvider, loggerFactory, jsRuntime.ReadJsonSerializerOptions(), jsComponentInterop) { - jsComponentInterop.SetRenderer(this); _client = client; _options = options; _serverComponentDeserializer = serverComponentDeserializer; From cd8cc1e7c8260989ab22d686a4f965f1cdbb1d93 Mon Sep 17 00:00:00 2001 From: Ilona Tomkowicz Date: Tue, 25 Nov 2025 11:01:25 +0100 Subject: [PATCH 10/13] JS root tests do not interfre with other tests now. --- .../Tests/StatePersistanceJSRootTest.cs | 42 +++++++++++++++++++ .../E2ETest/Tests/StatePersistenceTest.cs | 14 ------- .../Components.TestServer/Program.cs | 3 +- .../RazorComponentEndpointsStartup.cs | 5 ++- 4 files changed, 48 insertions(+), 16 deletions(-) create mode 100644 src/Components/test/E2ETest/Tests/StatePersistanceJSRootTest.cs diff --git a/src/Components/test/E2ETest/Tests/StatePersistanceJSRootTest.cs b/src/Components/test/E2ETest/Tests/StatePersistanceJSRootTest.cs new file mode 100644 index 000000000000..9c554fccab79 --- /dev/null +++ b/src/Components/test/E2ETest/Tests/StatePersistanceJSRootTest.cs @@ -0,0 +1,42 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Components.TestServer.RazorComponents; +using Microsoft.AspNetCore.Components.E2ETest.Infrastructure; +using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures; +using Microsoft.AspNetCore.Components.E2ETests.ServerRenderingTests; +using Microsoft.AspNetCore.Components.Web; +using Microsoft.AspNetCore.E2ETesting; +using OpenQA.Selenium; +using TestServer; +using Xunit.Abstractions; + +namespace Microsoft.AspNetCore.Components.E2ETests.Tests; + +// These tests are for Blazor Web implementation +// For Blazor Server and Webassembly, check SaveStateTest.cs +public class StatePersistanceJSRootTest : ServerTestBase>> +{ + public StatePersistanceJSRootTest( + BrowserFixture browserFixture, + BasicTestAppServerSiteFixture> serverFixture, + ITestOutputHelper output) + : base(browserFixture, serverFixture, output) + { + serverFixture.AdditionalArguments.AddRange("--RegisterDynamicJSRootComponent", "true"); + } + + [Theory] + [InlineData("ServerNonPrerendered")] + [InlineData("WebAssemblyNonPrerendered")] + public void PersistentStateIsSupportedInDynamicJSRoots(string renderMode) + { + Navigate($"subdir/WasmMinimal/dynamic-js-root.html?renderMode={renderMode}"); + + Browser.Equal("Counter", () => Browser.Exists(By.TagName("h1")).Text); + Browser.Equal("Current count: 0", () => Browser.Exists(By.CssSelector("p[role='status']")).Text); + + Browser.Click(By.CssSelector("button.btn-primary")); + Browser.Equal("Current count: 1", () => Browser.Exists(By.CssSelector("p[role='status']")).Text); + } +} diff --git a/src/Components/test/E2ETest/Tests/StatePersistenceTest.cs b/src/Components/test/E2ETest/Tests/StatePersistenceTest.cs index d342d4d519ca..007c7ce5de16 100644 --- a/src/Components/test/E2ETest/Tests/StatePersistenceTest.cs +++ b/src/Components/test/E2ETest/Tests/StatePersistenceTest.cs @@ -355,20 +355,6 @@ private void RenderComponentsWithPersistentStateAndValidate( interactiveRuntime: interactiveRuntime); } - [Theory] - [InlineData("ServerNonPrerendered")] - [InlineData("WebAssemblyNonPrerendered")] - public void PersistentStateIsSupportedInDynamicJSRoots(string renderMode) - { - Navigate($"subdir/WasmMinimal/dynamic-js-root.html?renderMode={renderMode}"); - - Browser.Equal("Counter", () => Browser.Exists(By.TagName("h1")).Text); - Browser.Equal("Current count: 0", () => Browser.Exists(By.CssSelector("p[role='status']")).Text); - - Browser.Click(By.CssSelector("button.btn-primary")); - Browser.Equal("Current count: 1", () => Browser.Exists(By.CssSelector("p[role='status']")).Text); - } - private void AssertPageState( string mode, string renderMode, diff --git a/src/Components/test/testassets/Components.TestServer/Program.cs b/src/Components/test/testassets/Components.TestServer/Program.cs index 568f809454a7..58e39b5d11c8 100644 --- a/src/Components/test/testassets/Components.TestServer/Program.cs +++ b/src/Components/test/testassets/Components.TestServer/Program.cs @@ -23,7 +23,8 @@ public static async Task Main(string[] args) ["Server authentication"] = (BuildWebHost(CreateAdditionalArgs(args)), "/subdir"), ["CORS (WASM)"] = (BuildWebHost(CreateAdditionalArgs(args)), "/subdir"), ["Prerendering (Server-side)"] = (BuildWebHost(CreateAdditionalArgs(args)), "/prerendered"), - ["Razor Component Endpoints"] = (BuildWebHost>(CreateAdditionalArgs(args)), "/subdir"), + ["Razor Component Endpoints"] = (BuildWebHost>(CreateAdditionalArgs(args)), "/subdir"), + ["Razor Component Endpoints with JS Root Component"] = (BuildWebHost>(CreateAdditionalArgs([.. args, "--RegisterDynamicJSRootComponent", "true"])), "/subdir"), ["Deferred component content (Server-side)"] = (BuildWebHost(CreateAdditionalArgs(args)), "/deferred-component-content"), ["Locked navigation (Server-side)"] = (BuildWebHost(CreateAdditionalArgs(args)), "/locked-navigation"), ["Client-side with fallback"] = (BuildWebHost(CreateAdditionalArgs(args)), "/fallback"), diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsStartup.cs b/src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsStartup.cs index 13e457f49995..986d4c391fd0 100644 --- a/src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsStartup.cs +++ b/src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsStartup.cs @@ -51,7 +51,10 @@ public void ConfigureServices(IServiceCollection services) options.DisconnectedCircuitMaxRetained = 0; options.DetailedErrors = true; } - options.RootComponents.RegisterForJavaScript("dynamic-js-root-counter"); + if (Configuration.GetValue("RegisterDynamicJSRootComponent")) + { + options.RootComponents.RegisterForJavaScript("dynamic-js-root-counter"); + } }) .AddAuthenticationStateSerialization(options => { From fb3717edf9e74c00406a3ffbb46d91f77b235504 Mon Sep 17 00:00:00 2001 From: Ilona Tomkowicz Date: Tue, 25 Nov 2025 11:16:05 +0100 Subject: [PATCH 11/13] Simplest fix. --- src/Components/Server/src/Circuits/RemoteRenderer.cs | 10 +++++++--- .../WebAssembly/src/Rendering/WebAssemblyRenderer.cs | 11 ++++++++--- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/src/Components/Server/src/Circuits/RemoteRenderer.cs b/src/Components/Server/src/Circuits/RemoteRenderer.cs index 8e161697abdb..654829189bb2 100644 --- a/src/Components/Server/src/Circuits/RemoteRenderer.cs +++ b/src/Components/Server/src/Circuits/RemoteRenderer.cs @@ -321,9 +321,13 @@ protected override ComponentState CreateComponentState(int componentId, ICompone internal ComponentMarkerKey GetMarkerKey(RemoteComponentState remoteComponentState) { - return remoteComponentState.ParentComponentState != null ? - default : - _webRootComponentManager!.GetRootComponentKey(remoteComponentState.ComponentId); + if (remoteComponentState.ParentComponentState != null) + { + return default; + } + + var webRootComponentManager = _webRootComponentManager ?? GetOrCreateWebRootComponentManager(); + return webRootComponentManager.GetRootComponentKey(remoteComponentState.ComponentId); } private void ProcessPendingBatch(string? errorMessageOrNull, UnacknowledgedRenderBatch entry) diff --git a/src/Components/WebAssembly/WebAssembly/src/Rendering/WebAssemblyRenderer.cs b/src/Components/WebAssembly/WebAssembly/src/Rendering/WebAssemblyRenderer.cs index 242e28ed7ba2..f280d7b0d9aa 100644 --- a/src/Components/WebAssembly/WebAssembly/src/Rendering/WebAssemblyRenderer.cs +++ b/src/Components/WebAssembly/WebAssembly/src/Rendering/WebAssemblyRenderer.cs @@ -224,9 +224,14 @@ protected override ComponentState CreateComponentState(int componentId, ICompone internal ComponentMarkerKey GetMarkerKey(WebAssemblyComponentState webAssemblyComponentState) { - return webAssemblyComponentState.ParentComponentState != null ? - default : - _webRootComponentManager!.GetRootComponentKey(webAssemblyComponentState.ComponentId); + if (webAssemblyComponentState.ParentComponentState != null) + { + return default; + } + + var webRootComponentManager = _webRootComponentManager ?? GetOrCreateWebRootComponentManager(); + + return webRootComponentManager.GetRootComponentKey(webAssemblyComponentState.ComponentId); } private static partial class Log From 013475de858294563ed33562e9a90cdd9cb4fb2d Mon Sep 17 00:00:00 2001 From: Ilona Tomkowicz Date: Tue, 25 Nov 2025 11:41:59 +0100 Subject: [PATCH 12/13] Rever: Wasm renderer already initializes the manager with `OnUpdateRootComponents`. --- .../WebAssembly/src/Rendering/WebAssemblyRenderer.cs | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/Components/WebAssembly/WebAssembly/src/Rendering/WebAssemblyRenderer.cs b/src/Components/WebAssembly/WebAssembly/src/Rendering/WebAssemblyRenderer.cs index f280d7b0d9aa..242e28ed7ba2 100644 --- a/src/Components/WebAssembly/WebAssembly/src/Rendering/WebAssemblyRenderer.cs +++ b/src/Components/WebAssembly/WebAssembly/src/Rendering/WebAssemblyRenderer.cs @@ -224,14 +224,9 @@ protected override ComponentState CreateComponentState(int componentId, ICompone internal ComponentMarkerKey GetMarkerKey(WebAssemblyComponentState webAssemblyComponentState) { - if (webAssemblyComponentState.ParentComponentState != null) - { - return default; - } - - var webRootComponentManager = _webRootComponentManager ?? GetOrCreateWebRootComponentManager(); - - return webRootComponentManager.GetRootComponentKey(webAssemblyComponentState.ComponentId); + return webAssemblyComponentState.ParentComponentState != null ? + default : + _webRootComponentManager!.GetRootComponentKey(webAssemblyComponentState.ComponentId); } private static partial class Log From da15cfb6481f767db45fa0e961d656d19e381510 Mon Sep 17 00:00:00 2001 From: Ilona Tomkowicz Date: Tue, 25 Nov 2025 15:30:05 +0100 Subject: [PATCH 13/13] Feedback - move the initialization to CircuitFatory. --- src/Components/Server/src/Circuits/CircuitFactory.cs | 1 + src/Components/Server/src/Circuits/RemoteRenderer.cs | 10 +++------- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/src/Components/Server/src/Circuits/CircuitFactory.cs b/src/Components/Server/src/Circuits/CircuitFactory.cs index 0c086f054c03..a9aefb9fbfc9 100644 --- a/src/Components/Server/src/Circuits/CircuitFactory.cs +++ b/src/Components/Server/src/Circuits/CircuitFactory.cs @@ -93,6 +93,7 @@ public async ValueTask CreateCircuitHostAsync( resourceCollection); circuitActivitySource.Init(new Infrastructure.Server.ComponentsActivityLinkStore(renderer)); + renderer.GetOrCreateWebRootComponentManager(); // In Blazor Server we have already restored the app state, so we can get the handlers from DI. // In Blazor Web the state is provided in the first call to UpdateRootComponents, so we need to diff --git a/src/Components/Server/src/Circuits/RemoteRenderer.cs b/src/Components/Server/src/Circuits/RemoteRenderer.cs index 654829189bb2..8e161697abdb 100644 --- a/src/Components/Server/src/Circuits/RemoteRenderer.cs +++ b/src/Components/Server/src/Circuits/RemoteRenderer.cs @@ -321,13 +321,9 @@ protected override ComponentState CreateComponentState(int componentId, ICompone internal ComponentMarkerKey GetMarkerKey(RemoteComponentState remoteComponentState) { - if (remoteComponentState.ParentComponentState != null) - { - return default; - } - - var webRootComponentManager = _webRootComponentManager ?? GetOrCreateWebRootComponentManager(); - return webRootComponentManager.GetRootComponentKey(remoteComponentState.ComponentId); + return remoteComponentState.ParentComponentState != null ? + default : + _webRootComponentManager!.GetRootComponentKey(remoteComponentState.ComponentId); } private void ProcessPendingBatch(string? errorMessageOrNull, UnacknowledgedRenderBatch entry)