Skip to content

Commit 08cff58

Browse files
authored
Follow-up: Improve stopping renderer, fix identity template (#61633)
* Remove the hard stop - the whole batch has to finish its rendering. * Change the test expectation - prerendering redirection behaves same as server or wasm redirection. * Add unit tests. * Feedback - improve trimming. * Add stopping renderer tests. * Do not throw on new way of navigation.
1 parent fc3994f commit 08cff58

37 files changed

+383
-144
lines changed

src/Components/Components/src/PublicAPI.Unshipped.txt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,3 @@ static Microsoft.AspNetCore.Components.Infrastructure.RegisterPersistentComponen
1717
static Microsoft.AspNetCore.Components.Infrastructure.ComponentsMetricsServiceCollectionExtensions.AddComponentsMetrics(Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection!
1818
static Microsoft.AspNetCore.Components.Infrastructure.ComponentsMetricsServiceCollectionExtensions.AddComponentsTracing(Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection!
1919
static Microsoft.Extensions.DependencyInjection.SupplyParameterFromPersistentComponentStateProviderServiceCollectionExtensions.AddSupplyValueFromPersistentComponentStateProvider(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection!
20-
virtual Microsoft.AspNetCore.Components.RenderTree.Renderer.SignalRendererToFinishRendering() -> void

src/Components/Components/src/RenderTree/Renderer.cs

Lines changed: 2 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,6 @@ public abstract partial class Renderer : IDisposable, IAsyncDisposable
4949
private bool _rendererIsDisposed;
5050

5151
private bool _hotReloadInitialized;
52-
private bool _rendererIsStopped;
5352

5453
/// <summary>
5554
/// Allows the caller to handle exceptions from the SynchronizationContext when one is available.
@@ -708,12 +707,6 @@ internal void AddToRenderQueue(int componentId, RenderFragment renderFragment)
708707
{
709708
Dispatcher.AssertAccess();
710709

711-
if (_rendererIsStopped)
712-
{
713-
// Once we're stopped, we'll disregard further attempts to queue anything
714-
return;
715-
}
716-
717710
var componentState = GetOptionalComponentState(componentId);
718711
if (componentState == null)
719712
{
@@ -780,22 +773,14 @@ private ComponentState GetRequiredRootComponentState(int componentId)
780773
return componentState;
781774
}
782775

783-
/// <summary>
784-
/// Stop adding render requests to the render queue.
785-
/// </summary>
786-
protected virtual void SignalRendererToFinishRendering()
787-
{
788-
_rendererIsStopped = true;
789-
}
790-
791776
/// <summary>
792777
/// Processes pending renders requests from components if there are any.
793778
/// </summary>
794779
protected virtual void ProcessPendingRender()
795780
{
796-
if (_rendererIsDisposed || _rendererIsStopped)
781+
if (_rendererIsDisposed)
797782
{
798-
// Once we're disposed or stopped, we'll disregard further attempts to render anything
783+
// Once we're disposed, we'll disregard further attempts to render anything
799784
return;
800785
}
801786

src/Components/Components/test/NavigationManagerTest.cs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using System.Buffers;
45
using System.Diagnostics;
6+
using System.Net.Http;
7+
using System.Text;
58
using Microsoft.AspNetCore.Components.Routing;
69
using Microsoft.AspNetCore.InternalTesting;
710

@@ -868,6 +871,22 @@ async ValueTask HandleLocationChanging(LocationChangingContext context)
868871
}
869872
}
870873

874+
[Fact]
875+
public void OnNotFoundSubscriptionIsTriggeredWhenNotFoundCalled()
876+
{
877+
// Arrange
878+
var baseUri = "scheme://host/";
879+
var testNavManager = new TestNavigationManager(baseUri);
880+
bool notFoundTriggered = false;
881+
testNavManager.OnNotFound += (sender, args) => notFoundTriggered = true;
882+
883+
// Simulate a component triggered NotFound
884+
testNavManager.NotFound();
885+
886+
// Assert
887+
Assert.True(notFoundTriggered, "The OnNotFound event was not triggered as expected.");
888+
}
889+
871890
private class TestNavigationManager : NavigationManager
872891
{
873892
public TestNavigationManager()

src/Components/Endpoints/src/DependencyInjection/HttpNavigationManager.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,15 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using Microsoft.AspNetCore.Components.Routing;
5+
using System.Diagnostics.CodeAnalysis;
56

67
namespace Microsoft.AspNetCore.Components.Endpoints;
78

89
internal sealed class HttpNavigationManager : NavigationManager, IHostEnvironmentNavigationManager
910
{
1011
private const string _enableThrowNavigationException = "Microsoft.AspNetCore.Components.Endpoints.NavigationManager.EnableThrowNavigationException";
1112

13+
[FeatureSwitchDefinition(_enableThrowNavigationException)]
1214
private static bool _throwNavigationException =>
1315
AppContext.TryGetSwitch(_enableThrowNavigationException, out var switchValue) && switchValue;
1416

src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.EventDispatch.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ private async Task SetNotFoundResponseAsync(string baseUri)
9797
// When the application triggers a NotFound event, we continue rendering the current batch.
9898
// However, after completing this batch, we do not want to process any further UI updates,
9999
// as we are going to return a 404 status and discard the UI updates generated so far.
100-
SignalRendererToFinishRenderingAfterCurrentBatch();
100+
SignalRendererToFinishRendering();
101101
}
102102

103103
private async Task OnNavigateTo(string uri)

src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.cs

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -183,19 +183,12 @@ protected override void AddPendingTask(ComponentState? componentState, Task task
183183
base.AddPendingTask(componentState, task);
184184
}
185185

186-
private void SignalRendererToFinishRenderingAfterCurrentBatch()
186+
private void SignalRendererToFinishRendering()
187187
{
188188
// sets a deferred stop on the renderer, which will have an effect after the current batch is completed
189189
_rendererIsStopped = true;
190190
}
191191

192-
protected override void SignalRendererToFinishRendering()
193-
{
194-
SignalRendererToFinishRenderingAfterCurrentBatch();
195-
// sets a hard stop on the renderer, which will have an effect immediately
196-
base.SignalRendererToFinishRendering();
197-
}
198-
199192
protected override void ProcessPendingRender()
200193
{
201194
if (_rendererIsStopped)

src/Components/Endpoints/test/EndpointHtmlRendererTest.cs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,21 @@ public EndpointHtmlRendererTest()
4646
renderer = GetEndpointHtmlRenderer();
4747
}
4848

49+
[Fact]
50+
public async Task DoesNotRenderChildAfterRendererStopped()
51+
{
52+
renderer.SignalRendererToFinishRendering();
53+
54+
var httpContext = GetHttpContext();
55+
var writer = new StringWriter();
56+
57+
var result = await renderer.PrerenderComponentAsync(httpContext, typeof(SimpleComponent), null, ParameterView.Empty);
58+
await renderer.Dispatcher.InvokeAsync(() => result.WriteTo(writer, HtmlEncoder.Default));
59+
var content = writer.ToString();
60+
61+
Assert.DoesNotContain("Hello from SimpleComponent", content);
62+
}
63+
4964
[Fact]
5065
public async Task CanRender_ParameterlessComponent_ClientMode()
5166
{
@@ -1756,6 +1771,7 @@ private TestEndpointHtmlRenderer GetEndpointHtmlRenderer(IServiceProvider servic
17561771

17571772
private class TestEndpointHtmlRenderer : EndpointHtmlRenderer
17581773
{
1774+
private bool _rendererIsStopped = false;
17591775
public TestEndpointHtmlRenderer(IServiceProvider serviceProvider, ILoggerFactory loggerFactory) : base(serviceProvider, loggerFactory)
17601776
{
17611777
}
@@ -1764,6 +1780,20 @@ internal int TestAssignRootComponentId(IComponent component)
17641780
{
17651781
return base.AssignRootComponentId(component);
17661782
}
1783+
public void SignalRendererToFinishRendering()
1784+
{
1785+
// sets a deferred stop on the renderer, which will have an effect after the current batch is completed
1786+
_rendererIsStopped = true;
1787+
}
1788+
1789+
protected override void ProcessPendingRender()
1790+
{
1791+
if (_rendererIsStopped)
1792+
{
1793+
return;
1794+
}
1795+
base.ProcessPendingRender();
1796+
}
17671797
}
17681798

17691799
private HttpContext GetHttpContext(HttpContext context = null)

src/Components/Server/src/Circuits/RemoteNavigationManager.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ internal sealed partial class RemoteNavigationManager : NavigationManager, IHost
1818
private IJSRuntime _jsRuntime;
1919
private bool? _navigationLockStateBeforeJsRuntimeAttached;
2020
private const string _enableThrowNavigationException = "Microsoft.AspNetCore.Components.Endpoints.NavigationManager.EnableThrowNavigationException";
21+
22+
[FeatureSwitchDefinition("Microsoft.AspNetCore.Components.Endpoints.NavigationManager.EnableThrowNavigationException")]
2123
private static bool _throwNavigationException =>
2224
AppContext.TryGetSwitch(_enableThrowNavigationException, out var switchValue) && switchValue;
2325
private Func<string, Task>? _onNavigateTo;

src/Components/test/E2ETest/ServerExecutionTests/PrerenderingTest.cs

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -97,24 +97,6 @@ public void CanReadUrlHashOnlyOnceConnected()
9797
() => Browser.Exists(By.TagName("strong")).Text);
9898
}
9999

100-
[Theory]
101-
[InlineData("base/relative", "prerendered/base/relative")]
102-
[InlineData("/root/relative", "/root/relative")]
103-
[InlineData("http://absolute/url", "http://absolute/url")]
104-
public async Task CanRedirectDuringPrerendering(string destinationParam, string expectedRedirectionLocation)
105-
{
106-
var requestUri = new Uri(
107-
_serverFixture.RootUri,
108-
"prerendered/prerendered-redirection?destination=" + destinationParam);
109-
110-
var httpClient = new HttpClient(new HttpClientHandler { AllowAutoRedirect = false });
111-
var response = await httpClient.GetAsync(requestUri);
112-
113-
var expectedUri = new Uri(_serverFixture.RootUri, expectedRedirectionLocation);
114-
Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
115-
Assert.Equal(expectedUri, response.Headers.Location);
116-
}
117-
118100
[Theory]
119101
[InlineData(null, null)]
120102
[InlineData(null, "Bert")]

src/Components/test/E2ETest/ServerRenderingTests/RenderingTest.cs

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,4 +66,53 @@ public void PostRequestRendersEndStateOfComponentsOnSSRPage()
6666

6767
Browser.Equal("loaded child", () => Browser.Exists(By.Id("child")).Text);
6868
}
69+
70+
[Theory]
71+
[InlineData(false, "ServerPrerendered", true)]
72+
[InlineData(false, "ServerPrerendered", false)]
73+
[InlineData(true, "ServerPrerendered", false)]
74+
[InlineData(true, "ServerNonPrerendered", false)]
75+
[InlineData(true, "WebAssemblyPrerendered", false)]
76+
[InlineData(true, "WebAssemblyNonPrerendered", false)]
77+
public async Task RenderBatchQueuedAfterRedirectionIsNotProcessed(bool redirect, string renderMode, bool throwSync)
78+
{
79+
string relativeUri = $"subdir/stopping-renderer?renderMode={renderMode}";
80+
if (redirect)
81+
{
82+
relativeUri += $"&destination=redirect";
83+
}
84+
85+
// async operation forces the next render batch
86+
if (throwSync)
87+
{
88+
relativeUri += $"&delay=0";
89+
}
90+
else
91+
{
92+
relativeUri += $"&delay=1";
93+
}
94+
95+
var requestUri = new Uri(_serverFixture.RootUri, relativeUri);
96+
var httpClient = new HttpClient(new HttpClientHandler { AllowAutoRedirect = false });
97+
var response = await httpClient.GetAsync(requestUri);
98+
99+
if (redirect)
100+
{
101+
var expectedUri = new Uri(_serverFixture.RootUri, "subdir/redirect");
102+
Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
103+
Assert.Equal(expectedUri, response.Headers.Location);
104+
}
105+
else
106+
{
107+
// the status code cannot be changed after it got set, so async throwing returns OK
108+
if (throwSync)
109+
{
110+
Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode);
111+
}
112+
else
113+
{
114+
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
115+
}
116+
}
117+
}
69118
}

src/Components/test/testassets/Components.TestServer/RazorComponents/Components/InteractiveStreamingRenderingComponent.razor

Lines changed: 1 addition & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ else
5959
object key = DisableKeys ? null : counter.Id;
6060

6161
<Counter
62-
@rendermode="@GetRenderMode(counter.RenderModeId)"
62+
@rendermode="@RenderModeHelper.GetRenderMode(counter.RenderModeId)"
6363
@key="@key"
6464
IdSuffix="@counter.Id.ToString()"
6565
IncrementAmount="counter.IncrementAmount"
@@ -201,30 +201,6 @@ else
201201
$"&{nameof(DisableKeys)}={disableKeys}";
202202
}
203203

204-
private static IComponentRenderMode GetRenderMode(RenderModeId renderMode)
205-
{
206-
return renderMode switch
207-
{
208-
RenderModeId.ServerPrerendered => RenderMode.InteractiveServer,
209-
RenderModeId.ServerNonPrerendered => new InteractiveServerRenderMode(false),
210-
RenderModeId.WebAssemblyPrerendered => RenderMode.InteractiveWebAssembly,
211-
RenderModeId.WebAssemblyNonPrerendered => new InteractiveWebAssemblyRenderMode(false),
212-
RenderModeId.AutoPrerendered => RenderMode.InteractiveAuto,
213-
RenderModeId.AutoNonPrerendered => new InteractiveAutoRenderMode(false),
214-
_ => throw new InvalidOperationException($"Unknown render mode: {renderMode}"),
215-
};
216-
}
217-
218-
private enum RenderModeId
219-
{
220-
ServerPrerendered = 0,
221-
ServerNonPrerendered = 1,
222-
WebAssemblyPrerendered = 2,
223-
WebAssemblyNonPrerendered = 3,
224-
AutoPrerendered = 4,
225-
AutoNonPrerendered = 5,
226-
}
227-
228204
private record struct CounterInfo(int Id, int IncrementAmount, RenderModeId RenderModeId);
229205

230206
private record ComponentState(ImmutableArray<CounterInfo> Counters, int NextCounterId)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
@if(throwException)
2+
{
3+
throw new InvalidOperationException("Child component UI exception: redirection should have stopped renderer.");
4+
}
5+
6+
@code {
7+
[Parameter]
8+
public int Delay { get; set; }
9+
10+
private bool throwException { get; set; }
11+
12+
private string message = string.Empty;
13+
14+
protected override async Task OnInitializedAsync()
15+
{
16+
await Task.Yield();
17+
_ = ScheduleRenderingExceptionAfterDelay();
18+
}
19+
20+
private async Task ScheduleRenderingExceptionAfterDelay()
21+
{
22+
// This update should not happen if the renderer is stopped
23+
await Task.Delay(Delay);
24+
throwException = true;
25+
StateHasChanged();
26+
}
27+
}
28+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
@page "/stopping-renderer"
2+
@inject NavigationManager NavigationManager
3+
4+
<p>Parent content</p>
5+
<AsyncComponent @rendermode="@RenderModeHelper.GetRenderMode(CurrentRenderMode)" Delay="@Delay" />
6+
7+
@code {
8+
[Parameter, SupplyParameterFromQuery(Name = "destination")]
9+
public string Destination { get; set; } = string.Empty;
10+
11+
[Parameter, SupplyParameterFromQuery(Name = "renderMode")]
12+
public string? RenderModeStr { get; set; }
13+
14+
[Parameter, SupplyParameterFromQuery(Name = "delay")]
15+
public int Delay { get; set; }
16+
17+
private RenderModeId CurrentRenderMode => RenderModeHelper.ParseRenderMode(RenderModeStr);
18+
19+
protected override Task OnInitializedAsync()
20+
{
21+
if (!string.IsNullOrEmpty(Destination))
22+
{
23+
NavigationManager.NavigateTo(Destination);
24+
}
25+
return Task.CompletedTask;
26+
}
27+
}

0 commit comments

Comments
 (0)