From 0317ecf3043662a40679aded94b4da658ee9d5e3 Mon Sep 17 00:00:00 2001 From: Ilona Tomkowicz Date: Tue, 14 Oct 2025 20:12:36 +0200 Subject: [PATCH 01/11] Fix --- .../Components/src/Routing/Router.cs | 16 +++++++---- .../NoInteractivityTest.cs | 20 +++++++++++++ .../RazorComponents/App.razor | 28 +++++++++++++++++-- 3 files changed, 57 insertions(+), 7 deletions(-) diff --git a/src/Components/Components/src/Routing/Router.cs b/src/Components/Components/src/Routing/Router.cs index ecb69fe2cf63..70c4ba3efaff 100644 --- a/src/Components/Components/src/Routing/Router.cs +++ b/src/Components/Components/src/Routing/Router.cs @@ -220,17 +220,23 @@ private void ClearRouteCaches() internal virtual void Refresh(bool isNavigationIntercepted) { + var endpointRouteData = RoutingStateProvider?.RouteData; + var navigationInProgress = _previousOnNavigateTask.Status != TaskStatus.RanToCompletion; + // If an `OnNavigateAsync` task is currently in progress, then wait // for it to complete before rendering. Note: because _previousOnNavigateTask // is initialized to a CompletedTask on initialization, this will still // allow first-render to complete successfully. - if (_previousOnNavigateTask.Status != TaskStatus.RanToCompletion) + if (navigationInProgress) { - if (Navigating != null) + if (endpointRouteData is null) { - _renderHandle.Render(Navigating); + if (Navigating != null) + { + _renderHandle.Render(Navigating); + } + return; } - return; } var relativePath = NavigationManager.ToBaseRelativePath(_locationAbsolute.AsSpan()); @@ -239,7 +245,7 @@ internal virtual void Refresh(bool isNavigationIntercepted) ComponentsActivityHandle activityHandle; // In order to avoid routing twice we check for RouteData - if (RoutingStateProvider?.RouteData is { } endpointRouteData) + if (endpointRouteData is not null) { activityHandle = RecordDiagnostics(endpointRouteData.PageType.FullName, endpointRouteData.Template); diff --git a/src/Components/test/E2ETest/ServerRenderingTests/NoInteractivityTest.cs b/src/Components/test/E2ETest/ServerRenderingTests/NoInteractivityTest.cs index a6421eb94689..d7c64f0c559c 100644 --- a/src/Components/test/E2ETest/ServerRenderingTests/NoInteractivityTest.cs +++ b/src/Components/test/E2ETest/ServerRenderingTests/NoInteractivityTest.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; using System.Net.Http; using Components.TestServer.RazorComponents; using Microsoft.AspNetCore.Components.E2ETest; @@ -125,6 +126,25 @@ public void BrowserNavigationToNotExistingPath_ReExecutesTo404(bool streaming) AssertReExecutionPageRendered(); } + [Fact] + public void BrowserNavigationToNotExistingPath_WithOnNavigateAsync_ReExecutesTo404() + { + AppContext.SetSwitch("Microsoft.AspNetCore.Components.Endpoints.NavigationManager.DisableThrowNavigationException", isEnabled: true); + + // using query for controlling router parameters does not work in re-execution scenario, we have to rely on other communication channel + const string useOnNavigateAsyncSwitch = "Components.TestServer.RazorComponents.UseOnNavigateAsync"; + AppContext.SetSwitch(useOnNavigateAsyncSwitch, true); + try + { + Navigate($"{ServerPathBase}/reexecution/not-existing-page"); + AssertReExecutionPageRendered(); + } + finally + { + AppContext.SetSwitch(useOnNavigateAsyncSwitch, false); + } + } + private void AssertReExecutionPageRendered() => Browser.Equal("Welcome On Page Re-executed After Not Found Event", () => Browser.Exists(By.Id("test-info")).Text); diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponents/App.razor b/src/Components/test/testassets/Components.TestServer/RazorComponents/App.razor index f547144c8fdd..76a023f8feff 100644 --- a/src/Components/test/testassets/Components.TestServer/RazorComponents/App.razor +++ b/src/Components/test/testassets/Components.TestServer/RazorComponents/App.razor @@ -3,6 +3,10 @@ @using Components.WasmMinimal.Pages.NotFound @using TestContentPackage.NotFound @using Components.TestServer.RazorComponents +@using Microsoft.AspNetCore.Components +@using Microsoft.AspNetCore.Components.Routing +@using Microsoft.AspNetCore.Components.Web +@using System.Threading.Tasks @code { [Parameter] @@ -17,8 +21,13 @@ [SupplyParameterFromQuery(Name = "appSetsEventArgsPath")] public bool AppSetsEventArgsPath { get; set; } + [Parameter] + [SupplyParameterFromQuery(Name = "useOnNavigateAsync")] + public string? UseOnNavigateAsync { get; set; } + private Type? NotFoundPageType { get; set; } private NavigationManager _navigationManager = default!; + private bool ShouldDelayOnNavigateAsync => string.Equals(UseOnNavigateAsync, "true", StringComparison.OrdinalIgnoreCase); [Inject] private NavigationManager NavigationManager @@ -70,6 +79,21 @@ _navigationManager.OnNotFound -= OnNotFoundEvent; } } + + private Task HandleOnNavigateAsync(NavigationContext args) + { + if (!ShouldDelayOnNavigateAsync) + { + return Task.CompletedTask; + } + + return PerformOnNavigateAsyncWork(); + } + + private async Task PerformOnNavigateAsyncWork() + { + await Task.Yield(); + } } @@ -93,7 +117,7 @@ { @if (NotFoundPageType is not null) { - + @@ -102,7 +126,7 @@ } else { - + From c90449bb1631a776a8af275ef43808956bcb1cda Mon Sep 17 00:00:00 2001 From: Ilona Tomkowicz <32700855+ilonatommy@users.noreply.github.com> Date: Wed, 15 Oct 2025 12:10:37 +0200 Subject: [PATCH 02/11] Update src/Components/test/testassets/Components.TestServer/RazorComponents/App.razor Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../testassets/Components.TestServer/RazorComponents/App.razor | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponents/App.razor b/src/Components/test/testassets/Components.TestServer/RazorComponents/App.razor index 76a023f8feff..1c6c272bf8ea 100644 --- a/src/Components/test/testassets/Components.TestServer/RazorComponents/App.razor +++ b/src/Components/test/testassets/Components.TestServer/RazorComponents/App.razor @@ -27,7 +27,8 @@ private Type? NotFoundPageType { get; set; } private NavigationManager _navigationManager = default!; - private bool ShouldDelayOnNavigateAsync => string.Equals(UseOnNavigateAsync, "true", StringComparison.OrdinalIgnoreCase); + private bool ShouldDelayOnNavigateAsync + => bool.TryParse(UseOnNavigateAsync, out var result) && result; [Inject] private NavigationManager NavigationManager From 64df78f408bfb40eb229c10681398240b004a390 Mon Sep 17 00:00:00 2001 From: Ilona Tomkowicz <32700855+ilonatommy@users.noreply.github.com> Date: Wed, 15 Oct 2025 12:10:48 +0200 Subject: [PATCH 03/11] Update src/Components/test/E2ETest/ServerRenderingTests/NoInteractivityTest.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../test/E2ETest/ServerRenderingTests/NoInteractivityTest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Components/test/E2ETest/ServerRenderingTests/NoInteractivityTest.cs b/src/Components/test/E2ETest/ServerRenderingTests/NoInteractivityTest.cs index d7c64f0c559c..aefef8da43ca 100644 --- a/src/Components/test/E2ETest/ServerRenderingTests/NoInteractivityTest.cs +++ b/src/Components/test/E2ETest/ServerRenderingTests/NoInteractivityTest.cs @@ -1,8 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System; using System.Net.Http; +using System; using Components.TestServer.RazorComponents; using Microsoft.AspNetCore.Components.E2ETest; using Microsoft.AspNetCore.Components.E2ETest.Infrastructure; From 5ed75df90a5be397846bfeb316de631363f79c09 Mon Sep 17 00:00:00 2001 From: Ilona Tomkowicz Date: Wed, 15 Oct 2025 16:26:56 +0200 Subject: [PATCH 04/11] Cleanup. --- src/Components/Components/src/Routing/Router.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Components/Components/src/Routing/Router.cs b/src/Components/Components/src/Routing/Router.cs index 70c4ba3efaff..7d8e98dff339 100644 --- a/src/Components/Components/src/Routing/Router.cs +++ b/src/Components/Components/src/Routing/Router.cs @@ -221,13 +221,12 @@ private void ClearRouteCaches() internal virtual void Refresh(bool isNavigationIntercepted) { var endpointRouteData = RoutingStateProvider?.RouteData; - var navigationInProgress = _previousOnNavigateTask.Status != TaskStatus.RanToCompletion; // If an `OnNavigateAsync` task is currently in progress, then wait // for it to complete before rendering. Note: because _previousOnNavigateTask // is initialized to a CompletedTask on initialization, this will still // allow first-render to complete successfully. - if (navigationInProgress) + if (_previousOnNavigateTask.Status != TaskStatus.RanToCompletion) { if (endpointRouteData is null) { From b8856ad29dac75d8ab049b6727f58db15a2c6adb Mon Sep 17 00:00:00 2001 From: Ilona Tomkowicz Date: Wed, 15 Oct 2025 16:29:44 +0200 Subject: [PATCH 05/11] More cleanup. --- src/Components/Components/src/Routing/Router.cs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/Components/Components/src/Routing/Router.cs b/src/Components/Components/src/Routing/Router.cs index 7d8e98dff339..2afa52e99a40 100644 --- a/src/Components/Components/src/Routing/Router.cs +++ b/src/Components/Components/src/Routing/Router.cs @@ -220,20 +220,18 @@ private void ClearRouteCaches() internal virtual void Refresh(bool isNavigationIntercepted) { + // endpointRouterData is populated only in navigations that passed through SSR, including re-executions var endpointRouteData = RoutingStateProvider?.RouteData; // If an `OnNavigateAsync` task is currently in progress, then wait // for it to complete before rendering. Note: because _previousOnNavigateTask // is initialized to a CompletedTask on initialization, this will still // allow first-render to complete successfully. - if (_previousOnNavigateTask.Status != TaskStatus.RanToCompletion) + if (_previousOnNavigateTask.Status != TaskStatus.RanToCompletion && endpointRouteData is null) { - if (endpointRouteData is null) + if (Navigating != null) { - if (Navigating != null) - { - _renderHandle.Render(Navigating); - } + _renderHandle.Render(Navigating); return; } } From 5f632fe601db415b897af62bed666ad048ded504 Mon Sep 17 00:00:00 2001 From: Ilona Tomkowicz Date: Mon, 20 Oct 2025 18:20:41 +0200 Subject: [PATCH 06/11] Prevent duplicate force-load navigations to keep the history intact. --- src/Components/Components/src/Routing/Router.cs | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/Components/Components/src/Routing/Router.cs b/src/Components/Components/src/Routing/Router.cs index 2afa52e99a40..ba627ac3840f 100644 --- a/src/Components/Components/src/Routing/Router.cs +++ b/src/Components/Components/src/Routing/Router.cs @@ -31,6 +31,8 @@ static readonly IReadOnlyDictionary _emptyParametersDictionary ILogger _logger; string _notFoundPageRoute; + private string _pendingExternalNavigationUri; + private string _updateScrollPositionForHashLastLocation; private bool _updateScrollPositionForHash; @@ -232,8 +234,14 @@ internal virtual void Refresh(bool isNavigationIntercepted) if (Navigating != null) { _renderHandle.Render(Navigating); - return; } + + return; + } + + if (_pendingExternalNavigationUri is not null && !string.Equals(_pendingExternalNavigationUri, _locationAbsolute, StringComparison.Ordinal)) + { + _pendingExternalNavigationUri = null; } var relativePath = NavigationManager.ToBaseRelativePath(_locationAbsolute.AsSpan()); @@ -309,7 +317,11 @@ internal virtual void Refresh(bool isNavigationIntercepted) activityHandle = RecordDiagnostics("External", "External"); Log.NavigatingToExternalUri(_logger, _locationAbsolute, locationPath, _baseUri); - NavigationManager.NavigateTo(_locationAbsolute, forceLoad: true); + if (!string.Equals(_pendingExternalNavigationUri, _locationAbsolute, StringComparison.Ordinal)) + { + _pendingExternalNavigationUri = _locationAbsolute; + NavigationManager.NavigateTo(_locationAbsolute, forceLoad: true); + } } } _renderHandle.ComponentActivitySource?.StopNavigateActivity(activityHandle, null); From 0f0cf6a694cabb66561f1397d79ec1eeb31590c1 Mon Sep 17 00:00:00 2001 From: Ilona Tomkowicz Date: Tue, 21 Oct 2025 12:01:38 +0200 Subject: [PATCH 07/11] Add a better test for https://github.com/dotnet/aspnetcore/pull/24225 to check if regression happens. --- .../NoInteractivityTest.cs | 20 ++++++ .../RazorComponents/App.razor | 5 ++ .../NavigationCompletionTracker.cs | 65 +++++++++++++++++++ .../Pages/Routing/WithLazyAssembly.razor | 12 ++++ 4 files changed, 102 insertions(+) create mode 100644 src/Components/test/testassets/Components.TestServer/RazorComponents/NavigationCompletionTracker.cs create mode 100644 src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/Routing/WithLazyAssembly.razor diff --git a/src/Components/test/E2ETest/ServerRenderingTests/NoInteractivityTest.cs b/src/Components/test/E2ETest/ServerRenderingTests/NoInteractivityTest.cs index aefef8da43ca..811d7e3da138 100644 --- a/src/Components/test/E2ETest/ServerRenderingTests/NoInteractivityTest.cs +++ b/src/Components/test/E2ETest/ServerRenderingTests/NoInteractivityTest.cs @@ -145,6 +145,23 @@ public void BrowserNavigationToNotExistingPath_WithOnNavigateAsync_ReExecutesTo4 } } + [Fact] + public void BrowserNavigationToLazyLoadedRoute_WaitsForOnNavigateAsyncGuard() + { + const string navigationGuardSwitch = "Components.TestServer.RazorComponents.UseNavigationCompletionGuard"; + AppContext.SetSwitch(navigationGuardSwitch, true); + + try + { + Navigate($"{ServerPathBase}/routing/with-lazy-assembly"); + Browser.Equal("Lazy route rendered", () => Browser.Exists(By.Id("lazy-route-status")).Text); + } + finally + { + AppContext.SetSwitch(navigationGuardSwitch, false); + } + } + private void AssertReExecutionPageRendered() => Browser.Equal("Welcome On Page Re-executed After Not Found Event", () => Browser.Exists(By.Id("test-info")).Text); @@ -454,3 +471,6 @@ public void StatusCodePagesWithReExecution() Browser.Equal("Re-executed page", () => Browser.Title); } } + +#pragma warning restore RS0037 // PublicAPI files must include '#nullable enable' +#pragma warning restore RS0016 // Add public types and members to the declared API diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponents/App.razor b/src/Components/test/testassets/Components.TestServer/RazorComponents/App.razor index 1c6c272bf8ea..bbd4d14fb6eb 100644 --- a/src/Components/test/testassets/Components.TestServer/RazorComponents/App.razor +++ b/src/Components/test/testassets/Components.TestServer/RazorComponents/App.razor @@ -83,6 +83,11 @@ private Task HandleOnNavigateAsync(NavigationContext args) { + if (NavigationCompletionTracker.TryGetGuardTask(args.Path, out var guardTask)) + { + return guardTask; + } + if (!ShouldDelayOnNavigateAsync) { return Task.CompletedTask; diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponents/NavigationCompletionTracker.cs b/src/Components/test/testassets/Components.TestServer/RazorComponents/NavigationCompletionTracker.cs new file mode 100644 index 000000000000..0f22080a7f3b --- /dev/null +++ b/src/Components/test/testassets/Components.TestServer/RazorComponents/NavigationCompletionTracker.cs @@ -0,0 +1,65 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Components.TestServer.RazorComponents; + +internal static class NavigationCompletionTracker +{ + internal const string GuardSwitchName = "Components.TestServer.RazorComponents.UseNavigationCompletionGuard"; + + private const string TrackedPathSuffix = "with-lazy-assembly"; + private static int _isNavigationTracked; + private static int _isNavigationCompleted; + + public static bool TryGetGuardTask(string? path, out Task guardTask) + { + if (!IsGuardEnabledForPath(path)) + { + guardTask = Task.CompletedTask; + return false; + } + + guardTask = TrackNavigationAsync(); + return true; + } + + public static void AssertNavigationCompleted() + { + if (Volatile.Read(ref _isNavigationTracked) == 1 && Volatile.Read(ref _isNavigationCompleted) == 0) + { + throw new InvalidOperationException("Navigation finished before OnNavigateAsync work completed."); + } + + Volatile.Write(ref _isNavigationTracked, 0); + } + + private static bool IsGuardEnabledForPath(string? path) + { + if (!AppContext.TryGetSwitch(GuardSwitchName, out var isEnabled) || !isEnabled) + { + return false; + } + + return path is not null && path.EndsWith(TrackedPathSuffix, StringComparison.OrdinalIgnoreCase); + } + + private static async Task TrackNavigationAsync() + { + Volatile.Write(ref _isNavigationTracked, 1); + Volatile.Write(ref _isNavigationCompleted, 0); + + try + { + await Task.Yield(); + await Task.Delay(TimeSpan.FromMilliseconds(50)).ConfigureAwait(false); + } + finally + { + Volatile.Write(ref _isNavigationCompleted, 1); + } + } +} diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/Routing/WithLazyAssembly.razor b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/Routing/WithLazyAssembly.razor new file mode 100644 index 000000000000..7abc58385177 --- /dev/null +++ b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/Routing/WithLazyAssembly.razor @@ -0,0 +1,12 @@ +@page "/routing/with-lazy-assembly" +@using Components.TestServer.RazorComponents; + +

Lazy route rendered

+ +@code +{ + protected override void OnInitialized() + { + NavigationCompletionTracker.AssertNavigationCompleted(); + } +} From f2fb81119abbf4027a7cccac2621c09bc98d8543 Mon Sep 17 00:00:00 2001 From: Ilona Tomkowicz Date: Wed, 22 Oct 2025 17:03:01 +0200 Subject: [PATCH 08/11] Revert Router changes + fix BrowserNavigationToNotExistingPath_WithOnNavigateAsync_ReExecutesTo404 test. --- .../Components/src/Routing/Router.cs | 21 +++---------------- .../RazorComponents/App.razor | 8 +++---- 2 files changed, 6 insertions(+), 23 deletions(-) diff --git a/src/Components/Components/src/Routing/Router.cs b/src/Components/Components/src/Routing/Router.cs index ba627ac3840f..ecb69fe2cf63 100644 --- a/src/Components/Components/src/Routing/Router.cs +++ b/src/Components/Components/src/Routing/Router.cs @@ -31,8 +31,6 @@ static readonly IReadOnlyDictionary _emptyParametersDictionary ILogger _logger; string _notFoundPageRoute; - private string _pendingExternalNavigationUri; - private string _updateScrollPositionForHashLastLocation; private bool _updateScrollPositionForHash; @@ -222,35 +220,26 @@ private void ClearRouteCaches() internal virtual void Refresh(bool isNavigationIntercepted) { - // endpointRouterData is populated only in navigations that passed through SSR, including re-executions - var endpointRouteData = RoutingStateProvider?.RouteData; - // If an `OnNavigateAsync` task is currently in progress, then wait // for it to complete before rendering. Note: because _previousOnNavigateTask // is initialized to a CompletedTask on initialization, this will still // allow first-render to complete successfully. - if (_previousOnNavigateTask.Status != TaskStatus.RanToCompletion && endpointRouteData is null) + if (_previousOnNavigateTask.Status != TaskStatus.RanToCompletion) { if (Navigating != null) { _renderHandle.Render(Navigating); } - return; } - if (_pendingExternalNavigationUri is not null && !string.Equals(_pendingExternalNavigationUri, _locationAbsolute, StringComparison.Ordinal)) - { - _pendingExternalNavigationUri = null; - } - var relativePath = NavigationManager.ToBaseRelativePath(_locationAbsolute.AsSpan()); var locationPathSpan = TrimQueryOrHash(relativePath); var locationPath = $"/{locationPathSpan}"; ComponentsActivityHandle activityHandle; // In order to avoid routing twice we check for RouteData - if (endpointRouteData is not null) + if (RoutingStateProvider?.RouteData is { } endpointRouteData) { activityHandle = RecordDiagnostics(endpointRouteData.PageType.FullName, endpointRouteData.Template); @@ -317,11 +306,7 @@ internal virtual void Refresh(bool isNavigationIntercepted) activityHandle = RecordDiagnostics("External", "External"); Log.NavigatingToExternalUri(_logger, _locationAbsolute, locationPath, _baseUri); - if (!string.Equals(_pendingExternalNavigationUri, _locationAbsolute, StringComparison.Ordinal)) - { - _pendingExternalNavigationUri = _locationAbsolute; - NavigationManager.NavigateTo(_locationAbsolute, forceLoad: true); - } + NavigationManager.NavigateTo(_locationAbsolute, forceLoad: true); } } _renderHandle.ComponentActivitySource?.StopNavigateActivity(activityHandle, null); diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponents/App.razor b/src/Components/test/testassets/Components.TestServer/RazorComponents/App.razor index bbd4d14fb6eb..c21fa05d746a 100644 --- a/src/Components/test/testassets/Components.TestServer/RazorComponents/App.razor +++ b/src/Components/test/testassets/Components.TestServer/RazorComponents/App.razor @@ -21,14 +21,12 @@ [SupplyParameterFromQuery(Name = "appSetsEventArgsPath")] public bool AppSetsEventArgsPath { get; set; } - [Parameter] - [SupplyParameterFromQuery(Name = "useOnNavigateAsync")] - public string? UseOnNavigateAsync { get; set; } + private const string UseOnNavigateAsyncSwitchName = "Components.TestServer.RazorComponents.UseOnNavigateAsync"; private Type? NotFoundPageType { get; set; } private NavigationManager _navigationManager = default!; - private bool ShouldDelayOnNavigateAsync - => bool.TryParse(UseOnNavigateAsync, out var result) && result; + private bool ShouldDelayOnNavigateAsync => + AppContext.TryGetSwitch(UseOnNavigateAsyncSwitchName, out var switchEnabled) && switchEnabled; [Inject] private NavigationManager NavigationManager From 496d430029fd7440b5db8cf896bbad29ecff769d Mon Sep 17 00:00:00 2001 From: Ilona Tomkowicz Date: Wed, 22 Oct 2025 19:23:42 +0200 Subject: [PATCH 09/11] Allow Router to bypass OnNavigateAsync guard during 404 re-execution. --- .../Components/src/Routing/RouteTable.cs | 2 ++ .../Components/src/Routing/Router.cs | 20 +++++++++++++++++-- .../src/RazorComponentEndpointInvoker.cs | 3 ++- .../src/Rendering/EndpointHtmlRenderer.cs | 17 ++++++++++++---- 4 files changed, 35 insertions(+), 7 deletions(-) diff --git a/src/Components/Components/src/Routing/RouteTable.cs b/src/Components/Components/src/Routing/RouteTable.cs index f56beec826b9..f8ee7649d626 100644 --- a/src/Components/Components/src/Routing/RouteTable.cs +++ b/src/Components/Components/src/Routing/RouteTable.cs @@ -78,6 +78,8 @@ public void Route(RouteContext routeContext) private static void ProcessParameters(InboundRouteEntry entry, RouteValueDictionary routeValues) { + routeValues.Remove(Router.AllowRenderDuringPendingNavigationKey); + // Add null values for unused route parameters. if (entry.UnusedRouteParameterNames != null) { diff --git a/src/Components/Components/src/Routing/Router.cs b/src/Components/Components/src/Routing/Router.cs index ecb69fe2cf63..ab29da22a08e 100644 --- a/src/Components/Components/src/Routing/Router.cs +++ b/src/Components/Components/src/Routing/Router.cs @@ -42,6 +42,8 @@ static readonly IReadOnlyDictionary _emptyParametersDictionary private bool _onNavigateCalled; + internal const string AllowRenderDuringPendingNavigationKey = "__BlazorAllowRenderDuringPendingNavigation"; + [Inject] private NavigationManager NavigationManager { get; set; } [Inject] private INavigationInterception NavigationInterception { get; set; } @@ -220,11 +222,14 @@ private void ClearRouteCaches() internal virtual void Refresh(bool isNavigationIntercepted) { + var providerRouteData = RoutingStateProvider?.RouteData; + var allowRenderDuringPendingNavigation = TryConsumeAllowRenderDuringPendingNavigation(providerRouteData); + // If an `OnNavigateAsync` task is currently in progress, then wait // for it to complete before rendering. Note: because _previousOnNavigateTask // is initialized to a CompletedTask on initialization, this will still // allow first-render to complete successfully. - if (_previousOnNavigateTask.Status != TaskStatus.RanToCompletion) + if (_previousOnNavigateTask.Status != TaskStatus.RanToCompletion && !allowRenderDuringPendingNavigation) { if (Navigating != null) { @@ -239,7 +244,7 @@ internal virtual void Refresh(bool isNavigationIntercepted) ComponentsActivityHandle activityHandle; // In order to avoid routing twice we check for RouteData - if (RoutingStateProvider?.RouteData is { } endpointRouteData) + if (providerRouteData is { } endpointRouteData) { activityHandle = RecordDiagnostics(endpointRouteData.PageType.FullName, endpointRouteData.Template); @@ -312,6 +317,17 @@ internal virtual void Refresh(bool isNavigationIntercepted) _renderHandle.ComponentActivitySource?.StopNavigateActivity(activityHandle, null); } + private static bool TryConsumeAllowRenderDuringPendingNavigation(RouteData? routeData) + { + if (routeData?.RouteValues.TryGetValue(AllowRenderDuringPendingNavigationKey, out var value) == true && value is true) + { + (routeData.RouteValues as IDictionary)?.Remove(AllowRenderDuringPendingNavigationKey); + return true; + } + + return false; + } + private ComponentsActivityHandle RecordDiagnostics(string componentType, string template) { ComponentsActivityHandle activityHandle = default; diff --git a/src/Components/Endpoints/src/RazorComponentEndpointInvoker.cs b/src/Components/Endpoints/src/RazorComponentEndpointInvoker.cs index 22119d0522c9..af9872137e64 100644 --- a/src/Components/Endpoints/src/RazorComponentEndpointInvoker.cs +++ b/src/Components/Endpoints/src/RazorComponentEndpointInvoker.cs @@ -87,7 +87,8 @@ await _renderer.InitializeStandardComponentServicesAsync( context, componentType: pageComponent, handler: result.HandlerName, - form: result.HandlerName != null && context.Request.HasFormContentType ? await context.Request.ReadFormAsync() : null); + form: result.HandlerName != null && context.Request.HasFormContentType ? await context.Request.ReadFormAsync() : null, + allowRenderingDuringPendingNavigation: isReExecuted); // Matches MVC's MemoryPoolHttpResponseStreamWriterFactory.DefaultBufferSize var defaultBufferSize = 16 * 1024; diff --git a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.cs b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.cs index 4d95f68c0fc5..c51b2d30f14c 100644 --- a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.cs +++ b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.cs @@ -54,6 +54,8 @@ internal partial class EndpointHtmlRenderer : StaticHtmlRenderer, IComponentPrer private string _notFoundUrl = string.Empty; + private const string AllowRenderDuringPendingNavigationKey = "__BlazorAllowRenderDuringPendingNavigation"; + public EndpointHtmlRenderer(IServiceProvider serviceProvider, ILoggerFactory loggerFactory) : base(serviceProvider, loggerFactory) { @@ -81,12 +83,13 @@ internal async Task InitializeStandardComponentServicesAsync( HttpContext httpContext, [DynamicallyAccessedMembers(Component)] Type? componentType = null, string? handler = null, - IFormCollection? form = null) + IFormCollection? form = null, + bool allowRenderingDuringPendingNavigation = false) { var navigationManager = httpContext.RequestServices.GetRequiredService(); ((IHostEnvironmentNavigationManager)navigationManager)?.Initialize( - GetContextBaseUri(httpContext.Request), - GetFullUri(httpContext.Request), + GetContextBaseUri(httpContext.Request), + GetFullUri(httpContext.Request), uri => GetErrorHandledTask(OnNavigateTo(uri))); navigationManager?.OnNotFound += (sender, args) => NotFoundEventArgs = args; @@ -132,7 +135,13 @@ internal async Task InitializeStandardComponentServicesAsync( { // Saving RouteData to avoid routing twice in Router component var routingStateProvider = httpContext.RequestServices.GetRequiredService(); - routingStateProvider.RouteData = new RouteData(componentType, httpContext.GetRouteData().Values); + var routeValues = new RouteValueDictionary(httpContext.GetRouteData().Values); + if (allowRenderingDuringPendingNavigation) + { + routeValues[AllowRenderDuringPendingNavigationKey] = true; + } + + routingStateProvider.RouteData = new RouteData(componentType, routeValues); if (httpContext.GetEndpoint() is RouteEndpoint routeEndpoint) { routingStateProvider.RouteData.Template = routeEndpoint.RoutePattern.RawText; From e97518aaed80519b111e2426e7ee650ca57a4b29 Mon Sep 17 00:00:00 2001 From: Ilona Tomkowicz Date: Thu, 23 Oct 2025 13:06:48 +0200 Subject: [PATCH 10/11] Move contants to shared file. --- .../src/Microsoft.AspNetCore.Components.csproj | 1 + src/Components/Components/src/Routing/RouteTable.cs | 2 +- src/Components/Components/src/Routing/Router.cs | 6 ++---- .../src/Microsoft.AspNetCore.Components.Endpoints.csproj | 1 + .../Endpoints/src/Rendering/EndpointHtmlRenderer.cs | 4 +--- src/Shared/Components/ComponentsConstants.cs | 9 +++++++++ 6 files changed, 15 insertions(+), 8 deletions(-) create mode 100644 src/Shared/Components/ComponentsConstants.cs diff --git a/src/Components/Components/src/Microsoft.AspNetCore.Components.csproj b/src/Components/Components/src/Microsoft.AspNetCore.Components.csproj index f165e6f82489..c8cf75f4714a 100644 --- a/src/Components/Components/src/Microsoft.AspNetCore.Components.csproj +++ b/src/Components/Components/src/Microsoft.AspNetCore.Components.csproj @@ -26,6 +26,7 @@ + diff --git a/src/Components/Components/src/Routing/RouteTable.cs b/src/Components/Components/src/Routing/RouteTable.cs index f8ee7649d626..fe5bc0acf67e 100644 --- a/src/Components/Components/src/Routing/RouteTable.cs +++ b/src/Components/Components/src/Routing/RouteTable.cs @@ -78,7 +78,7 @@ public void Route(RouteContext routeContext) private static void ProcessParameters(InboundRouteEntry entry, RouteValueDictionary routeValues) { - routeValues.Remove(Router.AllowRenderDuringPendingNavigationKey); + routeValues.Remove(ComponentsConstants.AllowRenderDuringPendingNavigationKey); // Add null values for unused route parameters. if (entry.UnusedRouteParameterNames != null) diff --git a/src/Components/Components/src/Routing/Router.cs b/src/Components/Components/src/Routing/Router.cs index ab29da22a08e..df7154f86303 100644 --- a/src/Components/Components/src/Routing/Router.cs +++ b/src/Components/Components/src/Routing/Router.cs @@ -42,8 +42,6 @@ static readonly IReadOnlyDictionary _emptyParametersDictionary private bool _onNavigateCalled; - internal const string AllowRenderDuringPendingNavigationKey = "__BlazorAllowRenderDuringPendingNavigation"; - [Inject] private NavigationManager NavigationManager { get; set; } [Inject] private INavigationInterception NavigationInterception { get; set; } @@ -319,9 +317,9 @@ internal virtual void Refresh(bool isNavigationIntercepted) private static bool TryConsumeAllowRenderDuringPendingNavigation(RouteData? routeData) { - if (routeData?.RouteValues.TryGetValue(AllowRenderDuringPendingNavigationKey, out var value) == true && value is true) + if (routeData?.RouteValues.TryGetValue(ComponentsConstants.AllowRenderDuringPendingNavigationKey, out var value) == true && value is true) { - (routeData.RouteValues as IDictionary)?.Remove(AllowRenderDuringPendingNavigationKey); + (routeData.RouteValues as IDictionary)?.Remove(ComponentsConstants.AllowRenderDuringPendingNavigationKey); return true; } diff --git a/src/Components/Endpoints/src/Microsoft.AspNetCore.Components.Endpoints.csproj b/src/Components/Endpoints/src/Microsoft.AspNetCore.Components.Endpoints.csproj index de0329c94e63..bce74f5aebdf 100644 --- a/src/Components/Endpoints/src/Microsoft.AspNetCore.Components.Endpoints.csproj +++ b/src/Components/Endpoints/src/Microsoft.AspNetCore.Components.Endpoints.csproj @@ -33,6 +33,7 @@ + diff --git a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.cs b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.cs index c51b2d30f14c..08fa3c39950f 100644 --- a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.cs +++ b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.cs @@ -54,8 +54,6 @@ internal partial class EndpointHtmlRenderer : StaticHtmlRenderer, IComponentPrer private string _notFoundUrl = string.Empty; - private const string AllowRenderDuringPendingNavigationKey = "__BlazorAllowRenderDuringPendingNavigation"; - public EndpointHtmlRenderer(IServiceProvider serviceProvider, ILoggerFactory loggerFactory) : base(serviceProvider, loggerFactory) { @@ -138,7 +136,7 @@ internal async Task InitializeStandardComponentServicesAsync( var routeValues = new RouteValueDictionary(httpContext.GetRouteData().Values); if (allowRenderingDuringPendingNavigation) { - routeValues[AllowRenderDuringPendingNavigationKey] = true; + routeValues[ComponentsConstants.AllowRenderDuringPendingNavigationKey] = true; } routingStateProvider.RouteData = new RouteData(componentType, routeValues); diff --git a/src/Shared/Components/ComponentsConstants.cs b/src/Shared/Components/ComponentsConstants.cs new file mode 100644 index 000000000000..1db716a22f10 --- /dev/null +++ b/src/Shared/Components/ComponentsConstants.cs @@ -0,0 +1,9 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.AspNetCore.Components; + +internal static class ComponentsConstants +{ + internal const string AllowRenderDuringPendingNavigationKey = "__BlazorAllowRenderDuringPendingNavigation"; +} From dd17b60f1ce656f21a6958d3bab29e41ea647981 Mon Sep 17 00:00:00 2001 From: Ilona Tomkowicz Date: Thu, 23 Oct 2025 13:07:13 +0200 Subject: [PATCH 11/11] Remove useless directives. --- .../test/E2ETest/ServerRenderingTests/NoInteractivityTest.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/Components/test/E2ETest/ServerRenderingTests/NoInteractivityTest.cs b/src/Components/test/E2ETest/ServerRenderingTests/NoInteractivityTest.cs index 811d7e3da138..af34a1845052 100644 --- a/src/Components/test/E2ETest/ServerRenderingTests/NoInteractivityTest.cs +++ b/src/Components/test/E2ETest/ServerRenderingTests/NoInteractivityTest.cs @@ -229,7 +229,7 @@ public void NotFoundSetOnInitialization_ResponseNotStarted_SSR(bool hasReExecuti [InlineData(false, true)] [InlineData(false, false)] // This tests the application subscribing to OnNotFound event and setting NotFoundEventArgs.Path, opposed to the framework doing it for the app. - public void NotFoundSetOnInitialization_ApplicationSubscribesToNotFoundEventToSetNotFoundPath_SSR (bool streaming, bool customRouter) + public void NotFoundSetOnInitialization_ApplicationSubscribesToNotFoundEventToSetNotFoundPath_SSR(bool streaming, bool customRouter) { string streamingPath = streaming ? "-streaming" : ""; string testUrl = $"{ServerPathBase}/set-not-found-ssr{streamingPath}?useCustomRouter={customRouter}&appSetsEventArgsPath=true"; @@ -471,6 +471,3 @@ public void StatusCodePagesWithReExecution() Browser.Equal("Re-executed page", () => Browser.Title); } } - -#pragma warning restore RS0037 // PublicAPI files must include '#nullable enable' -#pragma warning restore RS0016 // Add public types and members to the declared API