From 11956ec3ee5dd1fe18bfec11f4e2221c8dd7e261 Mon Sep 17 00:00:00 2001 From: Ilona Tomkowicz Date: Tue, 18 Mar 2025 17:16:51 +0100 Subject: [PATCH 01/42] Add `NotFoundPage`. --- .../Components/src/PublicAPI.Unshipped.txt | 2 + .../Components/src/Routing/Router.cs | 42 ++++++++++++++++++- .../E2ETest/Tests/GlobalInteractivityTest.cs | 26 ++++++++---- .../Pages/CustomNotFoundPage.razor | 4 ++ .../Components.WasmMinimal/Routes.razor | 24 ++++++++++- 5 files changed, 89 insertions(+), 9 deletions(-) create mode 100644 src/Components/test/testassets/Components.WasmMinimal/Pages/CustomNotFoundPage.razor diff --git a/src/Components/Components/src/PublicAPI.Unshipped.txt b/src/Components/Components/src/PublicAPI.Unshipped.txt index 441f46ffe210..efb30bc093f1 100644 --- a/src/Components/Components/src/PublicAPI.Unshipped.txt +++ b/src/Components/Components/src/PublicAPI.Unshipped.txt @@ -1,6 +1,8 @@ #nullable enable Microsoft.AspNetCore.Components.NavigationManager.NotFoundEvent -> System.EventHandler! virtual Microsoft.AspNetCore.Components.NavigationManager.NotFound() -> void +Microsoft.AspNetCore.Components.Routing.Router.NotFoundPage.get -> System.Type! +Microsoft.AspNetCore.Components.Routing.Router.NotFoundPage.set -> void Microsoft.AspNetCore.Components.Infrastructure.ComponentStatePersistenceManager.ComponentStatePersistenceManager(Microsoft.Extensions.Logging.ILogger! logger, System.IServiceProvider! serviceProvider) -> void Microsoft.AspNetCore.Components.Infrastructure.ComponentStatePersistenceManager.SetPlatformRenderMode(Microsoft.AspNetCore.Components.IComponentRenderMode! renderMode) -> void Microsoft.AspNetCore.Components.Infrastructure.RegisterPersistentComponentStateServiceCollectionExtensions diff --git a/src/Components/Components/src/Routing/Router.cs b/src/Components/Components/src/Routing/Router.cs index f27226d492a9..70f8eedf4185 100644 --- a/src/Components/Components/src/Routing/Router.cs +++ b/src/Components/Components/src/Routing/Router.cs @@ -3,11 +3,13 @@ #nullable disable warnings +using System.Diagnostics.CodeAnalysis; using System.Reflection; using System.Reflection.Metadata; using System.Runtime.ExceptionServices; using Microsoft.AspNetCore.Components.HotReload; using Microsoft.AspNetCore.Components.Rendering; +using Microsoft.AspNetCore.Internal; using Microsoft.Extensions.Logging; using Microsoft.Extensions.DependencyInjection; @@ -70,6 +72,13 @@ static readonly IReadOnlyDictionary _emptyParametersDictionary [Parameter] public RenderFragment NotFound { get; set; } + /// + /// Gets or sets the page content to display when no match is found for the requested route. + /// + [Parameter] + [DynamicallyAccessedMembers(LinkerFlags.Component)] + public Type NotFoundPage { get; set; } = default!; + /// /// Gets or sets the content to display when a match is found for the requested route. /// @@ -132,6 +141,22 @@ public async Task SetParametersAsync(ParameterView parameters) throw new InvalidOperationException($"The {nameof(Router)} component requires a value for the parameter {nameof(Found)}."); } + if (NotFoundPage != null) + { + if (!typeof(IComponent).IsAssignableFrom(NotFoundPage)) + { + throw new InvalidOperationException($"The type {NotFoundPage.FullName} " + + $"does not implement {typeof(IComponent).FullName}."); + } + + var routeAttributes = NotFoundPage.GetCustomAttributes(typeof(RouteAttribute), inherit: true); + if (routeAttributes.Length == 0) + { + throw new InvalidOperationException($"The type {NotFoundPage.FullName} " + + $"does not have a {typeof(RouteAttribute).FullName} applied to it."); + } + } + if (!_onNavigateCalled) { _onNavigateCalled = true; @@ -251,7 +276,22 @@ internal virtual void Refresh(bool isNavigationIntercepted) // We did not find a Component that matches the route. // Only show the NotFound content if the application developer programatically got us here i.e we did not // intercept the navigation. In all other cases, force a browser navigation since this could be non-Blazor content. - _renderHandle.Render(NotFound ?? DefaultNotFoundContent); + _renderHandle.Render(builder => + { + if (NotFoundPage != null) + { + builder.OpenComponent(0, NotFoundPage); + builder.CloseComponent(); + } + else if (NotFound != null) + { + NotFound(builder); + } + else + { + DefaultNotFoundContent(builder); + } + }); } else { diff --git a/src/Components/test/E2ETest/Tests/GlobalInteractivityTest.cs b/src/Components/test/E2ETest/Tests/GlobalInteractivityTest.cs index 2f1f70aa7511..a7115a288619 100644 --- a/src/Components/test/E2ETest/Tests/GlobalInteractivityTest.cs +++ b/src/Components/test/E2ETest/Tests/GlobalInteractivityTest.cs @@ -22,12 +22,16 @@ public class GlobalInteractivityTest( { [Theory] - [InlineData("server", true)] - [InlineData("webassembly", true)] - [InlineData("ssr", false)] - public void CanRenderNotFoundInteractive(string renderingMode, bool isInteractive) + [InlineData("server", true, false)] + [InlineData("webassembly", true, false)] + [InlineData("server", true, true)] + [InlineData("webassembly", true, true)] + [InlineData("ssr", false, true)] + [InlineData("ssr", false, false)] + public void CanRenderNotFoundPage(string renderingMode, bool isInteractive, bool useCustomNotFoundPage) { - Navigate($"/subdir/render-not-found-{renderingMode}"); + string query = useCustomNotFoundPage ? "?useCustomNotFoundPage=true" : ""; + Navigate($"{ServerPathBase}/render-not-found-{renderingMode}{query}"); if (isInteractive) { @@ -36,8 +40,16 @@ public void CanRenderNotFoundInteractive(string renderingMode, bool isInteractiv Browser.Exists(By.Id(buttonId)).Click(); } - var bodyText = Browser.FindElement(By.TagName("body")).Text; - Assert.Contains("There's nothing here", bodyText); + if (useCustomNotFoundPage) + { + var infoText = Browser.FindElement(By.Id("test-info")).Text; + Assert.Contains("Welcome On Custom Not Found Page", infoText); + } + else + { + var bodyText = Browser.FindElement(By.TagName("body")).Text; + Assert.Contains("There's nothing here", bodyText); + } } [Fact] diff --git a/src/Components/test/testassets/Components.WasmMinimal/Pages/CustomNotFoundPage.razor b/src/Components/test/testassets/Components.WasmMinimal/Pages/CustomNotFoundPage.razor new file mode 100644 index 000000000000..fc48947d6ec2 --- /dev/null +++ b/src/Components/test/testassets/Components.WasmMinimal/Pages/CustomNotFoundPage.razor @@ -0,0 +1,4 @@ +@page "/render-custom-not-found-page" + +

Welcome On Custom Not Found Page

+

Sorry, the page you are looking for does not exist.

\ No newline at end of file diff --git a/src/Components/test/testassets/Components.WasmMinimal/Routes.razor b/src/Components/test/testassets/Components.WasmMinimal/Routes.razor index ec39646aa5a5..a0807b09bf14 100644 --- a/src/Components/test/testassets/Components.WasmMinimal/Routes.razor +++ b/src/Components/test/testassets/Components.WasmMinimal/Routes.razor @@ -1,6 +1,28 @@ @using Microsoft.AspNetCore.Components.Routing +@using Components.WasmMinimal.Pages +@inject NavigationManager NavigationManager - +@code { + [Parameter] + [SupplyParameterFromQuery(Name = "useCustomNotFoundPage")] + public string? UseCustomNotFoundPage { get; set; } + + private Type? NotFoundPageType { get; set; } + + protected override void OnParametersSet() + { + if (UseCustomNotFoundPage == "true") + { + NotFoundPageType = typeof(CustomNotFoundPage); + } + else + { + NotFoundPageType = null; + } + } +} + + From 216ec31d88e4b6c04b7f8e956212433e18f739b2 Mon Sep 17 00:00:00 2001 From: Ilona Tomkowicz Date: Wed, 19 Mar 2025 10:46:08 +0100 Subject: [PATCH 02/42] Fix rebase error. --- .../Components/src/Routing/Router.cs | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/src/Components/Components/src/Routing/Router.cs b/src/Components/Components/src/Routing/Router.cs index 70f8eedf4185..e34b33fe85c5 100644 --- a/src/Components/Components/src/Routing/Router.cs +++ b/src/Components/Components/src/Routing/Router.cs @@ -276,22 +276,7 @@ internal virtual void Refresh(bool isNavigationIntercepted) // We did not find a Component that matches the route. // Only show the NotFound content if the application developer programatically got us here i.e we did not // intercept the navigation. In all other cases, force a browser navigation since this could be non-Blazor content. - _renderHandle.Render(builder => - { - if (NotFoundPage != null) - { - builder.OpenComponent(0, NotFoundPage); - builder.CloseComponent(); - } - else if (NotFound != null) - { - NotFound(builder); - } - else - { - DefaultNotFoundContent(builder); - } - }); + _renderHandle.Render(NotFound ?? DefaultNotFoundContent); } else { @@ -367,7 +352,22 @@ private void OnNotFound(object sender, EventArgs args) if (_renderHandle.IsInitialized) { Log.DisplayingNotFound(_logger); - _renderHandle.Render(NotFound ?? DefaultNotFoundContent); + _renderHandle.Render(builder => + { + if (NotFoundPage != null) + { + builder.OpenComponent(0, NotFoundPage); + builder.CloseComponent(); + } + else if (NotFound != null) + { + NotFound(builder); + } + else + { + DefaultNotFoundContent(builder); + } + }); } } From 31689934f19d842c7c5f6693850cbbb8762247ac Mon Sep 17 00:00:00 2001 From: Ilona Tomkowicz Date: Fri, 21 Mar 2025 16:21:43 +0100 Subject: [PATCH 03/42] Draft of template changes. --- .../Pages/WeatherDetails.razor | 39 ++++++++++++++++ .../Components/Models/WeatherForecast.cs | 8 ++++ .../Components/Pages/NotFound.razor | 4 ++ .../Components/Pages/Weather.razor | 17 ++++--- .../Components/Pages/WeatherDetails.razor | 37 +++++++++++++++ .../BlazorWeb-CSharp/Components/Routes.razor | 4 +- src/ProjectTemplates/scripts/startvs.cmd | 34 ++++++++++++++ .../Templates.Tests/template-baselines.json | 45 +++++++++++++++++++ 8 files changed, 181 insertions(+), 7 deletions(-) create mode 100644 src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp.Client/Pages/WeatherDetails.razor create mode 100644 src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Models/WeatherForecast.cs create mode 100644 src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/NotFound.razor create mode 100644 src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/WeatherDetails.razor create mode 100644 src/ProjectTemplates/scripts/startvs.cmd diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp.Client/Pages/WeatherDetails.razor b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp.Client/Pages/WeatherDetails.razor new file mode 100644 index 000000000000..a1998b6c3461 --- /dev/null +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp.Client/Pages/WeatherDetails.razor @@ -0,0 +1,39 @@ +@page "/weather/details/{date}" +@*#if (UseServer && !InteractiveAtRoot) +@rendermode InteractiveAuto +##elseif (!InteractiveAtRoot) +@rendermode InteractiveWebAssembly +##endif*@ +@inject NavigationManager NavigationManager + +@if (weatherDetails == null) +{ +

Loading...

