Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
599f846
Fix SSR non-streaming: rendering has higher priority than re-execution.
ilonatommy May 30, 2025
842941e
Add tests: move component common for global and local interactivity t…
ilonatommy May 30, 2025
079b66f
Missing build fixes for the previous commit.
ilonatommy May 30, 2025
301efac
Enable `Router` to stream in the `NotFound` content.
ilonatommy May 30, 2025
e7ce5ba
Remove debugging delay.
ilonatommy May 30, 2025
a2bfccf
Merge branch 'main' into fix-reexecution-priority-and-streaming-renering
ilonatommy Jun 2, 2025
526dbb1
Merge branch 'main' into fix-reexecution-priority-and-streaming-renering
ilonatommy Jun 5, 2025
b166369
Fix tests added with this PR.
ilonatommy Jun 10, 2025
1704811
Refactor test.
ilonatommy Jun 11, 2025
8184dd0
Fix enhanced navigation tests.
ilonatommy Jun 11, 2025
707683f
Markers for: `Router`, `Found`, `RouteView`, `RoutedPage` are now pre…
ilonatommy Jun 11, 2025
79c95e9
Client streams-in the NotFoundPage if it's provided to the Router.
ilonatommy Jun 12, 2025
3dfd293
Merge branch 'main' into fix-reexecution-priority-and-streaming-renering
ilonatommy Jun 12, 2025
82db52f
Update: client rendering the NotFoundPage if it's provided to the Rou…
ilonatommy Jun 12, 2025
a586b7f
Remove the streaming attribute.
ilonatommy Jun 13, 2025
60995b7
Fix the parameters order.
ilonatommy Jun 13, 2025
96287a4
unit test exception.
ilonatommy Jun 13, 2025
e8aab27
Remove not used code.
ilonatommy Jun 13, 2025
c6465d0
Fix POST rendering without disabling the "stop render" signal.
ilonatommy Jun 13, 2025
ab0d029
Unified SSR tests for POST and GET.
ilonatommy Jun 13, 2025
3d20fa9
Remove comments, test on CI
ilonatommy Jun 13, 2025
1caeaa6
Revert unnecessary changes.
ilonatommy Jun 13, 2025
fed0e5a
Clean up global interactivity tests.
ilonatommy Jun 13, 2025
1c6f97d
Trying to fix "Failed to load resource"
ilonatommy Jun 13, 2025
9d52115
Fix tests with custom not found page.
ilonatommy Jun 13, 2025
41a186c
Update src/Components/test/testassets/Components.Shared/Index.razor
ilonatommy Jun 16, 2025
f7c6092
Feedback: neat POST detection and redundant awaits removal.
ilonatommy Jun 16, 2025
29b5322
Use existing `TestContentPackage` as a shared project.
ilonatommy Jun 16, 2025
c0cee2f
Fix client navigation: always use enhanced nav for not found renderin…
ilonatommy Jun 16, 2025
975e250
Remove test duplication: when server always requests a render with en…
ilonatommy Jun 16, 2025
9a926ce
Use client redirect with url change when user dissbles enhanced navig…
ilonatommy Jun 16, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/Components/Components/src/Routing/Router.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ namespace Microsoft.AspNetCore.Components.Routing;
/// <summary>
/// A component that supplies route data corresponding to the current navigation state.
/// </summary>
[StreamRendering]
public partial class Router : IComponent, IHandleAfterRender, IDisposable
{
// Dictionary is intentionally used instead of ReadOnlyDictionary to reduce Blazor size
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,13 +102,6 @@ await _renderer.InitializeStandardComponentServicesAsync(
ParameterView.Empty,
waitForQuiescence: result.IsPost || isErrorHandlerOrReExecuted);

bool avoidStartingResponse = hasStatusCodePage && !isReExecuted && context.Response.StatusCode == StatusCodes.Status404NotFound;
if (avoidStartingResponse)
{
// the request is going to be re-executed, we should avoid writing to the response
return;
}

Task quiesceTask;
if (!result.IsPost)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,27 +77,21 @@ private Task ReturnErrorResponse(string detailedMessage)
: Task.CompletedTask;
}

private async Task SetNotFoundResponseAsync(string baseUri)
private void SetNotFoundResponse(object? sender, EventArgs args)
{
if (_httpContext.Response.HasStarted)
{
var defaultBufferSize = 16 * 1024;
await using var writer = new HttpResponseStreamWriter(_httpContext.Response.Body, Encoding.UTF8, defaultBufferSize, ArrayPool<byte>.Shared, ArrayPool<char>.Shared);
using var bufferWriter = new BufferedTextWriter(writer);
var notFoundUri = $"{baseUri}not-found";
HandleNavigationAfterResponseStarted(bufferWriter, _httpContext, notFoundUri);
await bufferWriter.FlushAsync();
// We're expecting the Router to continue streaming the NotFound contents
}
else
{
_httpContext.Response.StatusCode = StatusCodes.Status404NotFound;
_httpContext.Response.ContentType = null;
}

// 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.
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.
SignalRendererToFinishRendering();
}
}

