Skip to content
Open
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
0317ecf
Fix
ilonatommy Oct 14, 2025
d662028
Merge branch 'main' into fix-63933
ilonatommy Oct 15, 2025
c90449b
Update src/Components/test/testassets/Components.TestServer/RazorComp…
ilonatommy Oct 15, 2025
64df78f
Update src/Components/test/E2ETest/ServerRenderingTests/NoInteractivi…
ilonatommy Oct 15, 2025
5ed75df
Cleanup.
ilonatommy Oct 15, 2025
b8856ad
More cleanup.
ilonatommy Oct 15, 2025
d444c4a
Merge branch 'main' into fix-63933
ilonatommy Oct 16, 2025
5f632fe
Prevent duplicate force-load navigations to keep the history intact.
ilonatommy Oct 20, 2025
0f0cf6a
Add a better test for https://github.com/dotnet/aspnetcore/pull/24225…
ilonatommy Oct 21, 2025
f2fb811
Revert Router changes + fix BrowserNavigationToNotExistingPath_WithOn…
ilonatommy Oct 22, 2025
496d430
Allow Router to bypass OnNavigateAsync guard during 404 re-execution.
ilonatommy Oct 22, 2025
e97518a
Move contants to shared file.
ilonatommy Oct 23, 2025
dd17b60
Remove useless directives.
ilonatommy Oct 23, 2025
43038f3
Avoid redundant key removal.
ilonatommy Nov 10, 2025
a8f44f9
Remove the flag + use the `PageType` to detect if it's safe to render…
ilonatommy Nov 14, 2025
9f63c29
Make the comment more general
ilonatommy Nov 14, 2025
15ee674
Revert changes to SSR.
ilonatommy Nov 14, 2025
288a6c3
Inform router that quiescence includes waiting for OnNavigateAsync task.
ilonatommy Nov 18, 2025
1293d10
Cleanup.
ilonatommy Nov 18, 2025
edd8ae6
Feedback.
ilonatommy Nov 18, 2025
5116f8b
Feedback: cleanup tests.
ilonatommy Nov 20, 2025
9bae2fc
Redundant comment.
ilonatommy Nov 20, 2025
6ca29f0
Add test for not found page with streaming + clean the code.
ilonatommy Nov 20, 2025
ff67979
Move the re-execution configuration to the correct startup file.
ilonatommy Nov 20, 2025
a5fed5c
Cleanup.
ilonatommy Nov 20, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/Components/Components/src/Routing/RouteTable.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down
20 changes: 18 additions & 2 deletions src/Components/Components/src/Routing/Router.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ static readonly IReadOnlyDictionary<string, object> _emptyParametersDictionary

private bool _onNavigateCalled;

internal const string AllowRenderDuringPendingNavigationKey = "__BlazorAllowRenderDuringPendingNavigation";

[Inject] private NavigationManager NavigationManager { get; set; }

[Inject] private INavigationInterception NavigationInterception { get; set; }
Expand Down Expand Up @@ -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)
{
Expand All @@ -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);

Expand Down Expand Up @@ -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<string, object?>)?.Remove(AllowRenderDuringPendingNavigationKey);
return true;
}

return false;
}

private ComponentsActivityHandle RecordDiagnostics(string componentType, string template)
{
ComponentsActivityHandle activityHandle = default;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
17 changes: 13 additions & 4 deletions src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down Expand Up @@ -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<NavigationManager>();
((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;
Expand Down Expand Up @@ -132,7 +135,13 @@ internal async Task InitializeStandardComponentServicesAsync(
{
// Saving RouteData to avoid routing twice in Router component
var routingStateProvider = httpContext.RequestServices.GetRequiredService<EndpointRoutingStateProvider>();
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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System.Net.Http;
using System;
using Components.TestServer.RazorComponents;
using Microsoft.AspNetCore.Components.E2ETest;
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure;
Expand Down Expand Up @@ -125,6 +126,42 @@ 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);
}
}

[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);

Expand Down Expand Up @@ -434,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
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -17,8 +21,12 @@
[SupplyParameterFromQuery(Name = "appSetsEventArgsPath")]
public bool AppSetsEventArgsPath { get; set; }

private const string UseOnNavigateAsyncSwitchName = "Components.TestServer.RazorComponents.UseOnNavigateAsync";

private Type? NotFoundPageType { get; set; }
private NavigationManager _navigationManager = default!;
private bool ShouldDelayOnNavigateAsync =>
AppContext.TryGetSwitch(UseOnNavigateAsyncSwitchName, out var switchEnabled) && switchEnabled;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don' abuse an appcontextswitch for a test. Follow the pattern in other tests, like using a query string.


[Inject]
private NavigationManager NavigationManager
Expand Down Expand Up @@ -70,6 +78,26 @@
_navigationManager.OnNotFound -= OnNotFoundEvent;
}
}

private Task HandleOnNavigateAsync(NavigationContext args)
{
if (NavigationCompletionTracker.TryGetGuardTask(args.Path, out var guardTask))
{
return guardTask;
}

if (!ShouldDelayOnNavigateAsync)
{
return Task.CompletedTask;
}

return PerformOnNavigateAsyncWork();
}

private async Task PerformOnNavigateAsyncWork()
{
await Task.Yield();
}
Comment on lines +89 to +95
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return PerformOnNavigateAsyncWork();
}
private async Task PerformOnNavigateAsyncWork()
{
await Task.Yield();
}
return Task.Yield();
}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why?

Cannot implicitly convert type 'System.Runtime.CompilerServices.YieldAwaitable' to 'System.Threading.Tasks.Task'

}

<!DOCTYPE html>
Expand All @@ -93,7 +121,7 @@
{
@if (NotFoundPageType is not null)
{
<Router AppAssembly="@typeof(App).Assembly" AdditionalAssemblies="new[] { typeof(TestContentPackage.NotFound.NotFoundPage).Assembly }" NotFoundPage="NotFoundPageType">
<Router AppAssembly="@typeof(App).Assembly" AdditionalAssemblies="new[] { typeof(TestContentPackage.NotFound.NotFoundPage).Assembly }" NotFoundPage="NotFoundPageType" OnNavigateAsync="HandleOnNavigateAsync">
<Found Context="routeData">
<RouteView RouteData="@routeData" />
<FocusOnNavigate RouteData="@routeData" Selector="[data-focus-on-navigate]" />
Expand All @@ -102,7 +130,7 @@
}
else
{
<Router AppAssembly="@typeof(App).Assembly" AdditionalAssemblies="new[] { typeof(TestContentPackage.NotFound.NotFoundPage).Assembly }">
<Router AppAssembly="@typeof(App).Assembly" AdditionalAssemblies="new[] { typeof(TestContentPackage.NotFound.NotFoundPage).Assembly }" OnNavigateAsync="HandleOnNavigateAsync">
<Found Context="routeData">
<RouteView RouteData="@routeData" />
<FocusOnNavigate RouteData="@routeData" Selector="[data-focus-on-navigate]" />
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
@page "/routing/with-lazy-assembly"
@using Components.TestServer.RazorComponents;

<h1 id="lazy-route-status">Lazy route rendered</h1>

@code
{
protected override void OnInitialized()
{
NavigationCompletionTracker.AssertNavigationCompleted();
}
}
Loading