+} +else +{ +
+

Date: @weatherDetails.Date.ToShortDateString()

+

Temperature (C): @weatherDetails.TemperatureC

+

Temperature (F): @weatherDetails.TemperatureF

+

Summary: @weatherDetails.Summary

+
+} + +@code{ + [Parameter] + public string? Date { get; set; } + + private WeatherForecast? weatherDetails; + + protected override async Task OnInitializedAsync() + { + weatherDetails = null; + + // Simulate fetching data from a database + await Task.Delay(500); + + // Simulate a scenario where the details are not found + NavigationManager.NotFound(); + } +} \ No newline at end of file diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Models/WeatherForecast.cs b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Models/WeatherForecast.cs new file mode 100644 index 000000000000..45171015b25d --- /dev/null +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Models/WeatherForecast.cs @@ -0,0 +1,8 @@ +public class WeatherForecast +{ + public int Id { get; set; } + public DateOnly Date { get; set; } + public int TemperatureC { get; set; } + public string? Summary { get; set; } + public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); +} \ No newline at end of file diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/NotFound.razor b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/NotFound.razor new file mode 100644 index 000000000000..fe72f2fe199a --- /dev/null +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/NotFound.razor @@ -0,0 +1,4 @@ +@page "/not-found" + +

Not Found

+

Sorry, the content you are looking for does not exist.

\ No newline at end of file diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Weather.razor b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Weather.razor index 0d19b3e4bf3c..7b7bfe7891d0 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Weather.razor +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Weather.razor @@ -32,6 +32,13 @@ else @forecast.TemperatureC @forecast.TemperatureF @forecast.Summary + +@*#if (InteractiveAtRoot) --> + +##else + More info +##endif*@ + } @@ -54,17 +61,17 @@ else var summaries = new[] { "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" }; forecasts = Enumerable.Range(1, 5).Select(index => new WeatherForecast { + index = index, Date = startDate.AddDays(index), TemperatureC = Random.Shared.Next(-20, 55), Summary = summaries[Random.Shared.Next(summaries.Length)] }).ToArray(); } - private class WeatherForecast +@*#if (InteractiveAtRoot) --> + private void NavigateToDetails(int id) { - public DateOnly Date { get; set; } - public int TemperatureC { get; set; } - public string? Summary { get; set; } - public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); + NavigationManager.NavigateTo($"/weather/details/{id}"); } +##endif*@ } diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/WeatherDetails.razor b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/WeatherDetails.razor new file mode 100644 index 000000000000..88679bc5df3b --- /dev/null +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/WeatherDetails.razor @@ -0,0 +1,37 @@ +@page "/weather/details/{id:int}" +@*#if (!InteractiveAtRoot) --> +@attribute [StreamRendering] +##endif*@ +@inject NavigationManager NavigationManager + +@if (weatherDetails == null) +{ +

Loading...

+} +else +{ +
+

Date: @weatherDetails.Date.ToShortDateString()

+

Temperature (C): @weatherDetails.TemperatureC

+

Temperature (F): @weatherDetails.TemperatureF

+

Summary: @weatherDetails.Summary

+
+} + +@code{ + [Parameter] + public int Id { get; set; } + + private WeatherForecast? weatherDetails; + + protected override async Task OnInitializedAsync() + { + weatherDetails = null; + + // Simulate fetching data from a database + await Task.Delay(500); + + // Simulate a scenario where the details are not found + NavigationManager.NotFound(); + } +} \ No newline at end of file diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Routes.razor b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Routes.razor index fdf2fe8b427f..654ae7a121c9 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Routes.razor +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Routes.razor @@ -2,9 +2,9 @@ @using BlazorWeb_CSharp.Components.Account.Shared ##endif*@ @*#if (UseWebAssembly && !InteractiveAtRoot) - + ##else - + ##endif*@ @*#if (IndividualLocalAuth) diff --git a/src/ProjectTemplates/scripts/startvs.cmd b/src/ProjectTemplates/scripts/startvs.cmd new file mode 100644 index 000000000000..5229f91fdd6f --- /dev/null +++ b/src/ProjectTemplates/scripts/startvs.cmd @@ -0,0 +1,34 @@ +@ECHO OFF +SETLOCAL + +:: This command launches a Visual Studio solution with environment variables required to use a local version of the .NET Core SDK. + +:: This tells .NET Core to use the same dotnet.exe that build scripts use +SET DOTNET_ROOT=%~dp0\.dotnet +SET DOTNET_ROOT(x86)=%~dp0\.dotnet\x86 + +:: This tells .NET Core not to go looking for .NET Core in other places +SET DOTNET_MULTILEVEL_LOOKUP=0 + +:: Put our local dotnet.exe on PATH first so Visual Studio knows which one to use +SET PATH=%DOTNET_ROOT%;%PATH% + +SET sln=%~1 + +IF "%sln%"=="" ( + echo Error^: Expected argument ^ + echo Usage^: startvs.cmd ^ + + exit /b 1 +) + +IF NOT EXIST "%DOTNET_ROOT%\dotnet.exe" ( + echo .NET Core has not yet been installed. Run `%~dp0restore.cmd` to install tools + exit /b 1 +) + +IF "%VSINSTALLDIR%" == "" ( + start "" "%sln%" +) else ( + "%VSINSTALLDIR%\Common7\IDE\devenv.com" "%sln%" +) diff --git a/src/ProjectTemplates/test/Templates.Tests/template-baselines.json b/src/ProjectTemplates/test/Templates.Tests/template-baselines.json index dd5755ff3bbe..9f30edc93890 100644 --- a/src/ProjectTemplates/test/Templates.Tests/template-baselines.json +++ b/src/ProjectTemplates/test/Templates.Tests/template-baselines.json @@ -533,9 +533,12 @@ "Components/Layout/MainLayout.razor.css", "Components/Layout/NavMenu.razor", "Components/Layout/NavMenu.razor.css", + "Components/Models/WeatherForecast.cs", "Components/Pages/Error.razor", "Components/Pages/Home.razor", + "Components/Pages/NotFound.razor", "Components/Pages/Weather.razor", + "Components/Pages/WeatherDetails.razor", "Components/Routes.razor", "Components/_Imports.razor", "Program.cs", @@ -642,10 +645,13 @@ "Components/Layout/MainLayout.razor.css", "Components/Layout/NavMenu.razor", "Components/Layout/NavMenu.razor.css", + "Components/Models/WeatherForecast.cs", "Components/Pages/Auth.razor", "Components/Pages/Error.razor", "Components/Pages/Home.razor", + "Components/Pages/NotFound.razor", "Components/Pages/Weather.razor", + "Components/Pages/WeatherDetails.razor", "Components/Routes.razor", "Components/_Imports.razor", "Data/app.db", @@ -724,10 +730,13 @@ "Components/Layout/ReconnectModal.razor", "Components/Layout/ReconnectModal.razor.css", "Components/Layout/ReconnectModal.razor.js", + "Components/Models/WeatherForecast.cs", "Components/Pages/Counter.razor", "Components/Pages/Error.razor", "Components/Pages/Home.razor", + "Components/Pages/NotFound.razor", "Components/Pages/Weather.razor", + "Components/Pages/WeatherDetails.razor", "Properties/launchSettings.json", "wwwroot/app.css", "wwwroot/favicon.png", @@ -834,11 +843,14 @@ "Components/Layout/ReconnectModal.razor", "Components/Layout/ReconnectModal.razor.css", "Components/Layout/ReconnectModal.razor.js", + "Components/Models/WeatherForecast.cs", "Components/Pages/Auth.razor", "Components/Pages/Counter.razor", "Components/Pages/Error.razor", "Components/Pages/Home.razor", + "Components/Pages/NotFound.razor", "Components/Pages/Weather.razor", + "Components/Pages/WeatherDetails.razor", "Components/Routes.razor", "Components/_Imports.razor", "Data/app.db", @@ -955,11 +967,14 @@ "Components/Layout/ReconnectModal.razor", "Components/Layout/ReconnectModal.razor.css", "Components/Layout/ReconnectModal.razor.js", + "Components/Models/WeatherForecast.cs", "Components/Pages/Auth.razor", "Components/Pages/Counter.razor", "Components/Pages/Error.razor", "Components/Pages/Home.razor", + "Components/Pages/NotFound.razor", "Components/Pages/Weather.razor", + "Components/Pages/WeatherDetails.razor", "Components/Routes.razor", "Components/_Imports.razor", "Data/ApplicationDbContext.cs", @@ -1035,9 +1050,12 @@ "{ProjectName}/Components/Layout/MainLayout.razor.css", "{ProjectName}/Components/Layout/NavMenu.razor", "{ProjectName}/Components/Layout/NavMenu.razor.css", + "{ProjectName}/Components/Models/WeatherForecast.cs", "{ProjectName}/Components/Pages/Error.razor", "{ProjectName}/Components/Pages/Home.razor", + "{ProjectName}/Components/Pages/NotFound.razor", "{ProjectName}/Components/Pages/Weather.razor", + "{ProjectName}/Components/Pages/WeatherDetails.razor", "{ProjectName}/Properties/launchSettings.json", "{ProjectName}/wwwroot/app.css", "{ProjectName}/wwwroot/favicon.png", @@ -1154,9 +1172,12 @@ "{ProjectName}/Components/Layout/MainLayout.razor.css", "{ProjectName}/Components/Layout/NavMenu.razor", "{ProjectName}/Components/Layout/NavMenu.razor.css", + "{ProjectName}/Components/Models/WeatherForecast.cs", "{ProjectName}/Components/Pages/Error.razor", "{ProjectName}/Components/Pages/Home.razor", + "{ProjectName}/Components/Pages/NotFound.razor", "{ProjectName}/Components/Pages/Weather.razor", + "{ProjectName}/Components/Pages/WeatherDetails.razor", "{ProjectName}/Components/Routes.razor", "{ProjectName}/Components/_Imports.razor", "{ProjectName}/Data/app.db", @@ -1236,9 +1257,12 @@ "{ProjectName}/Components/Layout/ReconnectModal.razor", "{ProjectName}/Components/Layout/ReconnectModal.razor.css", "{ProjectName}/Components/Layout/ReconnectModal.razor.js", + "{ProjectName}/Components/Models/WeatherForecast.cs", "{ProjectName}/Components/Pages/Error.razor", "{ProjectName}/Components/Pages/Home.razor", + "{ProjectName}/Components/Pages/NotFound.razor", "{ProjectName}/Components/Pages/Weather.razor", + "{ProjectName}/Components/Pages/WeatherDetails.razor", "{ProjectName}/Properties/launchSettings.json", "{ProjectName}/wwwroot/app.css", "{ProjectName}/wwwroot/favicon.png", @@ -1359,9 +1383,12 @@ "{ProjectName}/Components/Layout/ReconnectModal.razor", "{ProjectName}/Components/Layout/ReconnectModal.razor.css", "{ProjectName}/Components/Layout/ReconnectModal.razor.js", + "{ProjectName}/Components/Models/WeatherForecast.cs", "{ProjectName}/Components/Pages/Error.razor", "{ProjectName}/Components/Pages/Home.razor", + "{ProjectName}/Components/Pages/NotFound.razor", "{ProjectName}/Components/Pages/Weather.razor", + "{ProjectName}/Components/Pages/WeatherDetails.razor", "{ProjectName}/Components/Routes.razor", "{ProjectName}/Components/_Imports.razor", "{ProjectName}/Data/app.db", @@ -1436,10 +1463,13 @@ "Components/Layout/ReconnectModal.razor", "Components/Layout/ReconnectModal.razor.css", "Components/Layout/ReconnectModal.razor.js", + "Components/Models/WeatherForecast.cs", "Components/Pages/Counter.razor", "Components/Pages/Error.razor", "Components/Pages/Home.razor", + "Components/Pages/NotFound.razor", "Components/Pages/Weather.razor", + "Components/Pages/WeatherDetails.razor", "Components/Routes.razor", "Components/_Imports.razor", "Program.cs", @@ -1502,9 +1532,12 @@ "{ProjectName}.Client/Layout/MainLayout.razor.css", "{ProjectName}.Client/Layout/NavMenu.razor", "{ProjectName}.Client/Layout/NavMenu.razor.css", + "{ProjectName}.Client/Models/WeatherForecast.cs", "{ProjectName}.Client/Pages/Counter.razor", "{ProjectName}.Client/Pages/Home.razor", + "{ProjectName}.Client/Pages/NotFound.razor", "{ProjectName}.Client/Pages/Weather.razor", + "{ProjectName}.Client/Pages/WeatherDetails.razor", "{ProjectName}.Client/Program.cs", "{ProjectName}.Client/Routes.razor", "{ProjectName}.Client/wwwroot/appsettings.Development.json", @@ -1580,9 +1613,12 @@ "{ProjectName}.Client/Layout/ReconnectModal.razor", "{ProjectName}.Client/Layout/ReconnectModal.razor.css", "{ProjectName}.Client/Layout/ReconnectModal.razor.js", + "{ProjectName}.Client/Models/WeatherForecast.cs", "{ProjectName}.Client/Pages/Counter.razor", "{ProjectName}.Client/Pages/Home.razor", + "{ProjectName}.Client/Pages/NotFound.razor", "{ProjectName}.Client/Pages/Weather.razor", + "{ProjectName}.Client/Pages/WeatherDetails.cs", "{ProjectName}.Client/Program.cs", "{ProjectName}.Client/Routes.razor", "{ProjectName}.Client/wwwroot/appsettings.Development.json", @@ -1872,11 +1908,14 @@ "Components/Layout/ReconnectModal.razor", "Components/Layout/ReconnectModal.razor.css", "Components/Layout/ReconnectModal.razor.js", + "Components/Models/WeatherForecast.cs", "Components/Pages/Auth.razor", "Components/Pages/Counter.razor", "Components/Pages/Error.razor", "Components/Pages/Home.razor", + "Components/Pages/NotFound.razor", "Components/Pages/Weather.razor", + "Components/Pages/WeatherDetails.razor", "Components/Routes.razor", "Components/_Imports.razor", "Data/app.db", @@ -1946,10 +1985,13 @@ "{ProjectName}.Client/Layout/NavMenu.razor", "{ProjectName}.Client/Layout/NavMenu.razor.css", "{ProjectName}.Client/{ProjectName}.Client.csproj", + "{ProjectName}.Client/Models/WeatherForecast.cs", "{ProjectName}.Client/Pages/Auth.razor", "{ProjectName}.Client/Pages/Counter.razor", "{ProjectName}.Client/Pages/Home.razor", + "{ProjectName}.Client/Pages/NotFound.razor", "{ProjectName}.Client/Pages/Weather.razor", + "{ProjectName}.Client/Pages/WeatherDetails.razor", "{ProjectName}.Client/Program.cs", "{ProjectName}.Client/RedirectToLogin.razor", "{ProjectName}.Client/Routes.razor", @@ -2073,10 +2115,13 @@ "{ProjectName}.Client/Layout/ReconnectModal.razor.css", "{ProjectName}.Client/Layout/ReconnectModal.razor.js", "{ProjectName}.Client/{ProjectName}.Client.csproj", + "{ProjectName}.Client/Models/WeatherForecast.cs", "{ProjectName}.Client/Pages/Auth.razor", "{ProjectName}.Client/Pages/Counter.razor", "{ProjectName}.Client/Pages/Home.razor", + "{ProjectName}.Client/Pages/NotFound.razor", "{ProjectName}.Client/Pages/Weather.razor", + "{ProjectName}.Client/Pages/WeatherDetails.cs", "{ProjectName}.Client/Program.cs", "{ProjectName}.Client/RedirectToLogin.razor", "{ProjectName}.Client/Routes.razor", From 067cfd24a8800e5c92bfbf9a149b900e300fc85a Mon Sep 17 00:00:00 2001 From: Ilona Tomkowicz Date: Fri, 21 Mar 2025 17:19:13 +0100 Subject: [PATCH 04/42] Typo errors. --- .../BlazorWeb-CSharp/Components/Pages/Weather.razor | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Weather.razor b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Weather.razor index 7b7bfe7891d0..bef4bdc3dd1f 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Weather.razor +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Weather.razor @@ -2,6 +2,7 @@ @*#if (!InteractiveAtRoot) --> @attribute [StreamRendering] ##endif*@ +@inject NavigationManager NavigationManager Weather @@ -61,7 +62,7 @@ else var summaries = new[] { "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" }; forecasts = Enumerable.Range(1, 5).Select(index => new WeatherForecast { - index = index, + Id = index, Date = startDate.AddDays(index), TemperatureC = Random.Shared.Next(-20, 55), Summary = summaries[Random.Shared.Next(summaries.Length)] From 6d22aad20a3cabf6143a85543115a1e1f67910c8 Mon Sep 17 00:00:00 2001 From: Ilona Tomkowicz Date: Fri, 21 Mar 2025 17:28:14 +0100 Subject: [PATCH 05/42] NavigationManager is not needed for SSR. --- .../BlazorWeb-CSharp/Components/Pages/Weather.razor | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Weather.razor b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Weather.razor index bef4bdc3dd1f..a0725c3d1d92 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Weather.razor +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Weather.razor @@ -1,8 +1,9 @@ @page "/weather" @*#if (!InteractiveAtRoot) --> @attribute [StreamRendering] -##endif*@ +##else @inject NavigationManager NavigationManager +##endif*@ Weather From 6ea70821dc202cdc7a89c7ed2ee1de927ab3daf3 Mon Sep 17 00:00:00 2001 From: Ilona Tomkowicz Date: Mon, 24 Mar 2025 12:06:41 +0100 Subject: [PATCH 06/42] Add BOM to new teamplate files. --- .../BlazorWeb-CSharp.Client/Pages/WeatherDetails.razor | 2 +- .../BlazorWeb-CSharp/Components/Pages/NotFound.razor | 2 +- .../BlazorWeb-CSharp/Components/Pages/WeatherDetails.razor | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp.Client/Pages/WeatherDetails.razor b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp.Client/Pages/WeatherDetails.razor index a1998b6c3461..e6cdfac08693 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp.Client/Pages/WeatherDetails.razor +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp.Client/Pages/WeatherDetails.razor @@ -1,4 +1,4 @@ -@page "/weather/details/{date}" +@page "/weather/details/{date}" @*#if (UseServer && !InteractiveAtRoot) @rendermode InteractiveAuto ##elseif (!InteractiveAtRoot) diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/NotFound.razor b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/NotFound.razor index fe72f2fe199a..52784945efc4 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/NotFound.razor +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/NotFound.razor @@ -1,4 +1,4 @@ -@page "/not-found" +@page "/not-found"

Not Found

Sorry, the content you are looking for does not exist.

\ No newline at end of file diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/WeatherDetails.razor b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/WeatherDetails.razor index 88679bc5df3b..a9a42e4b8a61 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/WeatherDetails.razor +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/WeatherDetails.razor @@ -1,4 +1,4 @@ -@page "/weather/details/{id:int}" +@page "/weather/details/{id:int}" @*#if (!InteractiveAtRoot) --> @attribute [StreamRendering] ##endif*@ From 25e8e66bfa8993fb42b2192d5dfd4565ab7461aa Mon Sep 17 00:00:00 2001 From: Ilona Tomkowicz Date: Mon, 24 Mar 2025 12:07:22 +0100 Subject: [PATCH 07/42] Move instead of exclude. --- .../content/BlazorWeb-CSharp/.template.config/template.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/.template.config/template.json b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/.template.config/template.json index dc632c166f1e..30f7e5388b71 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/.template.config/template.json +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/.template.config/template.json @@ -65,7 +65,9 @@ "condition": "(UseWebAssembly && InteractiveAtRoot)", "rename": { "BlazorWeb-CSharp/Components/Layout/": "./BlazorWeb-CSharp.Client/Layout/", + "BlazorWeb-CSharp/Components/Models/WeatherForecast.cs": "./BlazorWeb-CSharp.Client/Models/WeatherForecast.cs", "BlazorWeb-CSharp/Components/Pages/Home.razor": "./BlazorWeb-CSharp.Client/Pages/Home.razor", + "BlazorWeb-CSharp/Components/Pages/NotFound.razor": "./BlazorWeb-CSharp.Client/Pages/NotFound.razor", "BlazorWeb-CSharp/Components/Pages/Weather.razor": "./BlazorWeb-CSharp.Client/Pages/Weather.razor", "BlazorWeb-CSharp/Components/Routes.razor": "./BlazorWeb-CSharp.Client/Routes.razor" } @@ -90,7 +92,8 @@ { "condition": "(!(UseServer && !UseWebAssembly))", "exclude": [ - "BlazorWeb-CSharp/Components/Pages/Counter.razor" + "BlazorWeb-CSharp/Components/Pages/Counter.razor", + "BlazorWeb-CSharp/Components/Pages/WeatherDetails.razor" ] }, { From 1060b4d58459392820f512bd49accf7062f5b774 Mon Sep 17 00:00:00 2001 From: Ilona Tomkowicz Date: Mon, 24 Mar 2025 13:44:16 +0100 Subject: [PATCH 08/42] Clean up, fix tests. --- .../BlazorWeb-CSharp/.template.config/template.json | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/.template.config/template.json b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/.template.config/template.json index 30f7e5388b71..658beb043663 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/.template.config/template.json +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/.template.config/template.json @@ -70,7 +70,10 @@ "BlazorWeb-CSharp/Components/Pages/NotFound.razor": "./BlazorWeb-CSharp.Client/Pages/NotFound.razor", "BlazorWeb-CSharp/Components/Pages/Weather.razor": "./BlazorWeb-CSharp.Client/Pages/Weather.razor", "BlazorWeb-CSharp/Components/Routes.razor": "./BlazorWeb-CSharp.Client/Routes.razor" - } + }, + "exclude": [ + "BlazorWeb-CSharp/Components/Pages/WeatherDetails.razor" + ] }, { "condition": "(!UseProgramMain)", @@ -92,8 +95,7 @@ { "condition": "(!(UseServer && !UseWebAssembly))", "exclude": [ - "BlazorWeb-CSharp/Components/Pages/Counter.razor", - "BlazorWeb-CSharp/Components/Pages/WeatherDetails.razor" + "BlazorWeb-CSharp/Components/Pages/Counter.razor" ] }, { @@ -116,9 +118,12 @@ { "condition": "(!SampleContent)", "exclude": [ + "BlazorWeb-CSharp/Components/Models/WeatherForecast.*", "BlazorWeb-CSharp/Components/Pages/Auth.*", "BlazorWeb-CSharp/Components/Pages/Counter.*", + "BlazorWeb-CSharp/Components/Pages/NotFound.*", "BlazorWeb-CSharp/Components/Pages/Weather.*", + "BlazorWeb-CSharp/Components/Pages/WeatherDetails.*", "BlazorWeb-CSharp/Components/Layout/NavMenu.*", "BlazorWeb-CSharp/wwwroot/lib/**", "BlazorWeb-CSharp/wwwroot/favicon.*", From fc696c31255e67a4c7d4600089db0d2f3c8ac93d Mon Sep 17 00:00:00 2001 From: Ilona Tomkowicz Date: Mon, 24 Mar 2025 15:21:28 +0100 Subject: [PATCH 09/42] Fix --- .../content/BlazorWeb-CSharp/.template.config/template.json | 6 ++++++ .../test/Templates.Tests/template-baselines.json | 4 ++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/.template.config/template.json b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/.template.config/template.json index 658beb043663..f60e7fe0986e 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/.template.config/template.json +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/.template.config/template.json @@ -75,6 +75,12 @@ "BlazorWeb-CSharp/Components/Pages/WeatherDetails.razor" ] }, + { + "condition": "!(UseWebAssembly && InteractiveAtRoot)", + "exclude": [ + "BlazorWeb-CSharp.Client/Pages/WeatherDetails.razor" + ] + }, { "condition": "(!UseProgramMain)", "exclude": [ diff --git a/src/ProjectTemplates/test/Templates.Tests/template-baselines.json b/src/ProjectTemplates/test/Templates.Tests/template-baselines.json index 9f30edc93890..ca6c3c3d0b7e 100644 --- a/src/ProjectTemplates/test/Templates.Tests/template-baselines.json +++ b/src/ProjectTemplates/test/Templates.Tests/template-baselines.json @@ -1618,7 +1618,7 @@ "{ProjectName}.Client/Pages/Home.razor", "{ProjectName}.Client/Pages/NotFound.razor", "{ProjectName}.Client/Pages/Weather.razor", - "{ProjectName}.Client/Pages/WeatherDetails.cs", + "{ProjectName}.Client/Pages/WeatherDetails.razor", "{ProjectName}.Client/Program.cs", "{ProjectName}.Client/Routes.razor", "{ProjectName}.Client/wwwroot/appsettings.Development.json", @@ -2121,7 +2121,7 @@ "{ProjectName}.Client/Pages/Home.razor", "{ProjectName}.Client/Pages/NotFound.razor", "{ProjectName}.Client/Pages/Weather.razor", - "{ProjectName}.Client/Pages/WeatherDetails.cs", + "{ProjectName}.Client/Pages/WeatherDetails.razor", "{ProjectName}.Client/Program.cs", "{ProjectName}.Client/RedirectToLogin.razor", "{ProjectName}.Client/Routes.razor", From 44e7d8e6dd02c5f1711a45b4d2794562654e3bbb Mon Sep 17 00:00:00 2001 From: Ilona Tomkowicz Date: Tue, 25 Mar 2025 10:51:51 +0100 Subject: [PATCH 10/42] Apply smallest possible changes to templates. --- .../.template.config/template.json | 14 +------ .../Pages/WeatherDetails.razor | 39 ------------------- .../Components/Models/WeatherForecast.cs | 8 ---- .../Components/Pages/Weather.razor | 19 +++------ .../Components/Pages/WeatherDetails.razor | 37 ------------------ .../Templates.Tests/template-baselines.json | 14 ------- 6 files changed, 6 insertions(+), 125 deletions(-) delete mode 100644 src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp.Client/Pages/WeatherDetails.razor delete mode 100644 src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Models/WeatherForecast.cs delete mode 100644 src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/WeatherDetails.razor diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/.template.config/template.json b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/.template.config/template.json index f60e7fe0986e..33be40a65c2f 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/.template.config/template.json +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/.template.config/template.json @@ -65,21 +65,11 @@ "condition": "(UseWebAssembly && InteractiveAtRoot)", "rename": { "BlazorWeb-CSharp/Components/Layout/": "./BlazorWeb-CSharp.Client/Layout/", - "BlazorWeb-CSharp/Components/Models/WeatherForecast.cs": "./BlazorWeb-CSharp.Client/Models/WeatherForecast.cs", "BlazorWeb-CSharp/Components/Pages/Home.razor": "./BlazorWeb-CSharp.Client/Pages/Home.razor", "BlazorWeb-CSharp/Components/Pages/NotFound.razor": "./BlazorWeb-CSharp.Client/Pages/NotFound.razor", "BlazorWeb-CSharp/Components/Pages/Weather.razor": "./BlazorWeb-CSharp.Client/Pages/Weather.razor", "BlazorWeb-CSharp/Components/Routes.razor": "./BlazorWeb-CSharp.Client/Routes.razor" - }, - "exclude": [ - "BlazorWeb-CSharp/Components/Pages/WeatherDetails.razor" - ] - }, - { - "condition": "!(UseWebAssembly && InteractiveAtRoot)", - "exclude": [ - "BlazorWeb-CSharp.Client/Pages/WeatherDetails.razor" - ] + } }, { "condition": "(!UseProgramMain)", @@ -124,12 +114,10 @@ { "condition": "(!SampleContent)", "exclude": [ - "BlazorWeb-CSharp/Components/Models/WeatherForecast.*", "BlazorWeb-CSharp/Components/Pages/Auth.*", "BlazorWeb-CSharp/Components/Pages/Counter.*", "BlazorWeb-CSharp/Components/Pages/NotFound.*", "BlazorWeb-CSharp/Components/Pages/Weather.*", - "BlazorWeb-CSharp/Components/Pages/WeatherDetails.*", "BlazorWeb-CSharp/Components/Layout/NavMenu.*", "BlazorWeb-CSharp/wwwroot/lib/**", "BlazorWeb-CSharp/wwwroot/favicon.*", diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp.Client/Pages/WeatherDetails.razor b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp.Client/Pages/WeatherDetails.razor deleted file mode 100644 index e6cdfac08693..000000000000 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp.Client/Pages/WeatherDetails.razor +++ /dev/null @@ -1,39 +0,0 @@ -@page "/weather/details/{date}" -@*#if (UseServer && !InteractiveAtRoot) -@rendermode InteractiveAuto -##elseif (!InteractiveAtRoot) -@rendermode InteractiveWebAssembly -##endif*@ -@inject NavigationManager NavigationManager - -@if (weatherDetails == null) -{ -

Loading...

-} -else -{ -
-

Date: @weatherDetails.Date.ToShortDateString()

-

Temperature (C): @weatherDetails.TemperatureC

-

Temperature (F): @weatherDetails.TemperatureF

-

Summary: @weatherDetails.Summary

-
-} - -@code{ - [Parameter] - public string? Date { get; set; } - - private WeatherForecast? weatherDetails; - - protected override async Task OnInitializedAsync() - { - weatherDetails = null; - - // Simulate fetching data from a database - await Task.Delay(500); - - // Simulate a scenario where the details are not found - NavigationManager.NotFound(); - } -} \ No newline at end of file diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Models/WeatherForecast.cs b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Models/WeatherForecast.cs deleted file mode 100644 index 45171015b25d..000000000000 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Models/WeatherForecast.cs +++ /dev/null @@ -1,8 +0,0 @@ -public class WeatherForecast -{ - public int Id { get; set; } - public DateOnly Date { get; set; } - public int TemperatureC { get; set; } - public string? Summary { get; set; } - public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); -} \ No newline at end of file diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Weather.razor b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Weather.razor index a0725c3d1d92..0d19b3e4bf3c 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Weather.razor +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Weather.razor @@ -1,8 +1,6 @@ @page "/weather" @*#if (!InteractiveAtRoot) --> @attribute [StreamRendering] -##else -@inject NavigationManager NavigationManager ##endif*@ Weather @@ -34,13 +32,6 @@ else @forecast.TemperatureC @forecast.TemperatureF @forecast.Summary - -@*#if (InteractiveAtRoot) --> - -##else - More info -##endif*@ - } @@ -63,17 +54,17 @@ else var summaries = new[] { "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" }; forecasts = Enumerable.Range(1, 5).Select(index => new WeatherForecast { - Id = index, Date = startDate.AddDays(index), TemperatureC = Random.Shared.Next(-20, 55), Summary = summaries[Random.Shared.Next(summaries.Length)] }).ToArray(); } -@*#if (InteractiveAtRoot) --> - private void NavigateToDetails(int id) + private class WeatherForecast { - NavigationManager.NavigateTo($"/weather/details/{id}"); + public DateOnly Date { get; set; } + public int TemperatureC { get; set; } + public string? Summary { get; set; } + public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); } -##endif*@ } diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/WeatherDetails.razor b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/WeatherDetails.razor deleted file mode 100644 index a9a42e4b8a61..000000000000 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/WeatherDetails.razor +++ /dev/null @@ -1,37 +0,0 @@ -@page "/weather/details/{id:int}" -@*#if (!InteractiveAtRoot) --> -@attribute [StreamRendering] -##endif*@ -@inject NavigationManager NavigationManager - -@if (weatherDetails == null) -{ -

Loading...

-} -else -{ -
-

Date: @weatherDetails.Date.ToShortDateString()

-

Temperature (C): @weatherDetails.TemperatureC

-

Temperature (F): @weatherDetails.TemperatureF

-

Summary: @weatherDetails.Summary

-
-} - -@code{ - [Parameter] - public int Id { get; set; } - - private WeatherForecast? weatherDetails; - - protected override async Task OnInitializedAsync() - { - weatherDetails = null; - - // Simulate fetching data from a database - await Task.Delay(500); - - // Simulate a scenario where the details are not found - NavigationManager.NotFound(); - } -} \ No newline at end of file diff --git a/src/ProjectTemplates/test/Templates.Tests/template-baselines.json b/src/ProjectTemplates/test/Templates.Tests/template-baselines.json index ca6c3c3d0b7e..3b59af95aabf 100644 --- a/src/ProjectTemplates/test/Templates.Tests/template-baselines.json +++ b/src/ProjectTemplates/test/Templates.Tests/template-baselines.json @@ -533,12 +533,10 @@ "Components/Layout/MainLayout.razor.css", "Components/Layout/NavMenu.razor", "Components/Layout/NavMenu.razor.css", - "Components/Models/WeatherForecast.cs", "Components/Pages/Error.razor", "Components/Pages/Home.razor", "Components/Pages/NotFound.razor", "Components/Pages/Weather.razor", - "Components/Pages/WeatherDetails.razor", "Components/Routes.razor", "Components/_Imports.razor", "Program.cs", @@ -645,13 +643,11 @@ "Components/Layout/MainLayout.razor.css", "Components/Layout/NavMenu.razor", "Components/Layout/NavMenu.razor.css", - "Components/Models/WeatherForecast.cs", "Components/Pages/Auth.razor", "Components/Pages/Error.razor", "Components/Pages/Home.razor", "Components/Pages/NotFound.razor", "Components/Pages/Weather.razor", - "Components/Pages/WeatherDetails.razor", "Components/Routes.razor", "Components/_Imports.razor", "Data/app.db", @@ -730,13 +726,11 @@ "Components/Layout/ReconnectModal.razor", "Components/Layout/ReconnectModal.razor.css", "Components/Layout/ReconnectModal.razor.js", - "Components/Models/WeatherForecast.cs", "Components/Pages/Counter.razor", "Components/Pages/Error.razor", "Components/Pages/Home.razor", "Components/Pages/NotFound.razor", "Components/Pages/Weather.razor", - "Components/Pages/WeatherDetails.razor", "Properties/launchSettings.json", "wwwroot/app.css", "wwwroot/favicon.png", @@ -843,14 +837,12 @@ "Components/Layout/ReconnectModal.razor", "Components/Layout/ReconnectModal.razor.css", "Components/Layout/ReconnectModal.razor.js", - "Components/Models/WeatherForecast.cs", "Components/Pages/Auth.razor", "Components/Pages/Counter.razor", "Components/Pages/Error.razor", "Components/Pages/Home.razor", "Components/Pages/NotFound.razor", "Components/Pages/Weather.razor", - "Components/Pages/WeatherDetails.razor", "Components/Routes.razor", "Components/_Imports.razor", "Data/app.db", @@ -967,14 +959,12 @@ "Components/Layout/ReconnectModal.razor", "Components/Layout/ReconnectModal.razor.css", "Components/Layout/ReconnectModal.razor.js", - "Components/Models/WeatherForecast.cs", "Components/Pages/Auth.razor", "Components/Pages/Counter.razor", "Components/Pages/Error.razor", "Components/Pages/Home.razor", "Components/Pages/NotFound.razor", "Components/Pages/Weather.razor", - "Components/Pages/WeatherDetails.razor", "Components/Routes.razor", "Components/_Imports.razor", "Data/ApplicationDbContext.cs", @@ -1463,13 +1453,11 @@ "Components/Layout/ReconnectModal.razor", "Components/Layout/ReconnectModal.razor.css", "Components/Layout/ReconnectModal.razor.js", - "Components/Models/WeatherForecast.cs", "Components/Pages/Counter.razor", "Components/Pages/Error.razor", "Components/Pages/Home.razor", "Components/Pages/NotFound.razor", "Components/Pages/Weather.razor", - "Components/Pages/WeatherDetails.razor", "Components/Routes.razor", "Components/_Imports.razor", "Program.cs", @@ -1908,14 +1896,12 @@ "Components/Layout/ReconnectModal.razor", "Components/Layout/ReconnectModal.razor.css", "Components/Layout/ReconnectModal.razor.js", - "Components/Models/WeatherForecast.cs", "Components/Pages/Auth.razor", "Components/Pages/Counter.razor", "Components/Pages/Error.razor", "Components/Pages/Home.razor", "Components/Pages/NotFound.razor", "Components/Pages/Weather.razor", - "Components/Pages/WeatherDetails.razor", "Components/Routes.razor", "Components/_Imports.razor", "Data/app.db", From ae68011b2f2d2cecaefd4cb7bff903fcbb20d06f Mon Sep 17 00:00:00 2001 From: Ilona Tomkowicz Date: Tue, 25 Mar 2025 11:08:56 +0100 Subject: [PATCH 11/42] Missing changes to baseline. --- .../test/Templates.Tests/template-baselines.json | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/src/ProjectTemplates/test/Templates.Tests/template-baselines.json b/src/ProjectTemplates/test/Templates.Tests/template-baselines.json index 3b59af95aabf..51957ad95c84 100644 --- a/src/ProjectTemplates/test/Templates.Tests/template-baselines.json +++ b/src/ProjectTemplates/test/Templates.Tests/template-baselines.json @@ -1040,12 +1040,10 @@ "{ProjectName}/Components/Layout/MainLayout.razor.css", "{ProjectName}/Components/Layout/NavMenu.razor", "{ProjectName}/Components/Layout/NavMenu.razor.css", - "{ProjectName}/Components/Models/WeatherForecast.cs", "{ProjectName}/Components/Pages/Error.razor", "{ProjectName}/Components/Pages/Home.razor", "{ProjectName}/Components/Pages/NotFound.razor", "{ProjectName}/Components/Pages/Weather.razor", - "{ProjectName}/Components/Pages/WeatherDetails.razor", "{ProjectName}/Properties/launchSettings.json", "{ProjectName}/wwwroot/app.css", "{ProjectName}/wwwroot/favicon.png", @@ -1162,12 +1160,10 @@ "{ProjectName}/Components/Layout/MainLayout.razor.css", "{ProjectName}/Components/Layout/NavMenu.razor", "{ProjectName}/Components/Layout/NavMenu.razor.css", - "{ProjectName}/Components/Models/WeatherForecast.cs", "{ProjectName}/Components/Pages/Error.razor", "{ProjectName}/Components/Pages/Home.razor", "{ProjectName}/Components/Pages/NotFound.razor", "{ProjectName}/Components/Pages/Weather.razor", - "{ProjectName}/Components/Pages/WeatherDetails.razor", "{ProjectName}/Components/Routes.razor", "{ProjectName}/Components/_Imports.razor", "{ProjectName}/Data/app.db", @@ -1247,12 +1243,10 @@ "{ProjectName}/Components/Layout/ReconnectModal.razor", "{ProjectName}/Components/Layout/ReconnectModal.razor.css", "{ProjectName}/Components/Layout/ReconnectModal.razor.js", - "{ProjectName}/Components/Models/WeatherForecast.cs", "{ProjectName}/Components/Pages/Error.razor", "{ProjectName}/Components/Pages/Home.razor", "{ProjectName}/Components/Pages/NotFound.razor", "{ProjectName}/Components/Pages/Weather.razor", - "{ProjectName}/Components/Pages/WeatherDetails.razor", "{ProjectName}/Properties/launchSettings.json", "{ProjectName}/wwwroot/app.css", "{ProjectName}/wwwroot/favicon.png", @@ -1373,12 +1367,10 @@ "{ProjectName}/Components/Layout/ReconnectModal.razor", "{ProjectName}/Components/Layout/ReconnectModal.razor.css", "{ProjectName}/Components/Layout/ReconnectModal.razor.js", - "{ProjectName}/Components/Models/WeatherForecast.cs", "{ProjectName}/Components/Pages/Error.razor", "{ProjectName}/Components/Pages/Home.razor", "{ProjectName}/Components/Pages/NotFound.razor", "{ProjectName}/Components/Pages/Weather.razor", - "{ProjectName}/Components/Pages/WeatherDetails.razor", "{ProjectName}/Components/Routes.razor", "{ProjectName}/Components/_Imports.razor", "{ProjectName}/Data/app.db", @@ -1520,12 +1512,10 @@ "{ProjectName}.Client/Layout/MainLayout.razor.css", "{ProjectName}.Client/Layout/NavMenu.razor", "{ProjectName}.Client/Layout/NavMenu.razor.css", - "{ProjectName}.Client/Models/WeatherForecast.cs", "{ProjectName}.Client/Pages/Counter.razor", "{ProjectName}.Client/Pages/Home.razor", "{ProjectName}.Client/Pages/NotFound.razor", "{ProjectName}.Client/Pages/Weather.razor", - "{ProjectName}.Client/Pages/WeatherDetails.razor", "{ProjectName}.Client/Program.cs", "{ProjectName}.Client/Routes.razor", "{ProjectName}.Client/wwwroot/appsettings.Development.json", @@ -1601,12 +1591,10 @@ "{ProjectName}.Client/Layout/ReconnectModal.razor", "{ProjectName}.Client/Layout/ReconnectModal.razor.css", "{ProjectName}.Client/Layout/ReconnectModal.razor.js", - "{ProjectName}.Client/Models/WeatherForecast.cs", "{ProjectName}.Client/Pages/Counter.razor", "{ProjectName}.Client/Pages/Home.razor", "{ProjectName}.Client/Pages/NotFound.razor", "{ProjectName}.Client/Pages/Weather.razor", - "{ProjectName}.Client/Pages/WeatherDetails.razor", "{ProjectName}.Client/Program.cs", "{ProjectName}.Client/Routes.razor", "{ProjectName}.Client/wwwroot/appsettings.Development.json", @@ -1971,13 +1959,11 @@ "{ProjectName}.Client/Layout/NavMenu.razor", "{ProjectName}.Client/Layout/NavMenu.razor.css", "{ProjectName}.Client/{ProjectName}.Client.csproj", - "{ProjectName}.Client/Models/WeatherForecast.cs", "{ProjectName}.Client/Pages/Auth.razor", "{ProjectName}.Client/Pages/Counter.razor", "{ProjectName}.Client/Pages/Home.razor", "{ProjectName}.Client/Pages/NotFound.razor", "{ProjectName}.Client/Pages/Weather.razor", - "{ProjectName}.Client/Pages/WeatherDetails.razor", "{ProjectName}.Client/Program.cs", "{ProjectName}.Client/RedirectToLogin.razor", "{ProjectName}.Client/Routes.razor", @@ -2101,13 +2087,11 @@ "{ProjectName}.Client/Layout/ReconnectModal.razor.css", "{ProjectName}.Client/Layout/ReconnectModal.razor.js", "{ProjectName}.Client/{ProjectName}.Client.csproj", - "{ProjectName}.Client/Models/WeatherForecast.cs", "{ProjectName}.Client/Pages/Auth.razor", "{ProjectName}.Client/Pages/Counter.razor", "{ProjectName}.Client/Pages/Home.razor", "{ProjectName}.Client/Pages/NotFound.razor", "{ProjectName}.Client/Pages/Weather.razor", - "{ProjectName}.Client/Pages/WeatherDetails.razor", "{ProjectName}.Client/Program.cs", "{ProjectName}.Client/RedirectToLogin.razor", "{ProjectName}.Client/Routes.razor", From 39deb2b6945905c57dc95098110da81a4ccaba05 Mon Sep 17 00:00:00 2001 From: Ilona Tomkowicz Date: Tue, 25 Mar 2025 11:11:19 +0100 Subject: [PATCH 12/42] Prevent throwing. --- .../src/Rendering/EndpointHtmlRenderer.EventDispatch.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.EventDispatch.cs b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.EventDispatch.cs index 2b8455741f52..bc4590b182e0 100644 --- a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.EventDispatch.cs +++ b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.EventDispatch.cs @@ -78,7 +78,8 @@ private void SetNotFoundResponse(object? sender, EventArgs args) { if (_httpContext.Response.HasStarted) { - throw new InvalidOperationException("Cannot set a NotFound response after the response has already started."); + // We cannot set a NotFound code after the response has already started + return; } _httpContext.Response.StatusCode = StatusCodes.Status404NotFound; SignalRendererToFinishRendering(); From 6b620d5b11d15600f5c7e37870d06b477eb5981f Mon Sep 17 00:00:00 2001 From: Ilona Tomkowicz Date: Tue, 25 Mar 2025 12:46:10 +0100 Subject: [PATCH 13/42] Fix configurations without global router. --- src/Components/Components/src/NavigationManager.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/Components/Components/src/NavigationManager.cs b/src/Components/Components/src/NavigationManager.cs index 1c8c8e01563f..af4151236631 100644 --- a/src/Components/Components/src/NavigationManager.cs +++ b/src/Components/Components/src/NavigationManager.cs @@ -203,7 +203,15 @@ public virtual void Refresh(bool forceReload = false) private void NotFoundCore() { - _notFound?.Invoke(this, new EventArgs()); + if (_notFound == null) + { + // global router doesn't exist, no events were registered + NavigateTo($"{BaseUri}not-found"); + } + else + { + _notFound.Invoke(this, new EventArgs()); + } } /// From 74e3eae2307f300476110596398665563c548727 Mon Sep 17 00:00:00 2001 From: Ilona Tomkowicz Date: Wed, 26 Mar 2025 09:07:45 +0100 Subject: [PATCH 14/42] Fix "response started" scenarios. --- .../src/Rendering/EndpointHtmlRenderer.EventDispatch.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.EventDispatch.cs b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.EventDispatch.cs index bc4590b182e0..be40cce3b2d7 100644 --- a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.EventDispatch.cs +++ b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.EventDispatch.cs @@ -79,6 +79,8 @@ private void SetNotFoundResponse(object? sender, EventArgs args) if (_httpContext.Response.HasStarted) { // We cannot set a NotFound code after the response has already started + var navigationManager = _httpContext.RequestServices.GetRequiredService(); + navigationManager?.NavigateTo("/not-found"); return; } _httpContext.Response.StatusCode = StatusCodes.Status404NotFound; From e10b90c006703ffba40808e99283a41368368a73 Mon Sep 17 00:00:00 2001 From: Ilona Tomkowicz Date: Wed, 26 Mar 2025 10:01:10 +0100 Subject: [PATCH 15/42] Fix template tests. --- .../content/BlazorWeb-CSharp/.template.config/template.json | 1 - 1 file changed, 1 deletion(-) diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/.template.config/template.json b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/.template.config/template.json index 33be40a65c2f..09f28a39ffee 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/.template.config/template.json +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/.template.config/template.json @@ -116,7 +116,6 @@ "exclude": [ "BlazorWeb-CSharp/Components/Pages/Auth.*", "BlazorWeb-CSharp/Components/Pages/Counter.*", - "BlazorWeb-CSharp/Components/Pages/NotFound.*", "BlazorWeb-CSharp/Components/Pages/Weather.*", "BlazorWeb-CSharp/Components/Layout/NavMenu.*", "BlazorWeb-CSharp/wwwroot/lib/**", From b660d19529ef577a945b4e67f18a70f737a0e713 Mon Sep 17 00:00:00 2001 From: Ilona Tomkowicz Date: Wed, 26 Mar 2025 13:29:40 +0100 Subject: [PATCH 16/42] Fix baseline tests. --- .../test/Templates.Tests/template-baselines.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/ProjectTemplates/test/Templates.Tests/template-baselines.json b/src/ProjectTemplates/test/Templates.Tests/template-baselines.json index 51957ad95c84..5142ac24928e 100644 --- a/src/ProjectTemplates/test/Templates.Tests/template-baselines.json +++ b/src/ProjectTemplates/test/Templates.Tests/template-baselines.json @@ -1674,6 +1674,7 @@ "Components/Layout/MainLayout.razor.css", "Components/Pages/Error.razor", "Components/Pages/Home.razor", + "Components/Pages/NotFound.razor", "Properties/launchSettings.json", "wwwroot/app.css" ], @@ -1697,6 +1698,7 @@ "Components/Layout/ReconnectModal.razor.js", "Components/Pages/Error.razor", "Components/Pages/Home.razor", + "Components/Pages/NotFound.razor", "Properties/launchSettings.json", "wwwroot/app.css" ], @@ -1713,6 +1715,7 @@ "{ProjectName}/Program.cs", "{ProjectName}/Components/App.razor", "{ProjectName}/Components/Pages/Error.razor", + "{ProjectName}/Components/Pages/NotFound.razor", "{ProjectName}/Components/Routes.razor", "{ProjectName}/Components/_Imports.razor", "{ProjectName}/Components/Layout/MainLayout.razor", @@ -1745,6 +1748,7 @@ "{ProjectName}/Components/Layout/ReconnectModal.razor.js", "{ProjectName}/Components/Pages/Error.razor", "{ProjectName}/Components/Pages/Home.razor", + "{ProjectName}/Components/Pages/NotFound.razor", "{ProjectName}/Properties/launchSettings.json", "{ProjectName}/wwwroot/app.css", "{ProjectName}.Client/{ProjectName}.Client.csproj", @@ -1813,6 +1817,7 @@ "{ProjectName}/Components/Layout/ReconnectModal.razor.js", "{ProjectName}/Components/Pages/Error.razor", "{ProjectName}/Components/Pages/Home.razor", + "{ProjectName}/Components/Pages/NotFound.razor", "{ProjectName}/Components/Routes.razor", "{ProjectName}/Components/_Imports.razor", "{ProjectName}/Data/app.db", From d566c4b3424718c6e8775282871e0b61453869e2 Mon Sep 17 00:00:00 2001 From: Ilona Tomkowicz Date: Wed, 26 Mar 2025 16:07:38 +0100 Subject: [PATCH 17/42] This is a draft of uneffective `UseStatusCodePagesWithReExecute`, cc @javiercn. --- src/Components/Endpoints/src/RazorComponentEndpointInvoker.cs | 3 ++- .../src/Rendering/EndpointHtmlRenderer.Prerendering.cs | 2 +- .../Endpoints/src/Rendering/EndpointHtmlRenderer.Streaming.cs | 4 +++- .../Endpoints/src/Results/RazorComponentResultExecutor.cs | 3 ++- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/Components/Endpoints/src/RazorComponentEndpointInvoker.cs b/src/Components/Endpoints/src/RazorComponentEndpointInvoker.cs index 7638eda6163b..774415d8b5f2 100644 --- a/src/Components/Endpoints/src/RazorComponentEndpointInvoker.cs +++ b/src/Components/Endpoints/src/RazorComponentEndpointInvoker.cs @@ -39,11 +39,12 @@ private async Task RenderComponentCore(HttpContext context) { context.Response.ContentType = RazorComponentResultExecutor.DefaultContentType; var isErrorHandler = context.Features.Get() is not null; + var hasStatusCodePage = context.Features.Get() is not null; if (isErrorHandler) { Log.InteractivityDisabledForErrorHandling(_logger); } - _renderer.InitializeStreamingRenderingFraming(context, isErrorHandler); + _renderer.InitializeStreamingRenderingFraming(context, isErrorHandler, hasStatusCodePage); EndpointHtmlRenderer.MarkAsAllowingEnhancedNavigation(context); var endpoint = context.GetEndpoint() ?? throw new InvalidOperationException($"An endpoint must be set on the '{nameof(HttpContext)}'."); diff --git a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Prerendering.cs b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Prerendering.cs index e331fd5707c2..d30a34410521 100644 --- a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Prerendering.cs +++ b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Prerendering.cs @@ -18,7 +18,7 @@ internal partial class EndpointHtmlRenderer protected override IComponent ResolveComponentForRenderMode([DynamicallyAccessedMembers(Component)] Type componentType, int? parentComponentId, IComponentActivator componentActivator, IComponentRenderMode renderMode) { - if (_isHandlingErrors) + if (_isHandlingErrors || _hasStatusCodePage) { // Ignore the render mode boundary in error scenarios. return componentActivator.CreateInstance(componentType); diff --git a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Streaming.cs b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Streaming.cs index be1b910c6f6c..ac976dfa14d1 100644 --- a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Streaming.cs +++ b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Streaming.cs @@ -22,10 +22,12 @@ internal partial class EndpointHtmlRenderer private HashSet? _visitedComponentIdsInCurrentStreamingBatch; private string? _ssrFramingCommentMarkup; private bool _isHandlingErrors; + private bool _hasStatusCodePage; - public void InitializeStreamingRenderingFraming(HttpContext httpContext, bool isErrorHandler) + public void InitializeStreamingRenderingFraming(HttpContext httpContext, bool isErrorHandler, bool hasStatusCodePage) { _isHandlingErrors = isErrorHandler; + _hasStatusCodePage = hasStatusCodePage; if (IsProgressivelyEnhancedNavigation(httpContext.Request)) { var id = Guid.NewGuid().ToString(); diff --git a/src/Components/Endpoints/src/Results/RazorComponentResultExecutor.cs b/src/Components/Endpoints/src/Results/RazorComponentResultExecutor.cs index 8f22818c7a27..06e018ea68ff 100644 --- a/src/Components/Endpoints/src/Results/RazorComponentResultExecutor.cs +++ b/src/Components/Endpoints/src/Results/RazorComponentResultExecutor.cs @@ -48,7 +48,8 @@ private static Task RenderComponentToResponse( return endpointHtmlRenderer.Dispatcher.InvokeAsync(async () => { var isErrorHandler = httpContext.Features.Get() is not null; - endpointHtmlRenderer.InitializeStreamingRenderingFraming(httpContext, isErrorHandler); + var hasStatusCodePage = httpContext.Features.Get() is not null; + endpointHtmlRenderer.InitializeStreamingRenderingFraming(httpContext, isErrorHandler, hasStatusCodePage); EndpointHtmlRenderer.MarkAsAllowingEnhancedNavigation(httpContext); // We could pool these dictionary instances if we wanted, and possibly even the ParameterView From d41bd5b748a0def1bcb4a069f6d923cdcd96efda Mon Sep 17 00:00:00 2001 From: Ilona Tomkowicz Date: Thu, 27 Mar 2025 11:04:43 +0100 Subject: [PATCH 18/42] Update. --- .../Endpoints/src/RazorComponentEndpointInvoker.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/Components/Endpoints/src/RazorComponentEndpointInvoker.cs b/src/Components/Endpoints/src/RazorComponentEndpointInvoker.cs index 774415d8b5f2..390efe2f03fb 100644 --- a/src/Components/Endpoints/src/RazorComponentEndpointInvoker.cs +++ b/src/Components/Endpoints/src/RazorComponentEndpointInvoker.cs @@ -93,7 +93,7 @@ await _renderer.InitializeStandardComponentServicesAsync( context, rootComponent, ParameterView.Empty, - waitForQuiescence: result.IsPost || isErrorHandler); + waitForQuiescence: result.IsPost || isErrorHandler || hasStatusCodePage); Task quiesceTask; if (!result.IsPost) @@ -146,7 +146,7 @@ await _renderer.InitializeStandardComponentServicesAsync( } // Emit comment containing state. - if (!isErrorHandler) + if (!isErrorHandler && !hasStatusCodePage) { var componentStateHtmlContent = await _renderer.PrerenderPersistedStateAsync(context); componentStateHtmlContent.WriteTo(bufferWriter, HtmlEncoder.Default); @@ -161,10 +161,11 @@ await _renderer.InitializeStandardComponentServicesAsync( private async Task ValidateRequestAsync(HttpContext context, IAntiforgery? antiforgery) { var processPost = HttpMethods.IsPost(context.Request.Method) && - // Disable POST functionality during exception handling. + // Disable POST functionality during exception handling and reexecution. // The exception handler middleware will not update the request method, and we don't // want to run the form handling logic against the error page. - context.Features.Get() == null; + (context.Features.Get() == null || + context.Features.Get() == null); if (processPost) { From 005f2173137f435f462597b3181e2324adf6f795 Mon Sep 17 00:00:00 2001 From: Ilona Tomkowicz Date: Mon, 31 Mar 2025 11:42:42 +0200 Subject: [PATCH 19/42] Fix reexecution mechanism. --- .../src/RazorComponentEndpointInvoker.cs | 18 +++++- .../EndpointHtmlRenderer.EventDispatch.cs | 1 + .../EndpointHtmlRenderer.Prerendering.cs | 50 ++++++++++++++++- .../EndpointHtmlRenderer.Streaming.cs | 3 +- .../src/Rendering/EndpointHtmlRenderer.cs | 7 +++ .../Diagnostics/src/PublicAPI.Unshipped.txt | 3 + .../StatusCodePagesExtensions.cs | 55 +++++++++++++++++++ .../StatusCodePage/StatusCodePagesOptions.cs | 7 +++ 8 files changed, 141 insertions(+), 3 deletions(-) diff --git a/src/Components/Endpoints/src/RazorComponentEndpointInvoker.cs b/src/Components/Endpoints/src/RazorComponentEndpointInvoker.cs index 390efe2f03fb..fbea00326a65 100644 --- a/src/Components/Endpoints/src/RazorComponentEndpointInvoker.cs +++ b/src/Components/Endpoints/src/RazorComponentEndpointInvoker.cs @@ -45,7 +45,11 @@ private async Task RenderComponentCore(HttpContext context) Log.InteractivityDisabledForErrorHandling(_logger); } _renderer.InitializeStreamingRenderingFraming(context, isErrorHandler, hasStatusCodePage); - EndpointHtmlRenderer.MarkAsAllowingEnhancedNavigation(context); + bool avoidEditingHeaders = hasStatusCodePage && context.Response.StatusCode == StatusCodes.Status404NotFound; + if (!avoidEditingHeaders) + { + EndpointHtmlRenderer.MarkAsAllowingEnhancedNavigation(context); + } var endpoint = context.GetEndpoint() ?? throw new InvalidOperationException($"An endpoint must be set on the '{nameof(HttpContext)}'."); @@ -86,6 +90,8 @@ await _renderer.InitializeStandardComponentServicesAsync( await using var writer = new HttpResponseStreamWriter(context.Response.Body, Encoding.UTF8, defaultBufferSize, ArrayPool.Shared, ArrayPool.Shared); using var bufferWriter = new BufferedTextWriter(writer); + int originalStatusCode = context.Response.StatusCode; + // Note that we always use Static rendering mode for the top-level output from a RazorComponentResult, // because you never want to serialize the invocation of RazorComponentResultHost. Instead, that host // component takes care of switching into your desired render mode when it produces its own output. @@ -95,6 +101,16 @@ await _renderer.InitializeStandardComponentServicesAsync( ParameterView.Empty, waitForQuiescence: result.IsPost || isErrorHandler || hasStatusCodePage); + bool requresReexecution = originalStatusCode != context.Response.StatusCode && hasStatusCodePage; + if (requresReexecution) + { + // If the response is a 404, we don't want to write any content. + // This is because the 404 status code is used by the routing middleware + // to indicate that no endpoint was found for the request. + await bufferWriter.FlushAsync(); + return; + } + Task quiesceTask; if (!result.IsPost) { diff --git a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.EventDispatch.cs b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.EventDispatch.cs index be40cce3b2d7..47b9b019df11 100644 --- a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.EventDispatch.cs +++ b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.EventDispatch.cs @@ -84,6 +84,7 @@ private void SetNotFoundResponse(object? sender, EventArgs args) return; } _httpContext.Response.StatusCode = StatusCodes.Status404NotFound; + _httpContext.Response.ContentType = null; SignalRendererToFinishRendering(); } diff --git a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Prerendering.cs b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Prerendering.cs index d30a34410521..cddd8cfd3e07 100644 --- a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Prerendering.cs +++ b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Prerendering.cs @@ -8,6 +8,7 @@ using Microsoft.AspNetCore.Components.Web.HtmlRendering; using Microsoft.AspNetCore.Html; using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; using static Microsoft.AspNetCore.Internal.LinkerFlags; namespace Microsoft.AspNetCore.Components.Endpoints; @@ -15,6 +16,7 @@ namespace Microsoft.AspNetCore.Components.Endpoints; internal partial class EndpointHtmlRenderer { private static readonly object ComponentSequenceKey = new object(); + private bool stopAddingTasks; protected override IComponent ResolveComponentForRenderMode([DynamicallyAccessedMembers(Component)] Type componentType, int? parentComponentId, IComponentActivator componentActivator, IComponentRenderMode renderMode) { @@ -146,6 +148,7 @@ internal async ValueTask RenderEndpointComponen { var component = BeginRenderingComponent(rootComponentType, parameters); var result = new PrerenderedComponentHtmlContent(Dispatcher, component); + stopAddingTasks = httpContext.Response.StatusCode == StatusCodes.Status404NotFound && waitForQuiescence; await WaitForResultReady(waitForQuiescence, result); @@ -166,7 +169,52 @@ private async Task WaitForResultReady(bool waitForQuiescence, PrerenderedCompone } else if (_nonStreamingPendingTasks.Count > 0) { - await WaitForNonStreamingPendingTasks(); + if (stopAddingTasks) + { + HandleNonStreamingTasks(); + } + else + { + await WaitForNonStreamingPendingTasks(); + } + } + } + + public void HandleNonStreamingTasks() + { + if (NonStreamingPendingTasksCompletion == null) + { + // Iterate over the tasks and handle their exceptions + foreach (var task in _nonStreamingPendingTasks) + { + _ = GetErrorHandledTask(task); // Fire-and-forget with exception handling + } + + // Clear the pending tasks since we are handling them + _nonStreamingPendingTasks.Clear(); + + // Mark the tasks as completed + NonStreamingPendingTasksCompletion = Task.CompletedTask; + } + } + + private async Task GetErrorHandledTask(Task taskToHandle) + { + try + { + await taskToHandle; + } + catch (Exception ex) + { + // Ignore errors due to task cancellations. + if (!taskToHandle.IsCanceled) + { + _logger.LogError( + ex, + @"An exception occurred during non-streaming rendering. + This exception will be ignored because the response + is being discarded and the request is being re-executed."); + } } } diff --git a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Streaming.cs b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Streaming.cs index ac976dfa14d1..61e0444cbefe 100644 --- a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Streaming.cs +++ b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Streaming.cs @@ -28,7 +28,8 @@ public void InitializeStreamingRenderingFraming(HttpContext httpContext, bool is { _isHandlingErrors = isErrorHandler; _hasStatusCodePage = hasStatusCodePage; - if (IsProgressivelyEnhancedNavigation(httpContext.Request)) + bool avoidEditingHeaders = hasStatusCodePage && httpContext.Response.StatusCode == StatusCodes.Status404NotFound; + if (!avoidEditingHeaders && IsProgressivelyEnhancedNavigation(httpContext.Request)) { var id = Guid.NewGuid().ToString(); httpContext.Response.Headers.Add(_streamingRenderingFramingHeaderName, id); diff --git a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.cs b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.cs index 965858d807c0..a13e81f5f827 100644 --- a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.cs +++ b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.cs @@ -44,6 +44,7 @@ internal partial class EndpointHtmlRenderer : StaticHtmlRenderer, IComponentPrer private HttpContext _httpContext = default!; // Always set at the start of an inbound call private ResourceAssetCollection? _resourceCollection; private bool _rendererIsStopped; + private readonly ILogger _logger; // The underlying Renderer always tracks the pending tasks representing *full* quiescence, i.e., // when everything (regardless of streaming SSR) is fully complete. In this subclass we also track @@ -56,6 +57,7 @@ public EndpointHtmlRenderer(IServiceProvider serviceProvider, ILoggerFactory log { _services = serviceProvider; _options = serviceProvider.GetRequiredService>().Value; + _logger = loggerFactory.CreateLogger("Microsoft.AspNetCore.Components.RenderTree.Renderer"); } internal HttpContext? HttpContext => _httpContext; @@ -163,6 +165,11 @@ protected override ComponentState CreateComponentState(int componentId, ICompone protected override void AddPendingTask(ComponentState? componentState, Task task) { + if (stopAddingTasks) + { + return; + } + var streamRendering = componentState is null ? false : ((EndpointComponentState)componentState).StreamRendering; diff --git a/src/Middleware/Diagnostics/src/PublicAPI.Unshipped.txt b/src/Middleware/Diagnostics/src/PublicAPI.Unshipped.txt index 7dc5c58110bf..5be6ce344180 100644 --- a/src/Middleware/Diagnostics/src/PublicAPI.Unshipped.txt +++ b/src/Middleware/Diagnostics/src/PublicAPI.Unshipped.txt @@ -1 +1,4 @@ #nullable enable +Microsoft.AspNetCore.Builder.StatusCodePagesOptions.CreateScopeForErrors.get -> bool +Microsoft.AspNetCore.Builder.StatusCodePagesOptions.CreateScopeForErrors.set -> void +static Microsoft.AspNetCore.Builder.StatusCodePagesExtensions.UseStatusCodePagesWithReExecute(this Microsoft.AspNetCore.Builder.IApplicationBuilder! app, string! pathFormat, string? queryFormat = null, bool createScopeForErrors = false) -> Microsoft.AspNetCore.Builder.IApplicationBuilder! \ No newline at end of file diff --git a/src/Middleware/Diagnostics/src/StatusCodePage/StatusCodePagesExtensions.cs b/src/Middleware/Diagnostics/src/StatusCodePage/StatusCodePagesExtensions.cs index a431f35582f8..4a2804210813 100644 --- a/src/Middleware/Diagnostics/src/StatusCodePage/StatusCodePagesExtensions.cs +++ b/src/Middleware/Diagnostics/src/StatusCodePage/StatusCodePagesExtensions.cs @@ -1,11 +1,13 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Diagnostics.CodeAnalysis; using System.Globalization; using Microsoft.AspNetCore.Diagnostics; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; namespace Microsoft.AspNetCore.Builder; @@ -160,10 +162,50 @@ public static IApplicationBuilder UseStatusCodePagesWithReExecute( return app.UseStatusCodePages(CreateHandler(pathFormat, queryFormat)); } + /// + /// Adds a StatusCodePages middleware to the pipeline. Specifies that the response body should be generated by + /// re-executing the request pipeline using an alternate path. This path may contain a '{0}' placeholder of the status code. + /// + /// + /// + /// + /// Whether or not to create a new scope. + /// + [SuppressMessage("ApiDesign", "RS0026:Do not add multiple overloads with optional parameters", Justification = "Required to maintain compatibility")] + public static IApplicationBuilder UseStatusCodePagesWithReExecute( + this IApplicationBuilder app, + string pathFormat, + string? queryFormat = null, + bool createScopeForErrors = false) + { + ArgumentNullException.ThrowIfNull(app); + + // Only use this path if there's a global router (in the 'WebApplication' case). + if (app.Properties.TryGetValue(RerouteHelper.GlobalRouteBuilderKey, out var routeBuilder) && routeBuilder is not null) + { + return app.Use(next => + { + var newNext = RerouteHelper.Reroute(app, routeBuilder, next); + return new StatusCodePagesMiddleware(next, + Options.Create(new StatusCodePagesOptions() { + HandleAsync = CreateHandler(pathFormat, queryFormat, newNext), + CreateScopeForErrors = createScopeForErrors + })).Invoke; + }); + } + + return app.UseStatusCodePages(new StatusCodePagesOptions + { + HandleAsync = CreateHandler(pathFormat, queryFormat), + CreateScopeForErrors = createScopeForErrors + }); + } + private static Func CreateHandler(string pathFormat, string? queryFormat, RequestDelegate? next = null) { var handler = async (StatusCodeContext context) => { + // context.Options.CreateScopeForErrors var originalStatusCode = context.HttpContext.Response.StatusCode; var newPath = new PathString( @@ -176,6 +218,10 @@ private static Func CreateHandler(string pathFormat, st var originalQueryString = context.HttpContext.Request.QueryString; var routeValuesFeature = context.HttpContext.Features.Get(); + var oldScope = context.Options.CreateScopeForErrors ? context.HttpContext.RequestServices : null; + await using AsyncServiceScope? scope = context.Options.CreateScopeForErrors + ? context.HttpContext.RequestServices.GetRequiredService().CreateAsyncScope() // or .GetRequiredService().CreateAsyncScope() + : null; // Store the original paths so the app can check it. context.HttpContext.Features.Set(new StatusCodeReExecuteFeature() @@ -188,6 +234,11 @@ private static Func CreateHandler(string pathFormat, st RouteValues = routeValuesFeature?.RouteValues }); + if (scope.HasValue) + { + context.HttpContext.RequestServices = scope.Value.ServiceProvider; + } + // An endpoint may have already been set. Since we're going to re-invoke the middleware pipeline we need to reset // the endpoint and route values to ensure things are re-calculated. HttpExtensions.ClearEndpoint(context.HttpContext); @@ -210,6 +261,10 @@ private static Func CreateHandler(string pathFormat, st context.HttpContext.Request.QueryString = originalQueryString; context.HttpContext.Request.Path = originalPath; context.HttpContext.Features.Set(null); + if (oldScope != null) + { + context.HttpContext.RequestServices = oldScope; + } } }; diff --git a/src/Middleware/Diagnostics/src/StatusCodePage/StatusCodePagesOptions.cs b/src/Middleware/Diagnostics/src/StatusCodePage/StatusCodePagesOptions.cs index d3d946ab1763..fba436b01442 100644 --- a/src/Middleware/Diagnostics/src/StatusCodePage/StatusCodePagesOptions.cs +++ b/src/Middleware/Diagnostics/src/StatusCodePage/StatusCodePagesOptions.cs @@ -55,4 +55,11 @@ private static string BuildResponseBody(int httpStatusCode) /// The handler that generates the response body for the given . By default this produces a plain text response that includes the status code. /// public Func HandleAsync { get; set; } + + /// + /// Gets or sets whether the handler needs to create a separate scope and + /// replace it on when re-executing the request. + /// + /// The default value is . + public bool CreateScopeForErrors { get; set; } } From 8c7b6d206ce1861f8cf2ac85974bfb1526156129 Mon Sep 17 00:00:00 2001 From: Ilona Tomkowicz Date: Mon, 31 Mar 2025 12:15:42 +0200 Subject: [PATCH 20/42] Fix public API. --- src/Middleware/Diagnostics/src/PublicAPI.Unshipped.txt | 2 +- .../src/StatusCodePage/StatusCodePagesExtensions.cs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Middleware/Diagnostics/src/PublicAPI.Unshipped.txt b/src/Middleware/Diagnostics/src/PublicAPI.Unshipped.txt index 5be6ce344180..7c41a706c30c 100644 --- a/src/Middleware/Diagnostics/src/PublicAPI.Unshipped.txt +++ b/src/Middleware/Diagnostics/src/PublicAPI.Unshipped.txt @@ -1,4 +1,4 @@ #nullable enable Microsoft.AspNetCore.Builder.StatusCodePagesOptions.CreateScopeForErrors.get -> bool Microsoft.AspNetCore.Builder.StatusCodePagesOptions.CreateScopeForErrors.set -> void -static Microsoft.AspNetCore.Builder.StatusCodePagesExtensions.UseStatusCodePagesWithReExecute(this Microsoft.AspNetCore.Builder.IApplicationBuilder! app, string! pathFormat, string? queryFormat = null, bool createScopeForErrors = false) -> Microsoft.AspNetCore.Builder.IApplicationBuilder! \ No newline at end of file +static Microsoft.AspNetCore.Builder.StatusCodePagesExtensions.UseStatusCodePagesWithReExecute(this Microsoft.AspNetCore.Builder.IApplicationBuilder! app, bool createScopeForErrors, string! pathFormat, string? queryFormat = null) -> Microsoft.AspNetCore.Builder.IApplicationBuilder! \ No newline at end of file diff --git a/src/Middleware/Diagnostics/src/StatusCodePage/StatusCodePagesExtensions.cs b/src/Middleware/Diagnostics/src/StatusCodePage/StatusCodePagesExtensions.cs index 4a2804210813..e41c9367bccb 100644 --- a/src/Middleware/Diagnostics/src/StatusCodePage/StatusCodePagesExtensions.cs +++ b/src/Middleware/Diagnostics/src/StatusCodePage/StatusCodePagesExtensions.cs @@ -167,16 +167,16 @@ public static IApplicationBuilder UseStatusCodePagesWithReExecute( /// re-executing the request pipeline using an alternate path. This path may contain a '{0}' placeholder of the status code. /// /// + /// Whether or not to create a new scope. /// /// - /// Whether or not to create a new scope. /// [SuppressMessage("ApiDesign", "RS0026:Do not add multiple overloads with optional parameters", Justification = "Required to maintain compatibility")] public static IApplicationBuilder UseStatusCodePagesWithReExecute( this IApplicationBuilder app, + bool createScopeForErrors, string pathFormat, - string? queryFormat = null, - bool createScopeForErrors = false) + string? queryFormat = null) { ArgumentNullException.ThrowIfNull(app); From f876e4d58ea737e1034314b4af88851c01ddec59 Mon Sep 17 00:00:00 2001 From: Ilona Tomkowicz Date: Mon, 31 Mar 2025 15:42:17 +0200 Subject: [PATCH 21/42] Args order. --- src/Middleware/Diagnostics/src/PublicAPI.Unshipped.txt | 2 +- .../src/StatusCodePage/StatusCodePagesExtensions.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Middleware/Diagnostics/src/PublicAPI.Unshipped.txt b/src/Middleware/Diagnostics/src/PublicAPI.Unshipped.txt index 7c41a706c30c..13c61eb5eab2 100644 --- a/src/Middleware/Diagnostics/src/PublicAPI.Unshipped.txt +++ b/src/Middleware/Diagnostics/src/PublicAPI.Unshipped.txt @@ -1,4 +1,4 @@ #nullable enable Microsoft.AspNetCore.Builder.StatusCodePagesOptions.CreateScopeForErrors.get -> bool Microsoft.AspNetCore.Builder.StatusCodePagesOptions.CreateScopeForErrors.set -> void -static Microsoft.AspNetCore.Builder.StatusCodePagesExtensions.UseStatusCodePagesWithReExecute(this Microsoft.AspNetCore.Builder.IApplicationBuilder! app, bool createScopeForErrors, string! pathFormat, string? queryFormat = null) -> Microsoft.AspNetCore.Builder.IApplicationBuilder! \ No newline at end of file +static Microsoft.AspNetCore.Builder.StatusCodePagesExtensions.UseStatusCodePagesWithReExecute(this Microsoft.AspNetCore.Builder.IApplicationBuilder! app, string! pathFormat, bool createScopeForErrors, string? queryFormat = null) -> Microsoft.AspNetCore.Builder.IApplicationBuilder! \ No newline at end of file diff --git a/src/Middleware/Diagnostics/src/StatusCodePage/StatusCodePagesExtensions.cs b/src/Middleware/Diagnostics/src/StatusCodePage/StatusCodePagesExtensions.cs index e41c9367bccb..b4681c8cff15 100644 --- a/src/Middleware/Diagnostics/src/StatusCodePage/StatusCodePagesExtensions.cs +++ b/src/Middleware/Diagnostics/src/StatusCodePage/StatusCodePagesExtensions.cs @@ -167,15 +167,15 @@ public static IApplicationBuilder UseStatusCodePagesWithReExecute( /// re-executing the request pipeline using an alternate path. This path may contain a '{0}' placeholder of the status code. /// /// - /// Whether or not to create a new scope. /// + /// Whether or not to create a new scope. /// /// [SuppressMessage("ApiDesign", "RS0026:Do not add multiple overloads with optional parameters", Justification = "Required to maintain compatibility")] public static IApplicationBuilder UseStatusCodePagesWithReExecute( this IApplicationBuilder app, - bool createScopeForErrors, string pathFormat, + bool createScopeForErrors, string? queryFormat = null) { ArgumentNullException.ThrowIfNull(app); From 8744e8b02ef245f57987849f003e73a2c1c120bc Mon Sep 17 00:00:00 2001 From: Ilona Tomkowicz Date: Mon, 31 Mar 2025 18:15:28 +0200 Subject: [PATCH 22/42] Draft of test. --- .../StatusCodePagesTest.cs | 32 +++++++ .../RazorComponentEndpointsStartup.cs | 94 ++++++++++--------- .../Pages/PageThatSetsNotFound.razor | 12 +++ .../Pages/ReexecutedPage.razor | 6 ++ .../StatusCodePagesExtensions.cs | 5 +- 5 files changed, 103 insertions(+), 46 deletions(-) create mode 100644 src/Components/test/E2ETest/ServerRenderingTests/StatusCodePagesTest.cs create mode 100644 src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/PageThatSetsNotFound.razor create mode 100644 src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/ReexecutedPage.razor diff --git a/src/Components/test/E2ETest/ServerRenderingTests/StatusCodePagesTest.cs b/src/Components/test/E2ETest/ServerRenderingTests/StatusCodePagesTest.cs new file mode 100644 index 000000000000..c2edb8e670b0 --- /dev/null +++ b/src/Components/test/E2ETest/ServerRenderingTests/StatusCodePagesTest.cs @@ -0,0 +1,32 @@ +// 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.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures; +using Microsoft.AspNetCore.Components.E2ETest.Infrastructure; +using TestServer; +using Components.TestServer.RazorComponents; +using Microsoft.AspNetCore.E2ETesting; +using Xunit.Abstractions; +using OpenQA.Selenium; + +namespace Microsoft.AspNetCore.Components.E2ETests.ServerRenderingTests; + +public class StatusCodePagesTest(BrowserFixture browserFixture, BasicTestAppServerSiteFixture> serverFixture, ITestOutputHelper output) + : ServerTestBase>>(browserFixture, serverFixture, output) +{ + + [Fact] + public async Task StatusCodePagesWithReexecution() + { + Navigate($"{ServerPathBase}/set-not-found"); + + Browser.Equal("Re-executed page", () => Browser.Title); + var infoText = Browser.FindElement(By.Id("test-info")).Text; + Assert.Contains("Welcome On Page Re-executed After Not Found Event", infoText); + } +} diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsStartup.cs b/src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsStartup.cs index 9615dcf58df0..32178ef0a0dd 100644 --- a/src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsStartup.cs +++ b/src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsStartup.cs @@ -49,10 +49,6 @@ public void ConfigureServices(IServiceCollection services) services.AddScoped(); services.AddScoped(); - - - - services.AddHttpContextAccessor(); services.AddSingleton(); services.AddCascadingAuthenticationState(); @@ -77,57 +73,67 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) app.Map("/subdir", app => { - WebAssemblyTestHelper.ServeCoopHeadersIfWebAssemblyThreadingEnabled(app); - - if (!env.IsDevelopment()) + app.Map("/reexecution", reexecutionApp => { - app.UseExceptionHandler("/Error", createScopeForErrors: true); - } + reexecutionApp.UseStatusCodePagesWithReExecute("/not-found-reexecute", true); + }); - app.UseRouting(); - UseFakeAuthState(app); - app.UseAntiforgery(); + ConfigureSubdirPipeline(app, env); + }); + } + + protected virtual void ConfigureSubdirPipeline(IApplicationBuilder app, IWebHostEnvironment env) + { + WebAssemblyTestHelper.ServeCoopHeadersIfWebAssemblyThreadingEnabled(app); - app.Use((ctx, nxt) => + if (!env.IsDevelopment()) + { + app.UseExceptionHandler("/Error", createScopeForErrors: true); + } + + app.UseRouting(); + UseFakeAuthState(app); + app.UseAntiforgery(); + + app.Use((ctx, nxt) => + { + if (ctx.Request.Query.ContainsKey("add-csp")) { - if (ctx.Request.Query.ContainsKey("add-csp")) - { - ctx.Response.Headers.Add("Content-Security-Policy", "script-src 'self' 'unsafe-inline'"); - } - return nxt(); - }); + ctx.Response.Headers.Add("Content-Security-Policy", "script-src 'self' 'unsafe-inline'"); + } + return nxt(); + }); - _ = app.UseEndpoints(endpoints => + _ = app.UseEndpoints(endpoints => + { + var contentRootStaticAssetsPath = Path.Combine(env.ContentRootPath, "Components.TestServer.staticwebassets.endpoints.json"); + if (File.Exists(contentRootStaticAssetsPath)) { - var contentRootStaticAssetsPath = Path.Combine(env.ContentRootPath, "Components.TestServer.staticwebassets.endpoints.json"); - if (File.Exists(contentRootStaticAssetsPath)) - { - endpoints.MapStaticAssets(contentRootStaticAssetsPath); - } - else - { - endpoints.MapStaticAssets(); - } + endpoints.MapStaticAssets(contentRootStaticAssetsPath); + } + else + { + endpoints.MapStaticAssets(); + } - _ = endpoints.MapRazorComponents() - .AddAdditionalAssemblies(Assembly.Load("Components.WasmMinimal")) - .AddInteractiveServerRenderMode(options => - { - var config = app.ApplicationServices.GetRequiredService(); - options.DisableWebSocketCompression = config.IsCompressionDisabled; + _ = endpoints.MapRazorComponents() + .AddAdditionalAssemblies(Assembly.Load("Components.WasmMinimal")) + .AddInteractiveServerRenderMode(options => + { + var config = app.ApplicationServices.GetRequiredService(); + options.DisableWebSocketCompression = config.IsCompressionDisabled; - options.ContentSecurityFrameAncestorsPolicy = config.CspPolicy; + options.ContentSecurityFrameAncestorsPolicy = config.CspPolicy; - options.ConfigureWebSocketAcceptContext = config.ConfigureWebSocketAcceptContext; - }) - .AddInteractiveWebAssemblyRenderMode(options => options.PathPrefix = "/WasmMinimal"); + options.ConfigureWebSocketAcceptContext = config.ConfigureWebSocketAcceptContext; + }) + .AddInteractiveWebAssemblyRenderMode(options => options.PathPrefix = "/WasmMinimal"); - NotEnabledStreamingRenderingComponent.MapEndpoints(endpoints); - StreamingRenderingForm.MapEndpoints(endpoints); - InteractiveStreamingRenderingComponent.MapEndpoints(endpoints); + NotEnabledStreamingRenderingComponent.MapEndpoints(endpoints); + StreamingRenderingForm.MapEndpoints(endpoints); + InteractiveStreamingRenderingComponent.MapEndpoints(endpoints); - MapEnhancedNavigationEndpoints(endpoints); - }); + MapEnhancedNavigationEndpoints(endpoints); }); } diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/PageThatSetsNotFound.razor b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/PageThatSetsNotFound.razor new file mode 100644 index 000000000000..e4386fd3f808 --- /dev/null +++ b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/PageThatSetsNotFound.razor @@ -0,0 +1,12 @@ +@page "/reexecution/set-not-found" +@attribute [StreamRendering(false)] +@inject NavigationManager NavigationManager + +

Any content

+ +@code{ + protected override void OnInitialized() + { + NavigationManager.NotFound(); + } +} diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/ReexecutedPage.razor b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/ReexecutedPage.razor new file mode 100644 index 000000000000..8a6a3af89828 --- /dev/null +++ b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/ReexecutedPage.razor @@ -0,0 +1,6 @@ +@page "/not-found-reexecute" + +Re-executed page + +

Welcome On Page Re-executed After Not Found Event

+

This page is shown when UseStatusCodePagesWithReExecute is set and another page sensitive 404

diff --git a/src/Middleware/Diagnostics/src/StatusCodePage/StatusCodePagesExtensions.cs b/src/Middleware/Diagnostics/src/StatusCodePage/StatusCodePagesExtensions.cs index b4681c8cff15..e9b7b7d4ed15 100644 --- a/src/Middleware/Diagnostics/src/StatusCodePage/StatusCodePagesExtensions.cs +++ b/src/Middleware/Diagnostics/src/StatusCodePage/StatusCodePagesExtensions.cs @@ -194,11 +194,12 @@ public static IApplicationBuilder UseStatusCodePagesWithReExecute( }); } - return app.UseStatusCodePages(new StatusCodePagesOptions + var options = new StatusCodePagesOptions { HandleAsync = CreateHandler(pathFormat, queryFormat), CreateScopeForErrors = createScopeForErrors - }); + }; + return app.UseMiddleware(options); } private static Func CreateHandler(string pathFormat, string? queryFormat, RequestDelegate? next = null) From aee53a93d4eb96a1a173e7ed2d25bfbe83e00c46 Mon Sep 17 00:00:00 2001 From: Ilona Tomkowicz Date: Tue, 1 Apr 2025 12:55:13 +0200 Subject: [PATCH 23/42] Per page interactivity test. --- .../E2ETest/ServerRenderingTests/StatusCodePagesTest.cs | 4 ++-- .../RazorComponentEndpointsStartup.cs | 7 +++++++ .../RazorComponents/Pages/Index.razor | 1 + .../RazorComponents/Pages/ReexecutedPage.razor | 2 +- .../src/StatusCodePage/StatusCodePagesExtensions.cs | 6 +++--- 5 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/Components/test/E2ETest/ServerRenderingTests/StatusCodePagesTest.cs b/src/Components/test/E2ETest/ServerRenderingTests/StatusCodePagesTest.cs index c2edb8e670b0..85af58eab2a1 100644 --- a/src/Components/test/E2ETest/ServerRenderingTests/StatusCodePagesTest.cs +++ b/src/Components/test/E2ETest/ServerRenderingTests/StatusCodePagesTest.cs @@ -21,9 +21,9 @@ public class StatusCodePagesTest(BrowserFixture browserFixture, BasicTestAppServ { [Fact] - public async Task StatusCodePagesWithReexecution() + public void StatusCodePagesWithReexecution() { - Navigate($"{ServerPathBase}/set-not-found"); + Navigate($"{ServerPathBase}/reexecution/set-not-found"); Browser.Equal("Re-executed page", () => Browser.Title); var infoText = Browser.FindElement(By.Id("test-info")).Text; diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsStartup.cs b/src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsStartup.cs index 32178ef0a0dd..d907fccb71ff 100644 --- a/src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsStartup.cs +++ b/src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsStartup.cs @@ -76,6 +76,13 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) app.Map("/reexecution", reexecutionApp => { reexecutionApp.UseStatusCodePagesWithReExecute("/not-found-reexecute", true); + + reexecutionApp.UseRouting(); + reexecutionApp.UseAntiforgery(); + reexecutionApp.UseEndpoints(endpoints => + { + endpoints.MapRazorComponents(); + }); }); ConfigureSubdirPipeline(app, env); diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/Index.razor b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/Index.razor index 81020bd258ed..94b8e3bffd1d 100644 --- a/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/Index.razor +++ b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/Index.razor @@ -1,4 +1,5 @@ @page "/" +@page "/reexecution" Home diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/ReexecutedPage.razor b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/ReexecutedPage.razor index 8a6a3af89828..6cf5ec51481a 100644 --- a/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/ReexecutedPage.razor +++ b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/ReexecutedPage.razor @@ -3,4 +3,4 @@ Re-executed page

Welcome On Page Re-executed After Not Found Event

-

This page is shown when UseStatusCodePagesWithReExecute is set and another page sensitive 404

+

This page is shown when UseStatusCodePagesWithReExecute is set and another page sets 404

diff --git a/src/Middleware/Diagnostics/src/StatusCodePage/StatusCodePagesExtensions.cs b/src/Middleware/Diagnostics/src/StatusCodePage/StatusCodePagesExtensions.cs index e9b7b7d4ed15..6aa6843995b8 100644 --- a/src/Middleware/Diagnostics/src/StatusCodePage/StatusCodePagesExtensions.cs +++ b/src/Middleware/Diagnostics/src/StatusCodePage/StatusCodePagesExtensions.cs @@ -199,14 +199,14 @@ public static IApplicationBuilder UseStatusCodePagesWithReExecute( HandleAsync = CreateHandler(pathFormat, queryFormat), CreateScopeForErrors = createScopeForErrors }; - return app.UseMiddleware(options); + var wrappedOptions = new OptionsWrapper(options); + return app.UseMiddleware(wrappedOptions); } private static Func CreateHandler(string pathFormat, string? queryFormat, RequestDelegate? next = null) { var handler = async (StatusCodeContext context) => { - // context.Options.CreateScopeForErrors var originalStatusCode = context.HttpContext.Response.StatusCode; var newPath = new PathString( @@ -221,7 +221,7 @@ private static Func CreateHandler(string pathFormat, st var routeValuesFeature = context.HttpContext.Features.Get(); var oldScope = context.Options.CreateScopeForErrors ? context.HttpContext.RequestServices : null; await using AsyncServiceScope? scope = context.Options.CreateScopeForErrors - ? context.HttpContext.RequestServices.GetRequiredService().CreateAsyncScope() // or .GetRequiredService().CreateAsyncScope() + ? context.HttpContext.RequestServices.GetRequiredService().CreateAsyncScope() : null; // Store the original paths so the app can check it. From de91b4bfeda4d369f04b82057e5b659583a1d193 Mon Sep 17 00:00:00 2001 From: Ilona Tomkowicz Date: Tue, 1 Apr 2025 17:08:27 +0200 Subject: [PATCH 24/42] Revert unnecessary change. --- .../Components.TestServer/RazorComponents/Pages/Index.razor | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/Index.razor b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/Index.razor index 94b8e3bffd1d..81020bd258ed 100644 --- a/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/Index.razor +++ b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/Index.razor @@ -1,5 +1,4 @@ @page "/" -@page "/reexecution" Home From 7e7f1fef4d8b062d354a98a09609ab9ef2d5b225 Mon Sep 17 00:00:00 2001 From: Ilona Tomkowicz Date: Wed, 2 Apr 2025 11:33:58 +0200 Subject: [PATCH 25/42] Typo: we want to stop only if status pages are on. --- .../src/Rendering/EndpointHtmlRenderer.Prerendering.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Prerendering.cs b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Prerendering.cs index cddd8cfd3e07..9068e6fb01cf 100644 --- a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Prerendering.cs +++ b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Prerendering.cs @@ -148,7 +148,7 @@ internal async ValueTask RenderEndpointComponen { var component = BeginRenderingComponent(rootComponentType, parameters); var result = new PrerenderedComponentHtmlContent(Dispatcher, component); - stopAddingTasks = httpContext.Response.StatusCode == StatusCodes.Status404NotFound && waitForQuiescence; + stopAddingTasks = httpContext.Response.StatusCode == StatusCodes.Status404NotFound && _hasStatusCodePage; await WaitForResultReady(waitForQuiescence, result); From 6a100629dd9908806d980b667bf44a468173a4ee Mon Sep 17 00:00:00 2001 From: Ilona Tomkowicz Date: Wed, 2 Apr 2025 11:37:16 +0200 Subject: [PATCH 26/42] Remove comments. --- .../src/Rendering/EndpointHtmlRenderer.Prerendering.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Prerendering.cs b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Prerendering.cs index 9068e6fb01cf..c3038e540cc2 100644 --- a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Prerendering.cs +++ b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Prerendering.cs @@ -184,16 +184,14 @@ public void HandleNonStreamingTasks() { if (NonStreamingPendingTasksCompletion == null) { - // Iterate over the tasks and handle their exceptions foreach (var task in _nonStreamingPendingTasks) { - _ = GetErrorHandledTask(task); // Fire-and-forget with exception handling + _ = GetErrorHandledTask(task); } // Clear the pending tasks since we are handling them _nonStreamingPendingTasks.Clear(); - // Mark the tasks as completed NonStreamingPendingTasksCompletion = Task.CompletedTask; } } From 5518812c0987058568cc439b970e0b655e9f24cc Mon Sep 17 00:00:00 2001 From: Ilona Tomkowicz Date: Wed, 2 Apr 2025 13:08:35 +0200 Subject: [PATCH 27/42] Fix tests. --- src/Components/Endpoints/src/RazorComponentEndpointInvoker.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Components/Endpoints/src/RazorComponentEndpointInvoker.cs b/src/Components/Endpoints/src/RazorComponentEndpointInvoker.cs index fbea00326a65..6f951a64de38 100644 --- a/src/Components/Endpoints/src/RazorComponentEndpointInvoker.cs +++ b/src/Components/Endpoints/src/RazorComponentEndpointInvoker.cs @@ -180,8 +180,8 @@ private async Task ValidateRequestAsync(HttpContext cont // Disable POST functionality during exception handling and reexecution. // The exception handler middleware will not update the request method, and we don't // want to run the form handling logic against the error page. - (context.Features.Get() == null || - context.Features.Get() == null); + context.Features.Get() == null && + context.Features.Get() == null; if (processPost) { From c8eb629ff240bc452f61a33f1f22d38faa221c34 Mon Sep 17 00:00:00 2001 From: Ilona Tomkowicz Date: Mon, 7 Apr 2025 14:08:04 +0200 Subject: [PATCH 28/42] Feedback. --- src/Components/Components/src/NavigationManager.cs | 4 +++- .../Endpoints/src/RazorComponentEndpointInvoker.cs | 9 +++++---- .../src/Rendering/EndpointHtmlRenderer.EventDispatch.cs | 7 ++++++- .../src/Rendering/EndpointHtmlRenderer.Prerendering.cs | 6 +++--- 4 files changed, 17 insertions(+), 9 deletions(-) diff --git a/src/Components/Components/src/NavigationManager.cs b/src/Components/Components/src/NavigationManager.cs index 6a622c5aca1a..1cb4359871c5 100644 --- a/src/Components/Components/src/NavigationManager.cs +++ b/src/Components/Components/src/NavigationManager.cs @@ -54,6 +54,8 @@ public event EventHandler OnNotFound private EventHandler? _notFound; + private static readonly NotFoundEventArgs _notFoundEventArgs = new NotFoundEventArgs(); + // For the baseUri it's worth storing as a System.Uri so we can do operations // on that type. System.Uri gives us access to the original string anyway. private Uri? _baseUri; @@ -210,7 +212,7 @@ private void NotFoundCore() } else { - _notFound.Invoke(this, new NotFoundEventArgs()); + _notFound.Invoke(this, _notFoundEventArgs); } } diff --git a/src/Components/Endpoints/src/RazorComponentEndpointInvoker.cs b/src/Components/Endpoints/src/RazorComponentEndpointInvoker.cs index 6f951a64de38..fce7b9495ef0 100644 --- a/src/Components/Endpoints/src/RazorComponentEndpointInvoker.cs +++ b/src/Components/Endpoints/src/RazorComponentEndpointInvoker.cs @@ -91,6 +91,7 @@ await _renderer.InitializeStandardComponentServicesAsync( using var bufferWriter = new BufferedTextWriter(writer); int originalStatusCode = context.Response.StatusCode; + bool isErrorHandlerOrHasStatusCodePage = isErrorHandler || hasStatusCodePage; // Note that we always use Static rendering mode for the top-level output from a RazorComponentResult, // because you never want to serialize the invocation of RazorComponentResultHost. Instead, that host @@ -99,10 +100,10 @@ await _renderer.InitializeStandardComponentServicesAsync( context, rootComponent, ParameterView.Empty, - waitForQuiescence: result.IsPost || isErrorHandler || hasStatusCodePage); + waitForQuiescence: result.IsPost || isErrorHandlerOrHasStatusCodePage); - bool requresReexecution = originalStatusCode != context.Response.StatusCode && hasStatusCodePage; - if (requresReexecution) + bool requiresReexecution = originalStatusCode != context.Response.StatusCode && hasStatusCodePage; + if (requiresReexecution) { // If the response is a 404, we don't want to write any content. // This is because the 404 status code is used by the routing middleware @@ -162,7 +163,7 @@ await _renderer.InitializeStandardComponentServicesAsync( } // Emit comment containing state. - if (!isErrorHandler && !hasStatusCodePage) + if (!isErrorHandlerOrHasStatusCodePage) { var componentStateHtmlContent = await _renderer.PrerenderPersistedStateAsync(context); componentStateHtmlContent.WriteTo(bufferWriter, HtmlEncoder.Default); diff --git a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.EventDispatch.cs b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.EventDispatch.cs index 47b9b019df11..086cde0cd88b 100644 --- a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.EventDispatch.cs +++ b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.EventDispatch.cs @@ -80,7 +80,12 @@ private void SetNotFoundResponse(object? sender, EventArgs args) { // We cannot set a NotFound code after the response has already started var navigationManager = _httpContext.RequestServices.GetRequiredService(); - navigationManager?.NavigateTo("/not-found"); + if (navigationManager is null) + { + throw new InvalidOperationException("The NavigationManager service is not available. Cannot navigate to a 404 page."); + } + var notFoundUri = $"{navigationManager.BaseUri}not-found"; + navigationManager.NavigateTo(notFoundUri); return; } _httpContext.Response.StatusCode = StatusCodes.Status404NotFound; diff --git a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Prerendering.cs b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Prerendering.cs index c3038e540cc2..3714207e5957 100644 --- a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Prerendering.cs +++ b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Prerendering.cs @@ -209,9 +209,9 @@ private async Task GetErrorHandledTask(Task taskToHandle) { _logger.LogError( ex, - @"An exception occurred during non-streaming rendering. - This exception will be ignored because the response - is being discarded and the request is being re-executed."); + "An exception occurred during non-streaming rendering. " + + "This exception will be ignored because the response " + + "is being discarded and the request is being re-executed."); } } } From 66b563c0d568c19b43a9992122565d77becf4b8d Mon Sep 17 00:00:00 2001 From: Ilona Tomkowicz Date: Mon, 7 Apr 2025 16:03:05 +0200 Subject: [PATCH 29/42] Feedback. --- .../Endpoints/src/RazorComponentEndpointInvoker.cs | 1 - .../src/Rendering/EndpointHtmlRenderer.EventDispatch.cs | 4 ---- .../src/Rendering/EndpointHtmlRenderer.Prerendering.cs | 6 +++--- .../Endpoints/src/Rendering/EndpointHtmlRenderer.cs | 2 +- .../Components.TestServer/RazorComponentEndpointsStartup.cs | 2 +- 5 files changed, 5 insertions(+), 10 deletions(-) diff --git a/src/Components/Endpoints/src/RazorComponentEndpointInvoker.cs b/src/Components/Endpoints/src/RazorComponentEndpointInvoker.cs index fce7b9495ef0..e188f3d6fc2a 100644 --- a/src/Components/Endpoints/src/RazorComponentEndpointInvoker.cs +++ b/src/Components/Endpoints/src/RazorComponentEndpointInvoker.cs @@ -108,7 +108,6 @@ await _renderer.InitializeStandardComponentServicesAsync( // If the response is a 404, we don't want to write any content. // This is because the 404 status code is used by the routing middleware // to indicate that no endpoint was found for the request. - await bufferWriter.FlushAsync(); return; } diff --git a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.EventDispatch.cs b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.EventDispatch.cs index 086cde0cd88b..406513f7d44f 100644 --- a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.EventDispatch.cs +++ b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.EventDispatch.cs @@ -80,10 +80,6 @@ private void SetNotFoundResponse(object? sender, EventArgs args) { // We cannot set a NotFound code after the response has already started var navigationManager = _httpContext.RequestServices.GetRequiredService(); - if (navigationManager is null) - { - throw new InvalidOperationException("The NavigationManager service is not available. Cannot navigate to a 404 page."); - } var notFoundUri = $"{navigationManager.BaseUri}not-found"; navigationManager.NavigateTo(notFoundUri); return; diff --git a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Prerendering.cs b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Prerendering.cs index 3714207e5957..08ce762927dc 100644 --- a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Prerendering.cs +++ b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Prerendering.cs @@ -16,7 +16,7 @@ namespace Microsoft.AspNetCore.Components.Endpoints; internal partial class EndpointHtmlRenderer { private static readonly object ComponentSequenceKey = new object(); - private bool stopAddingTasks; + private bool _isHandlingNotFound; protected override IComponent ResolveComponentForRenderMode([DynamicallyAccessedMembers(Component)] Type componentType, int? parentComponentId, IComponentActivator componentActivator, IComponentRenderMode renderMode) { @@ -148,7 +148,7 @@ internal async ValueTask RenderEndpointComponen { var component = BeginRenderingComponent(rootComponentType, parameters); var result = new PrerenderedComponentHtmlContent(Dispatcher, component); - stopAddingTasks = httpContext.Response.StatusCode == StatusCodes.Status404NotFound && _hasStatusCodePage; + _isHandlingNotFound = httpContext.Response.StatusCode == StatusCodes.Status404NotFound && _hasStatusCodePage; await WaitForResultReady(waitForQuiescence, result); @@ -169,7 +169,7 @@ private async Task WaitForResultReady(bool waitForQuiescence, PrerenderedCompone } else if (_nonStreamingPendingTasks.Count > 0) { - if (stopAddingTasks) + if (_isHandlingNotFound) { HandleNonStreamingTasks(); } diff --git a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.cs b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.cs index a13e81f5f827..496cc27f819e 100644 --- a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.cs +++ b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.cs @@ -165,7 +165,7 @@ protected override ComponentState CreateComponentState(int componentId, ICompone protected override void AddPendingTask(ComponentState? componentState, Task task) { - if (stopAddingTasks) + if (_isHandlingNotFound) { return; } diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsStartup.cs b/src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsStartup.cs index d907fccb71ff..f91db9aa4ee3 100644 --- a/src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsStartup.cs +++ b/src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsStartup.cs @@ -75,7 +75,7 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { app.Map("/reexecution", reexecutionApp => { - reexecutionApp.UseStatusCodePagesWithReExecute("/not-found-reexecute", true); + reexecutionApp.UseStatusCodePagesWithReExecute("/not-found-reexecute", createScopeForErrors: true); reexecutionApp.UseRouting(); reexecutionApp.UseAntiforgery(); From 1da92f999c91a7f37d050b930cd085de5d9033c0 Mon Sep 17 00:00:00 2001 From: Ilona Tomkowicz Date: Mon, 7 Apr 2025 17:15:15 +0200 Subject: [PATCH 30/42] Failing test - re-executed without a reason. --- .../ServerRenderingTests/StatusCodePagesTest.cs | 15 ++++++++++----- .../Pages/PageThatSetsNotFound.razor | 11 ++++++++++- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/src/Components/test/E2ETest/ServerRenderingTests/StatusCodePagesTest.cs b/src/Components/test/E2ETest/ServerRenderingTests/StatusCodePagesTest.cs index 85af58eab2a1..5b6d70bc7cf2 100644 --- a/src/Components/test/E2ETest/ServerRenderingTests/StatusCodePagesTest.cs +++ b/src/Components/test/E2ETest/ServerRenderingTests/StatusCodePagesTest.cs @@ -20,13 +20,18 @@ public class StatusCodePagesTest(BrowserFixture browserFixture, BasicTestAppServ : ServerTestBase>>(browserFixture, serverFixture, output) { - [Fact] - public void StatusCodePagesWithReexecution() + [Theory] + [InlineData(true)] + [InlineData(false)] + public void StatusCodePagesWithReexecution(bool setNotFound) { - Navigate($"{ServerPathBase}/reexecution/set-not-found"); + Navigate($"{ServerPathBase}/reexecution/set-not-found?shouldSet={setNotFound}"); - Browser.Equal("Re-executed page", () => Browser.Title); + string expectedTitle = setNotFound ? "Re-executed page" : "Original page"; + Browser.Equal(expectedTitle, () => Browser.Title); var infoText = Browser.FindElement(By.Id("test-info")).Text; - Assert.Contains("Welcome On Page Re-executed After Not Found Event", infoText); + string expectedInfoText = setNotFound ? "Welcome On Page Re-executed After Not Found Event" : "Any content"; + Assert.Contains(expectedInfoText, infoText); } + } diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/PageThatSetsNotFound.razor b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/PageThatSetsNotFound.razor index e4386fd3f808..1afedff2f2da 100644 --- a/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/PageThatSetsNotFound.razor +++ b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/PageThatSetsNotFound.razor @@ -2,11 +2,20 @@ @attribute [StreamRendering(false)] @inject NavigationManager NavigationManager +Original page +

Any content

@code{ + [Parameter] + [SupplyParameterFromQuery(Name = "shouldSet")] + private bool ShouldSet { get; set; } = true; + protected override void OnInitialized() { - NavigationManager.NotFound(); + if (ShouldSet) + { + NavigationManager.NotFound(); + } } } From b7775ef1a4a6d67cf709cea1fc23da09efb60e9b Mon Sep 17 00:00:00 2001 From: Ilona Tomkowicz Date: Tue, 8 Apr 2025 12:49:58 +0200 Subject: [PATCH 31/42] Add streaming test after response started. --- .../ServerRenderingTests/NoInteractivityTest.cs | 9 +++++++++ .../RazorComponents/Pages/NotFoundPage.razor | 5 +++++ .../StreamingRendering/StreamingSetNotFound.razor | 12 ++++++++++++ 3 files changed, 26 insertions(+) create mode 100644 src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/NotFoundPage.razor create mode 100644 src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/StreamingRendering/StreamingSetNotFound.razor diff --git a/src/Components/test/E2ETest/ServerRenderingTests/NoInteractivityTest.cs b/src/Components/test/E2ETest/ServerRenderingTests/NoInteractivityTest.cs index 0b30003f4b70..d241e2fb189d 100644 --- a/src/Components/test/E2ETest/ServerRenderingTests/NoInteractivityTest.cs +++ b/src/Components/test/E2ETest/ServerRenderingTests/NoInteractivityTest.cs @@ -3,6 +3,7 @@ using System.Net.Http; using Components.TestServer.RazorComponents; +using Microsoft.AspNetCore.Components.E2ETest; using Microsoft.AspNetCore.Components.E2ETest.Infrastructure; using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures; using Microsoft.AspNetCore.E2ETesting; @@ -64,4 +65,12 @@ public void CanUseServerAuthenticationStateByDefault() Browser.Equal("True", () => Browser.FindElement(By.Id("is-in-test-role-1")).Text); Browser.Equal("True", () => Browser.FindElement(By.Id("is-in-test-role-2")).Text); } + + [Fact] + public void CanRenderNotFoundPage() + { + Navigate($"{ServerPathBase}/streaming-set-not-found"); + Browser.WaitForElementToBeVisible(By.Id("test-info")); + Browser.Equal("Default Not Found Page", () => Browser.Exists(By.Id("test-info")).Text); + } } diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/NotFoundPage.razor b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/NotFoundPage.razor new file mode 100644 index 000000000000..7345716f0bdf --- /dev/null +++ b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/NotFoundPage.razor @@ -0,0 +1,5 @@ +@page "/not-found" + +

Default Not Found Page

+

This page is used for a workaround of NavigationManager.NotFound() method, used in SSR when the response already started and changing it to 404 is not possible. + This workaround triggers navigation to a constant "not-found" relative path.

\ No newline at end of file diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/StreamingRendering/StreamingSetNotFound.razor b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/StreamingRendering/StreamingSetNotFound.razor new file mode 100644 index 000000000000..9b89968e305e --- /dev/null +++ b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/StreamingRendering/StreamingSetNotFound.razor @@ -0,0 +1,12 @@ +@page "/streaming-set-not-found" +@attribute [StreamRendering] +@inject NavigationManager NavigationManager + +@code{ + protected override async Task OnInitializedAsync() + { + // Simulate some delay before triggering NotFound to start streaming response + await Task.Delay(1000); + NavigationManager.NotFound(); + } +} \ No newline at end of file From cb32f949edafa05283484c9c38cc8a4d62d716aa Mon Sep 17 00:00:00 2001 From: Ilona Tomkowicz Date: Tue, 8 Apr 2025 15:51:46 +0200 Subject: [PATCH 32/42] Test SSR with no interactivity. --- .../NoInteractivityTest.cs | 22 +++++++++++++++++- .../RazorComponents/App.razor | 23 ++++++++++++++++++- .../Pages/PageThatSetsNotFound.razor | 3 ++- 3 files changed, 45 insertions(+), 3 deletions(-) diff --git a/src/Components/test/E2ETest/ServerRenderingTests/NoInteractivityTest.cs b/src/Components/test/E2ETest/ServerRenderingTests/NoInteractivityTest.cs index d241e2fb189d..5d9534b18346 100644 --- a/src/Components/test/E2ETest/ServerRenderingTests/NoInteractivityTest.cs +++ b/src/Components/test/E2ETest/ServerRenderingTests/NoInteractivityTest.cs @@ -67,10 +67,30 @@ public void CanUseServerAuthenticationStateByDefault() } [Fact] - public void CanRenderNotFoundPage() + public void CanRenderNotFoundPageAfterStreamingStarted() { Navigate($"{ServerPathBase}/streaming-set-not-found"); Browser.WaitForElementToBeVisible(By.Id("test-info")); Browser.Equal("Default Not Found Page", () => Browser.Exists(By.Id("test-info")).Text); } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void CanRenderNotFoundPageNoStreaming(bool useCustomNotFoundPage) + { + string query = useCustomNotFoundPage ? "&useCustomNotFoundPage=true" : ""; + Navigate($"{ServerPathBase}/set-not-found?shouldSet=true{query}"); + + if (useCustomNotFoundPage) + { + var infoText = Browser.FindElement(By.Id("test-info")).Text; + Assert.Contains("Welcome On Custom Not Found Page", infoText); + } + else + { + var bodyText = Browser.FindElement(By.TagName("body")).Text; + Assert.Contains("There's nothing here", bodyText); + } + } } diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponents/App.razor b/src/Components/test/testassets/Components.TestServer/RazorComponents/App.razor index 8c4a8f258b51..4418fd392d9b 100644 --- a/src/Components/test/testassets/Components.TestServer/RazorComponents/App.razor +++ b/src/Components/test/testassets/Components.TestServer/RazorComponents/App.razor @@ -1,4 +1,25 @@ @using Components.TestServer.RazorComponents.Pages.Forms +@using Components.WasmMinimal.Pages + +@code { + [Parameter] + [SupplyParameterFromQuery(Name = "useCustomNotFoundPage")] + public string? UseCustomNotFoundPage { get; set; } + + private Type? NotFoundPageType { get; set; } + + protected override void OnParametersSet() + { + if (UseCustomNotFoundPage == "true") + { + NotFoundPageType = typeof(CustomNotFoundPage); + } + else + { + NotFoundPageType = null; + } + } +} @@ -8,7 +29,7 @@ - + diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/PageThatSetsNotFound.razor b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/PageThatSetsNotFound.razor index 1afedff2f2da..af3b8a00e03e 100644 --- a/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/PageThatSetsNotFound.razor +++ b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/PageThatSetsNotFound.razor @@ -1,4 +1,5 @@ @page "/reexecution/set-not-found" +@page "/set-not-found" @attribute [StreamRendering(false)] @inject NavigationManager NavigationManager @@ -9,7 +10,7 @@ @code{ [Parameter] [SupplyParameterFromQuery(Name = "shouldSet")] - private bool ShouldSet { get; set; } = true; + public bool ShouldSet { get; set; } = true; protected override void OnInitialized() { From 8273758d3d33e7f84f93d27a39e34fd0c0a57cf9 Mon Sep 17 00:00:00 2001 From: Ilona Tomkowicz Date: Tue, 8 Apr 2025 18:45:59 +0200 Subject: [PATCH 33/42] Stop the renderer regardless of `Response.HasStarted`. --- .../src/Rendering/EndpointHtmlRenderer.EventDispatch.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.EventDispatch.cs b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.EventDispatch.cs index 406513f7d44f..390faf23994a 100644 --- a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.EventDispatch.cs +++ b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.EventDispatch.cs @@ -82,10 +82,12 @@ private void SetNotFoundResponse(object? sender, EventArgs args) var navigationManager = _httpContext.RequestServices.GetRequiredService(); var notFoundUri = $"{navigationManager.BaseUri}not-found"; navigationManager.NavigateTo(notFoundUri); - return; } - _httpContext.Response.StatusCode = StatusCodes.Status404NotFound; - _httpContext.Response.ContentType = null; + else + { + _httpContext.Response.StatusCode = StatusCodes.Status404NotFound; + _httpContext.Response.ContentType = null; + } SignalRendererToFinishRendering(); } From c31812c4e2f73430fb6c726219e1c72867a3fa41 Mon Sep 17 00:00:00 2001 From: Ilona Tomkowicz Date: Wed, 9 Apr 2025 11:56:05 +0200 Subject: [PATCH 34/42] Feedback: not checking status code works as well. --- .../src/Rendering/EndpointHtmlRenderer.Prerendering.cs | 4 +--- .../Endpoints/src/Rendering/EndpointHtmlRenderer.cs | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Prerendering.cs b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Prerendering.cs index 08ce762927dc..a00bc772628c 100644 --- a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Prerendering.cs +++ b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Prerendering.cs @@ -16,7 +16,6 @@ namespace Microsoft.AspNetCore.Components.Endpoints; internal partial class EndpointHtmlRenderer { private static readonly object ComponentSequenceKey = new object(); - private bool _isHandlingNotFound; protected override IComponent ResolveComponentForRenderMode([DynamicallyAccessedMembers(Component)] Type componentType, int? parentComponentId, IComponentActivator componentActivator, IComponentRenderMode renderMode) { @@ -148,7 +147,6 @@ internal async ValueTask RenderEndpointComponen { var component = BeginRenderingComponent(rootComponentType, parameters); var result = new PrerenderedComponentHtmlContent(Dispatcher, component); - _isHandlingNotFound = httpContext.Response.StatusCode == StatusCodes.Status404NotFound && _hasStatusCodePage; await WaitForResultReady(waitForQuiescence, result); @@ -169,7 +167,7 @@ private async Task WaitForResultReady(bool waitForQuiescence, PrerenderedCompone } else if (_nonStreamingPendingTasks.Count > 0) { - if (_isHandlingNotFound) + if (_hasStatusCodePage) { HandleNonStreamingTasks(); } diff --git a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.cs b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.cs index 496cc27f819e..5efdc0bcb4f2 100644 --- a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.cs +++ b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.cs @@ -165,7 +165,7 @@ protected override ComponentState CreateComponentState(int componentId, ICompone protected override void AddPendingTask(ComponentState? componentState, Task task) { - if (_isHandlingNotFound) + if (_hasStatusCodePage) { return; } From b162946aa2add1f54ef567e4ab99c172711f4d69 Mon Sep 17 00:00:00 2001 From: Ilona Tomkowicz Date: Thu, 10 Apr 2025 11:57:52 +0200 Subject: [PATCH 35/42] Feedback: improve handling streaming-in-process case. --- .../EndpointHtmlRenderer.EventDispatch.cs | 15 ++++++++++----- .../src/Rendering/EndpointHtmlRenderer.cs | 2 +- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.EventDispatch.cs b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.EventDispatch.cs index 390faf23994a..aa8f8dd7486c 100644 --- a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.EventDispatch.cs +++ b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.EventDispatch.cs @@ -1,11 +1,14 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using Microsoft.AspNetCore.Components.Endpoints.Rendering; using Microsoft.AspNetCore.Components.Rendering; using Microsoft.AspNetCore.Components.RenderTree; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.WebUtilities; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; +using System.Buffers; using System.Globalization; using System.Linq; using System.Text; @@ -74,14 +77,16 @@ private Task ReturnErrorResponse(string detailedMessage) : Task.CompletedTask; } - private void SetNotFoundResponse(object? sender, EventArgs args) + private async Task SetNotFoundResponseAsync(string baseUri) { if (_httpContext.Response.HasStarted) { - // We cannot set a NotFound code after the response has already started - var navigationManager = _httpContext.RequestServices.GetRequiredService(); - var notFoundUri = $"{navigationManager.BaseUri}not-found"; - navigationManager.NavigateTo(notFoundUri); + var defaultBufferSize = 16 * 1024; + await using var writer = new HttpResponseStreamWriter(_httpContext.Response.Body, Encoding.UTF8, defaultBufferSize, ArrayPool.Shared, ArrayPool.Shared); + using var bufferWriter = new BufferedTextWriter(writer); + var notFoundUri = $"{baseUri}not-found"; + HandleNavigationAfterResponseStarted(bufferWriter, _httpContext, notFoundUri); + await bufferWriter.FlushAsync(); } else { diff --git a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.cs b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.cs index 5efdc0bcb4f2..1259e0bf2540 100644 --- a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.cs +++ b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.cs @@ -85,7 +85,7 @@ internal async Task InitializeStandardComponentServicesAsync( if (navigationManager != null) { - navigationManager.OnNotFound += SetNotFoundResponse; + navigationManager.OnNotFound += async (sender, args) => await SetNotFoundResponseAsync(navigationManager.BaseUri); } var authenticationStateProvider = httpContext.RequestServices.GetService(); From 3e77c62f4e71ae7dc05e1e395ec123e0b54a4079 Mon Sep 17 00:00:00 2001 From: Ilona Tomkowicz Date: Fri, 11 Apr 2025 14:44:21 +0200 Subject: [PATCH 36/42] Throw on NotFound without global router. --- src/Components/Components/src/NavigationManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Components/Components/src/NavigationManager.cs b/src/Components/Components/src/NavigationManager.cs index 1cb4359871c5..d04fa7ce0758 100644 --- a/src/Components/Components/src/NavigationManager.cs +++ b/src/Components/Components/src/NavigationManager.cs @@ -208,7 +208,7 @@ private void NotFoundCore() if (_notFound == null) { // global router doesn't exist, no events were registered - NavigateTo($"{BaseUri}not-found"); + throw new InvalidOperationException("No handler is subscribed to the OnNotFound event. Ensure that the application has a global router or manually subscribe to the OnNotFound event."); } else { From d6125923460d2414c8cb3810294e05dbc9affe67 Mon Sep 17 00:00:00 2001 From: Ilona Tomkowicz Date: Fri, 11 Apr 2025 14:49:34 +0200 Subject: [PATCH 37/42] Use `IStatusCodeReExecuteFeature`. --- ...oft.AspNetCore.Components.Endpoints.csproj | 1 + .../src/RazorComponentEndpointInvoker.cs | 25 ++++++++++--------- .../EndpointHtmlRenderer.EventDispatch.cs | 3 +++ .../EndpointHtmlRenderer.Prerendering.cs | 4 +-- .../EndpointHtmlRenderer.Streaming.cs | 9 +++---- .../src/Rendering/EndpointHtmlRenderer.cs | 2 +- .../Results/RazorComponentResultExecutor.cs | 4 +-- .../StatusCodePagesTest.cs | 22 +++++++++------- 8 files changed, 39 insertions(+), 31 deletions(-) diff --git a/src/Components/Endpoints/src/Microsoft.AspNetCore.Components.Endpoints.csproj b/src/Components/Endpoints/src/Microsoft.AspNetCore.Components.Endpoints.csproj index 5362350bb1d7..4fa9814ea77e 100644 --- a/src/Components/Endpoints/src/Microsoft.AspNetCore.Components.Endpoints.csproj +++ b/src/Components/Endpoints/src/Microsoft.AspNetCore.Components.Endpoints.csproj @@ -40,6 +40,7 @@ + diff --git a/src/Components/Endpoints/src/RazorComponentEndpointInvoker.cs b/src/Components/Endpoints/src/RazorComponentEndpointInvoker.cs index e188f3d6fc2a..51a48dff6952 100644 --- a/src/Components/Endpoints/src/RazorComponentEndpointInvoker.cs +++ b/src/Components/Endpoints/src/RazorComponentEndpointInvoker.cs @@ -39,15 +39,15 @@ private async Task RenderComponentCore(HttpContext context) { context.Response.ContentType = RazorComponentResultExecutor.DefaultContentType; var isErrorHandler = context.Features.Get() is not null; - var hasStatusCodePage = context.Features.Get() is not null; + var isReExecuted = context.Features.Get() is not null; if (isErrorHandler) { Log.InteractivityDisabledForErrorHandling(_logger); } - _renderer.InitializeStreamingRenderingFraming(context, isErrorHandler, hasStatusCodePage); - bool avoidEditingHeaders = hasStatusCodePage && context.Response.StatusCode == StatusCodes.Status404NotFound; - if (!avoidEditingHeaders) + _renderer.InitializeStreamingRenderingFraming(context, isErrorHandler, isReExecuted); + if (!isReExecuted) { + // re-executed pages have Headers already set up EndpointHtmlRenderer.MarkAsAllowingEnhancedNavigation(context); } @@ -91,7 +91,7 @@ await _renderer.InitializeStandardComponentServicesAsync( using var bufferWriter = new BufferedTextWriter(writer); int originalStatusCode = context.Response.StatusCode; - bool isErrorHandlerOrHasStatusCodePage = isErrorHandler || hasStatusCodePage; + bool isErrorHandlerOrReExecuted = isErrorHandler || isReExecuted; // Note that we always use Static rendering mode for the top-level output from a RazorComponentResult, // because you never want to serialize the invocation of RazorComponentResultHost. Instead, that host @@ -100,14 +100,15 @@ await _renderer.InitializeStandardComponentServicesAsync( context, rootComponent, ParameterView.Empty, - waitForQuiescence: result.IsPost || isErrorHandlerOrHasStatusCodePage); + waitForQuiescence: result.IsPost || isErrorHandlerOrReExecuted); - bool requiresReexecution = originalStatusCode != context.Response.StatusCode && hasStatusCodePage; - if (requiresReexecution) + bool isReExecutionRequested = context.Features.Get() is not null; + bool avoidStartingResponse = !isReExecuted && isReExecutionRequested; + if (avoidStartingResponse) { - // If the response is a 404, we don't want to write any content. - // This is because the 404 status code is used by the routing middleware - // to indicate that no endpoint was found for the request. + // re-execution feature was set during rendering, + // we should finish early to avoid writing to the response + // and let the re-execution middleware take care of it return; } @@ -162,7 +163,7 @@ await _renderer.InitializeStandardComponentServicesAsync( } // Emit comment containing state. - if (!isErrorHandlerOrHasStatusCodePage) + if (!isErrorHandlerOrReExecuted) { var componentStateHtmlContent = await _renderer.PrerenderPersistedStateAsync(context); componentStateHtmlContent.WriteTo(bufferWriter, HtmlEncoder.Default); diff --git a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.EventDispatch.cs b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.EventDispatch.cs index aa8f8dd7486c..6fc158ef8fe3 100644 --- a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.EventDispatch.cs +++ b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.EventDispatch.cs @@ -4,6 +4,7 @@ using Microsoft.AspNetCore.Components.Endpoints.Rendering; using Microsoft.AspNetCore.Components.Rendering; using Microsoft.AspNetCore.Components.RenderTree; +using Microsoft.AspNetCore.Diagnostics; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.WebUtilities; using Microsoft.Extensions.DependencyInjection; @@ -93,6 +94,8 @@ private async Task SetNotFoundResponseAsync(string baseUri) _httpContext.Response.StatusCode = StatusCodes.Status404NotFound; _httpContext.Response.ContentType = null; } + var statusCodeFeature = new StatusCodeReExecuteFeature(); + _httpContext.Features.Set(statusCodeFeature); SignalRendererToFinishRendering(); } diff --git a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Prerendering.cs b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Prerendering.cs index a00bc772628c..75915072d8ef 100644 --- a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Prerendering.cs +++ b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Prerendering.cs @@ -19,7 +19,7 @@ internal partial class EndpointHtmlRenderer protected override IComponent ResolveComponentForRenderMode([DynamicallyAccessedMembers(Component)] Type componentType, int? parentComponentId, IComponentActivator componentActivator, IComponentRenderMode renderMode) { - if (_isHandlingErrors || _hasStatusCodePage) + if (_isHandlingErrors || _isReExecuted) { // Ignore the render mode boundary in error scenarios. return componentActivator.CreateInstance(componentType); @@ -167,7 +167,7 @@ private async Task WaitForResultReady(bool waitForQuiescence, PrerenderedCompone } else if (_nonStreamingPendingTasks.Count > 0) { - if (_hasStatusCodePage) + if (_isReExecuted) { HandleNonStreamingTasks(); } diff --git a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Streaming.cs b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Streaming.cs index d2cce7a5fa2e..c17f7cd53555 100644 --- a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Streaming.cs +++ b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Streaming.cs @@ -23,14 +23,13 @@ internal partial class EndpointHtmlRenderer private HashSet? _visitedComponentIdsInCurrentStreamingBatch; private string? _ssrFramingCommentMarkup; private bool _isHandlingErrors; - private bool _hasStatusCodePage; + private bool _isReExecuted; - public void InitializeStreamingRenderingFraming(HttpContext httpContext, bool isErrorHandler, bool hasStatusCodePage) + public void InitializeStreamingRenderingFraming(HttpContext httpContext, bool isErrorHandler, bool isReExecuted) { _isHandlingErrors = isErrorHandler; - _hasStatusCodePage = hasStatusCodePage; - bool avoidEditingHeaders = hasStatusCodePage && httpContext.Response.StatusCode == StatusCodes.Status404NotFound; - if (!avoidEditingHeaders && IsProgressivelyEnhancedNavigation(httpContext.Request)) + _isReExecuted = isReExecuted; + if (!isReExecuted && IsProgressivelyEnhancedNavigation(httpContext.Request)) { var id = Guid.NewGuid().ToString(); httpContext.Response.Headers.Add(_streamingRenderingFramingHeaderName, id); diff --git a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.cs b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.cs index 1259e0bf2540..e0554c7cacce 100644 --- a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.cs +++ b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.cs @@ -165,7 +165,7 @@ protected override ComponentState CreateComponentState(int componentId, ICompone protected override void AddPendingTask(ComponentState? componentState, Task task) { - if (_hasStatusCodePage) + if (_isReExecuted) { return; } diff --git a/src/Components/Endpoints/src/Results/RazorComponentResultExecutor.cs b/src/Components/Endpoints/src/Results/RazorComponentResultExecutor.cs index 06e018ea68ff..3f092c15626a 100644 --- a/src/Components/Endpoints/src/Results/RazorComponentResultExecutor.cs +++ b/src/Components/Endpoints/src/Results/RazorComponentResultExecutor.cs @@ -48,8 +48,8 @@ private static Task RenderComponentToResponse( return endpointHtmlRenderer.Dispatcher.InvokeAsync(async () => { var isErrorHandler = httpContext.Features.Get() is not null; - var hasStatusCodePage = httpContext.Features.Get() is not null; - endpointHtmlRenderer.InitializeStreamingRenderingFraming(httpContext, isErrorHandler, hasStatusCodePage); + var isReExecuted = httpContext.Features.Get() is not null; + endpointHtmlRenderer.InitializeStreamingRenderingFraming(httpContext, isErrorHandler, isReExecuted); EndpointHtmlRenderer.MarkAsAllowingEnhancedNavigation(httpContext); // We could pool these dictionary instances if we wanted, and possibly even the ParameterView diff --git a/src/Components/test/E2ETest/ServerRenderingTests/StatusCodePagesTest.cs b/src/Components/test/E2ETest/ServerRenderingTests/StatusCodePagesTest.cs index 5b6d70bc7cf2..3f081a1516a0 100644 --- a/src/Components/test/E2ETest/ServerRenderingTests/StatusCodePagesTest.cs +++ b/src/Components/test/E2ETest/ServerRenderingTests/StatusCodePagesTest.cs @@ -6,13 +6,14 @@ using System.Linq; using System.Text; using System.Threading.Tasks; -using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures; -using Microsoft.AspNetCore.Components.E2ETest.Infrastructure; -using TestServer; using Components.TestServer.RazorComponents; +using Components.TestServer.RazorComponents.Pages.StreamingRendering; +using Microsoft.AspNetCore.Components.E2ETest.Infrastructure; +using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures; using Microsoft.AspNetCore.E2ETesting; -using Xunit.Abstractions; using OpenQA.Selenium; +using TestServer; +using Xunit.Abstractions; namespace Microsoft.AspNetCore.Components.E2ETests.ServerRenderingTests; @@ -21,16 +22,19 @@ public class StatusCodePagesTest(BrowserFixture browserFixture, BasicTestAppServ { [Theory] - [InlineData(true)] - [InlineData(false)] - public void StatusCodePagesWithReexecution(bool setNotFound) + [InlineData(false, true)] + [InlineData(true, false)] + [InlineData(false, false)] + public void StatusCodePagesWithReExecution(bool setNotFound, bool streaming) { - Navigate($"{ServerPathBase}/reexecution/set-not-found?shouldSet={setNotFound}"); + string streamingPath = streaming ? "streaming-" : ""; + Navigate($"{ServerPathBase}/reexecution/{streamingPath}set-not-found?shouldSet={setNotFound}"); string expectedTitle = setNotFound ? "Re-executed page" : "Original page"; Browser.Equal(expectedTitle, () => Browser.Title); var infoText = Browser.FindElement(By.Id("test-info")).Text; - string expectedInfoText = setNotFound ? "Welcome On Page Re-executed After Not Found Event" : "Any content"; + // streaming when response started does not re-execute + string expectedInfoText = streaming ? "Default Not Found Page" : setNotFound ? "Welcome On Page Re-executed After Not Found Event" : "Any content"; Assert.Contains(expectedInfoText, infoText); } From cd5dffa96e77bc9bdea0daaafae21bbd80a069aa Mon Sep 17 00:00:00 2001 From: Ilona Tomkowicz Date: Tue, 15 Apr 2025 09:37:54 +0200 Subject: [PATCH 38/42] Unify "fallback" pages - check for titles only. --- .../ServerRenderingTests/NoInteractivityTest.cs | 3 +-- .../ServerRenderingTests/StatusCodePagesTest.cs | 11 ++++++----- .../RazorComponents/Pages/NotFoundPage.razor | 5 +++-- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/Components/test/E2ETest/ServerRenderingTests/NoInteractivityTest.cs b/src/Components/test/E2ETest/ServerRenderingTests/NoInteractivityTest.cs index 5d9534b18346..7c321069bcca 100644 --- a/src/Components/test/E2ETest/ServerRenderingTests/NoInteractivityTest.cs +++ b/src/Components/test/E2ETest/ServerRenderingTests/NoInteractivityTest.cs @@ -70,8 +70,7 @@ public void CanUseServerAuthenticationStateByDefault() public void CanRenderNotFoundPageAfterStreamingStarted() { Navigate($"{ServerPathBase}/streaming-set-not-found"); - Browser.WaitForElementToBeVisible(By.Id("test-info")); - Browser.Equal("Default Not Found Page", () => Browser.Exists(By.Id("test-info")).Text); + Browser.Equal("Default Not Found Page", () => Browser.Title); } [Theory] diff --git a/src/Components/test/E2ETest/ServerRenderingTests/StatusCodePagesTest.cs b/src/Components/test/E2ETest/ServerRenderingTests/StatusCodePagesTest.cs index 3f081a1516a0..69d691062d77 100644 --- a/src/Components/test/E2ETest/ServerRenderingTests/StatusCodePagesTest.cs +++ b/src/Components/test/E2ETest/ServerRenderingTests/StatusCodePagesTest.cs @@ -30,12 +30,13 @@ public void StatusCodePagesWithReExecution(bool setNotFound, bool streaming) string streamingPath = streaming ? "streaming-" : ""; Navigate($"{ServerPathBase}/reexecution/{streamingPath}set-not-found?shouldSet={setNotFound}"); - string expectedTitle = setNotFound ? "Re-executed page" : "Original page"; - Browser.Equal(expectedTitle, () => Browser.Title); - var infoText = Browser.FindElement(By.Id("test-info")).Text; // streaming when response started does not re-execute - string expectedInfoText = streaming ? "Default Not Found Page" : setNotFound ? "Welcome On Page Re-executed After Not Found Event" : "Any content"; - Assert.Contains(expectedInfoText, infoText); + string expectedTitle = streaming + ? "Default Not Found Page" + : setNotFound + ? "Re-executed page" + : "Original page"; + Browser.Equal(expectedTitle, () => Browser.Title); } } diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/NotFoundPage.razor b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/NotFoundPage.razor index 7345716f0bdf..26641748f282 100644 --- a/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/NotFoundPage.razor +++ b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/NotFoundPage.razor @@ -1,5 +1,6 @@ @page "/not-found" -

Default Not Found Page

-

This page is used for a workaround of NavigationManager.NotFound() method, used in SSR when the response already started and changing it to 404 is not possible. +Default Not Found Page + +

This page is used for a workaround of NavigationManager.NotFound() method, used in SSR when the response already started and changing it to 404 is not possible. This workaround triggers navigation to a constant "not-found" relative path.

\ No newline at end of file From b416805e047c5f9e8d515975a589ca4df53d8ed2 Mon Sep 17 00:00:00 2001 From: Ilona Tomkowicz Date: Thu, 17 Apr 2025 09:40:37 +0200 Subject: [PATCH 39/42] Fix early return condition. --- .../Endpoints/src/RazorComponentEndpointInvoker.cs | 9 +++------ .../src/Rendering/EndpointHtmlRenderer.EventDispatch.cs | 3 --- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/src/Components/Endpoints/src/RazorComponentEndpointInvoker.cs b/src/Components/Endpoints/src/RazorComponentEndpointInvoker.cs index 51a48dff6952..245f811d7f76 100644 --- a/src/Components/Endpoints/src/RazorComponentEndpointInvoker.cs +++ b/src/Components/Endpoints/src/RazorComponentEndpointInvoker.cs @@ -39,6 +39,7 @@ private async Task RenderComponentCore(HttpContext context) { context.Response.ContentType = RazorComponentResultExecutor.DefaultContentType; var isErrorHandler = context.Features.Get() is not null; + var hasStatusCodePage = context.Features.Get() is not null; var isReExecuted = context.Features.Get() is not null; if (isErrorHandler) { @@ -90,7 +91,6 @@ await _renderer.InitializeStandardComponentServicesAsync( await using var writer = new HttpResponseStreamWriter(context.Response.Body, Encoding.UTF8, defaultBufferSize, ArrayPool.Shared, ArrayPool.Shared); using var bufferWriter = new BufferedTextWriter(writer); - int originalStatusCode = context.Response.StatusCode; bool isErrorHandlerOrReExecuted = isErrorHandler || isReExecuted; // Note that we always use Static rendering mode for the top-level output from a RazorComponentResult, @@ -102,13 +102,10 @@ await _renderer.InitializeStandardComponentServicesAsync( ParameterView.Empty, waitForQuiescence: result.IsPost || isErrorHandlerOrReExecuted); - bool isReExecutionRequested = context.Features.Get() is not null; - bool avoidStartingResponse = !isReExecuted && isReExecutionRequested; + bool avoidStartingResponse = hasStatusCodePage && !isReExecuted && context.Response.StatusCode == StatusCodes.Status404NotFound; if (avoidStartingResponse) { - // re-execution feature was set during rendering, - // we should finish early to avoid writing to the response - // and let the re-execution middleware take care of it + // the request is going to be re-executed, we should avoid writing to the response return; } diff --git a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.EventDispatch.cs b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.EventDispatch.cs index 6fc158ef8fe3..aa8f8dd7486c 100644 --- a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.EventDispatch.cs +++ b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.EventDispatch.cs @@ -4,7 +4,6 @@ using Microsoft.AspNetCore.Components.Endpoints.Rendering; using Microsoft.AspNetCore.Components.Rendering; using Microsoft.AspNetCore.Components.RenderTree; -using Microsoft.AspNetCore.Diagnostics; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.WebUtilities; using Microsoft.Extensions.DependencyInjection; @@ -94,8 +93,6 @@ private async Task SetNotFoundResponseAsync(string baseUri) _httpContext.Response.StatusCode = StatusCodes.Status404NotFound; _httpContext.Response.ContentType = null; } - var statusCodeFeature = new StatusCodeReExecuteFeature(); - _httpContext.Features.Set(statusCodeFeature); SignalRendererToFinishRendering(); } From c0e7f868b463636b35eb1fd6c5605e1842cb20c0 Mon Sep 17 00:00:00 2001 From: Ilona Tomkowicz Date: Tue, 22 Apr 2025 18:43:58 +0200 Subject: [PATCH 40/42] Feedback. --- .../Pages/StreamingRendering/StreamingSetNotFound.razor | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/StreamingRendering/StreamingSetNotFound.razor b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/StreamingRendering/StreamingSetNotFound.razor index 9b89968e305e..31466a1e9858 100644 --- a/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/StreamingRendering/StreamingSetNotFound.razor +++ b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/StreamingRendering/StreamingSetNotFound.razor @@ -6,7 +6,7 @@ protected override async Task OnInitializedAsync() { // Simulate some delay before triggering NotFound to start streaming response - await Task.Delay(1000); + await Task.Yield(); NavigationManager.NotFound(); } } \ No newline at end of file From ebdf9a9f30c5ecf083c0d84922f49306ef589b8e Mon Sep 17 00:00:00 2001 From: Ilona Tomkowicz Date: Wed, 23 Apr 2025 10:59:06 +0200 Subject: [PATCH 41/42] No-op instead of exception. --- src/Components/Components/src/NavigationManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Components/Components/src/NavigationManager.cs b/src/Components/Components/src/NavigationManager.cs index d04fa7ce0758..25d6da65b94e 100644 --- a/src/Components/Components/src/NavigationManager.cs +++ b/src/Components/Components/src/NavigationManager.cs @@ -208,7 +208,7 @@ private void NotFoundCore() if (_notFound == null) { // global router doesn't exist, no events were registered - throw new InvalidOperationException("No handler is subscribed to the OnNotFound event. Ensure that the application has a global router or manually subscribe to the OnNotFound event."); + return; } else { From fc49fe634d35ea3d3ecdab138be4d3cd72e81326 Mon Sep 17 00:00:00 2001 From: Ilona Tomkowicz Date: Wed, 23 Apr 2025 11:46:25 +0200 Subject: [PATCH 42/42] Solve merge conflict: hard stop in redirection and deferred stop in 404. --- .../EndpointHtmlRenderer.EventDispatch.cs | 6 +++- .../src/Rendering/EndpointHtmlRenderer.cs | 18 +++++++++++- .../NoInteractivityTest.cs | 13 +++++++++ .../StatusCodePagesTest.cs | 15 ++++------ .../Pages/PageThatSetsNotFound.razor | 5 ++-- .../StreamingSetNotFound.razor | 28 +++++++++++++++---- 6 files changed, 67 insertions(+), 18 deletions(-) diff --git a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.EventDispatch.cs b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.EventDispatch.cs index 734f17079cff..8a1062a58d76 100644 --- a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.EventDispatch.cs +++ b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.EventDispatch.cs @@ -93,7 +93,11 @@ private async Task SetNotFoundResponseAsync(string baseUri) _httpContext.Response.StatusCode = StatusCodes.Status404NotFound; _httpContext.Response.ContentType = null; } - SignalRendererToFinishRendering(); + + // When the application triggers a NotFound event, we continue rendering the current batch. + // However, after completing this batch, we do not want to process any further UI updates, + // as we are going to return a 404 status and discard the UI updates generated so far. + SignalRendererToFinishRenderingAfterCurrentBatch(); } private async Task OnNavigateTo(string uri) diff --git a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.cs b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.cs index 18bd8825b37d..e99574aa881e 100644 --- a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.cs +++ b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.cs @@ -183,12 +183,28 @@ protected override void AddPendingTask(ComponentState? componentState, Task task base.AddPendingTask(componentState, task); } - protected override void SignalRendererToFinishRendering() + private void SignalRendererToFinishRenderingAfterCurrentBatch() { + // sets a deferred stop on the renderer, which will have an effect after the current batch is completed _rendererIsStopped = true; + } + + protected override void SignalRendererToFinishRendering() + { + SignalRendererToFinishRenderingAfterCurrentBatch(); + // sets a hard stop on the renderer, which will have an effect immediately base.SignalRendererToFinishRendering(); } + protected override void ProcessPendingRender() + { + if (_rendererIsStopped) + { + return; + } + base.ProcessPendingRender(); + } + // For tests only internal Task? NonStreamingPendingTasksCompletion; diff --git a/src/Components/test/E2ETest/ServerRenderingTests/NoInteractivityTest.cs b/src/Components/test/E2ETest/ServerRenderingTests/NoInteractivityTest.cs index 00291c1d4802..139f3db4726e 100644 --- a/src/Components/test/E2ETest/ServerRenderingTests/NoInteractivityTest.cs +++ b/src/Components/test/E2ETest/ServerRenderingTests/NoInteractivityTest.cs @@ -106,4 +106,17 @@ public void CanRenderNotFoundPageNoStreaming(bool useCustomNotFoundPage) Assert.Contains("There's nothing here", bodyText); } } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void CanRenderNotFoundPageWithStreaming(bool useCustomNotFoundPage) + { + // when streaming started, we always render page under "not-found" path + string query = useCustomNotFoundPage ? "?useCustomNotFoundPage=true" : ""; + Navigate($"{ServerPathBase}/streaming-set-not-found{query}"); + + string expectedTitle = "Default Not Found Page"; + Browser.Equal(expectedTitle, () => Browser.Title); + } } diff --git a/src/Components/test/E2ETest/ServerRenderingTests/StatusCodePagesTest.cs b/src/Components/test/E2ETest/ServerRenderingTests/StatusCodePagesTest.cs index 69d691062d77..58ac90b39bbe 100644 --- a/src/Components/test/E2ETest/ServerRenderingTests/StatusCodePagesTest.cs +++ b/src/Components/test/E2ETest/ServerRenderingTests/StatusCodePagesTest.cs @@ -22,21 +22,18 @@ public class StatusCodePagesTest(BrowserFixture browserFixture, BasicTestAppServ { [Theory] - [InlineData(false, true)] - [InlineData(true, false)] [InlineData(false, false)] - public void StatusCodePagesWithReExecution(bool setNotFound, bool streaming) + [InlineData(true, false)] + [InlineData(true, true)] + public void StatusCodePagesWithReExecution(bool streaming, bool responseStarted) { string streamingPath = streaming ? "streaming-" : ""; - Navigate($"{ServerPathBase}/reexecution/{streamingPath}set-not-found?shouldSet={setNotFound}"); + Navigate($"{ServerPathBase}/reexecution/{streamingPath}set-not-found?responseStarted={responseStarted}"); // streaming when response started does not re-execute - string expectedTitle = streaming + string expectedTitle = responseStarted ? "Default Not Found Page" - : setNotFound - ? "Re-executed page" - : "Original page"; + : "Re-executed page"; Browser.Equal(expectedTitle, () => Browser.Title); } - } diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/PageThatSetsNotFound.razor b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/PageThatSetsNotFound.razor index af3b8a00e03e..e397f81672a3 100644 --- a/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/PageThatSetsNotFound.razor +++ b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/PageThatSetsNotFound.razor @@ -10,11 +10,12 @@ @code{ [Parameter] [SupplyParameterFromQuery(Name = "shouldSet")] - public bool ShouldSet { get; set; } = true; + public bool? ShouldSet { get; set; } protected override void OnInitialized() { - if (ShouldSet) + bool shouldSet = ShouldSet ?? true; + if (shouldSet) { NavigationManager.NotFound(); } diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/StreamingRendering/StreamingSetNotFound.razor b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/StreamingRendering/StreamingSetNotFound.razor index 31466a1e9858..92b5f95d7c4b 100644 --- a/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/StreamingRendering/StreamingSetNotFound.razor +++ b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/StreamingRendering/StreamingSetNotFound.razor @@ -1,12 +1,30 @@ +@page "/reexecution/streaming-set-not-found" @page "/streaming-set-not-found" @attribute [StreamRendering] @inject NavigationManager NavigationManager -@code{ +@code { + [Parameter] + [SupplyParameterFromQuery(Name = "shouldSet")] + public bool? ShouldSet { get; set; } + + [Parameter] + [SupplyParameterFromQuery(Name = "responseStarted")] + public bool? ResponseStarted { get; set; } + protected override async Task OnInitializedAsync() { - // Simulate some delay before triggering NotFound to start streaming response - await Task.Yield(); - NavigationManager.NotFound(); + bool shouldSet = ShouldSet ?? true; + bool responseStarted = ResponseStarted ?? true; + if (responseStarted) + { + // Simulate some delay before triggering NotFound to start streaming response + await Task.Yield(); + } + + if (shouldSet) + { + NavigationManager.NotFound(); + } } -} \ No newline at end of file +}