private async Task OnNavigateTo(string uri)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ internal async Task InitializeStandardComponentServicesAsync(

if (navigationManager != null)
{
navigationManager.OnNotFound += async (sender, args) => await SetNotFoundResponseAsync(navigationManager.BaseUri);
navigationManager.OnNotFound += SetNotFoundResponse;
}

var authenticationStateProvider = httpContext.RequestServices.GetService<AuthenticationStateProvider>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1448,4 +1448,62 @@ public void BrowserNavigationToNotExistingPathReExecutesTo404(string renderMode)

private void Assert404ReExecuted() =>
Browser.Equal("Welcome On Page Re-executed After Not Found Event", () => Browser.Exists(By.Id("test-info")).Text);

[Theory]
[InlineData(true)]
[InlineData(false)]
public void CanRenderNotFoundPage_SSR(bool streamingStarted)
{
string streamingPath = streamingStarted ? "-streaming" : "";
Navigate($"{ServerPathBase}/set-not-found-ssr{streamingPath}?useCustomNotFoundPage=true");
AssertCustomNotFoundPageRendered();
}

[Theory]
[InlineData("ServerNonPrerendered")]
[InlineData("WebAssemblyNonPrerendered")]
public void CanRenderNotFoundPage_Interactive(string renderMode)
{
Navigate($"{ServerPathBase}/set-not-found?useCustomNotFoundPage=true&renderMode={renderMode}");
AssertCustomNotFoundPageRendered();
}

private void AssertCustomNotFoundPageRendered()
{
var infoText = Browser.FindElement(By.Id("test-info")).Text;
Assert.Contains("Welcome On Custom Not Found Page", infoText);
// custom page should have a custom layout
var aboutLink = Browser.FindElement(By.Id("about-link")).Text;
Assert.Contains("About", aboutLink);
}

[Theory]
[InlineData(false)]
[InlineData(true)]
public void DoesNotReExecuteIf404WasHandled_SSR(bool streamingStarted)
{
string streamingPath = streamingStarted ? "-streaming" : "";
Navigate($"{ServerPathBase}/reexecution/set-not-found-ssr{streamingPath}");
AssertNotFoundFragmentRendered();
}

[Theory]
[InlineData("ServerNonPrerendered")]
[InlineData("WebAssemblyNonPrerendered")]
public async Task DoesNotReExecuteIf404WasHandled_Interactive(string renderMode)
{
Navigate($"{ServerPathBase}/reexecution/set-not-found?renderMode={renderMode}");
await Task.Delay(5000);
AssertNotFoundFragmentRendered();
}

private void AssertNotFoundFragmentRendered() =>
Browser.Equal("There's nothing here", () => Browser.FindElement(By.CssSelector("body > p")).Text);

[Fact]
public void StatusCodePagesWithReExecution()
{
Navigate($"{ServerPathBase}/reexecution/trigger-404");
Assert404ReExecuted();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -80,13 +80,6 @@ public void NavigatesWithoutInteractivityByRequestRedirection(bool controlFlowBy
Browser.Equal("Routing test cases", () => Browser.Exists(By.Id("test-info")).Text);
}

[Fact]
public void CanRenderNotFoundPageAfterStreamingStarted()
{
Navigate($"{ServerPathBase}/streaming-set-not-found");
Browser.Equal("Default Not Found Page", () => Browser.Title);
}

[Theory]
[InlineData(true)]
[InlineData(false)]
Expand Down Expand Up @@ -127,36 +120,35 @@ private void Assert404ReExecuted() =>
[Theory]
[InlineData(true)]
[InlineData(false)]
public void CanRenderNotFoundPageNoStreaming(bool useCustomNotFoundPage)
public void CanRenderNotFoundPage(bool streamingStarted)
{
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);
// custom page should have a custom layout
var aboutLink = Browser.FindElement(By.Id("about-link")).Text;
Assert.Contains("About", aboutLink);
}
else
{
var bodyText = Browser.FindElement(By.TagName("body")).Text;
Assert.Contains("There's nothing here", bodyText);
}
string streamingPath = streamingStarted ? "-streaming" : "";
Navigate($"{ServerPathBase}/set-not-found-ssr{streamingPath}?useCustomNotFoundPage=true");

var infoText = Browser.FindElement(By.Id("test-info")).Text;
Assert.Contains("Welcome On Custom Not Found Page", infoText);
// custom page should have a custom layout
var aboutLink = Browser.FindElement(By.Id("about-link")).Text;
Assert.Contains("About", aboutLink);
}

[Theory]
[InlineData(true)]
[InlineData(false)]
public void CanRenderNotFoundPageWithStreaming(bool useCustomNotFoundPage)
[InlineData(true)]
public void DoesNotReExecuteIf404WasHandled(bool streamingStarted)
{
// when streaming started, we always render page under "not-found" path
string query = useCustomNotFoundPage ? "?useCustomNotFoundPage=true" : "";
Navigate($"{ServerPathBase}/streaming-set-not-found{query}");
string streamingPath = streamingStarted ? "-streaming" : "";
Navigate($"{ServerPathBase}/reexecution/set-not-found-ssr{streamingPath}");
AssertNotFoundFragmentRendered();
}

private void AssertNotFoundFragmentRendered() =>
Browser.Equal("There's nothing here", () => Browser.FindElement(By.CssSelector("body > p")).Text);

string expectedTitle = "Default Not Found Page";
Browser.Equal(expectedTitle, () => Browser.Title);
[Fact]
public void StatusCodePagesWithReExecution()
{
Navigate($"{ServerPathBase}/reexecution/trigger-404");
Browser.Equal("Re-executed page", () => Browser.Title);
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
@inject NavigationManager NavigationManager

@if (!WaitForInteractivity || RendererInfo.IsInteractive)
{
<PageTitle>Original page</PageTitle>

<p id="test-info">Any content</p>

}

@code{
[Parameter]
public bool StartStreaming { get; set; } = false;

[Parameter]
public bool WaitForInteractivity { get; set; } = false;

protected async override Task OnInitializedAsync()
{
if (StartStreaming)
{
await Task.Yield();
}
NavigationManager.NotFound();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk.Razor">

<PropertyGroup>
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>

<ItemGroup>
<SupportedPlatform Include="browser" />
</ItemGroup>

<ItemGroup>
<Reference Include="Microsoft.AspNetCore.Components.Web" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
@page "/reexecution/set-not-found-ssr"
@page "/set-not-found-ssr"
@attribute [StreamRendering(false)]

@*
this page is used in global interactivity and no interactivity scenarios
the content is rendered on the server without streaming and might become
interactive later if interactivity was enabled in the app
*@

<Components.Shared.ComponentThatSetsNotFound />
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
@page "/reexecution/set-not-found-ssr-streaming"
@page "/set-not-found-ssr-streaming"
@attribute [StreamRendering(true)]

@*
this page is used in global interactivity and no interactivity scenarios
the content is rendered on the server with streaming and might become
interactive later if interactivity was enabled in the app
*@

<Components.Shared.ComponentThatSetsNotFound StartStreaming="true"/>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
@using Microsoft.AspNetCore.Components.Web
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
<ProjectReference Include="..\BasicTestApp\BasicTestApp.csproj" />
<ProjectReference Include="..\Components.WasmMinimal\Components.WasmMinimal.csproj" />
<ProjectReference Include="..\Components.WasmRemoteAuthentication\Components.WasmRemoteAuthentication.csproj" />
<ProjectReference Include="..\Components.Shared\Components.Shared.csproj" />
</ItemGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,15 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.Map("/reexecution", reexecutionApp =>
{
app.Map("/trigger-404", trigger404App =>
{
trigger404App.Run(async context =>
{
context.Response.StatusCode = 404;
await context.Response.WriteAsync("Triggered a 404 status code.");
});
});

if (!env.IsDevelopment())
{
app.UseExceptionHandler("/Error", createScopeForErrors: true);
Expand All @@ -62,7 +71,8 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
reexecutionApp.UseAntiforgery();
reexecutionApp.UseEndpoints(endpoints =>
{
endpoints.MapRazorComponents<TRootComponent>();
endpoints.MapRazorComponents<TRootComponent>()
.AddAdditionalAssemblies(Assembly.Load("Components.Shared"));
});
});

Expand All @@ -83,7 +93,8 @@ private void ConfigureSubdirPipeline(IApplicationBuilder app, IWebHostEnvironmen
app.UseAntiforgery();
app.UseEndpoints(endpoints =>
{
endpoints.MapRazorComponents<TRootComponent>();
endpoints.MapRazorComponents<TRootComponent>()
.AddAdditionalAssemblies(Assembly.Load("Components.Shared"));
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,14 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.Map("/reexecution", reexecutionApp =>
{
app.Map("/trigger-404", app =>
{
app.Run(async context =>
{
context.Response.StatusCode = 404;
await context.Response.WriteAsync("Triggered a 404 status code.");
});
});
reexecutionApp.UseStatusCodePagesWithReExecute("/not-found-reexecute", createScopeForErrors: true);
reexecutionApp.UseRouting();

Expand Down Expand Up @@ -125,6 +133,7 @@ private void ConfigureEndpoints(IApplicationBuilder app, IWebHostEnvironment env
}

_ = endpoints.MapRazorComponents<TRootComponent>()
.AddAdditionalAssemblies(Assembly.Load("Components.Shared"))
.AddAdditionalAssemblies(Assembly.Load("Components.WasmMinimal"))
.AddInteractiveServerRenderMode(options =>
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
<HeadOutlet />
</head>
<body>
<Router AppAssembly="@typeof(App).Assembly" NotFoundPage="NotFoundPageType">
<Router AppAssembly="@typeof(App).Assembly" AdditionalAssemblies="new[] { typeof(Components.Shared.NotFoundPage).Assembly }" NotFoundPage="NotFoundPageType">
<Found Context="routeData">
<RouteView RouteData="@routeData" />
<FocusOnNavigate RouteData="@routeData" Selector="[data-focus-on-navigate]" />
Expand Down
Loading
Loading