Skip to content

Commit 30ef19c

Browse files
[release/8.0] [Blazor] Invoke inbound activity handlers on circuit initialization (#57715)
Backport of #57557 to release/8.0 # [Blazor] Invoke inbound activity handlers on circuit initialization Fixes an issue where inbound activity handlers don't get invoked on circuit initialization. > [!NOTE] > This bug only affects Blazor Server apps, _not_ Blazor Web apps utilizing server interactivity ## Description Inbound activity handlers were added in .NET 8 to enable: * Monitoring inbound circuit activity * Enabling server-side Blazor services to be [accessed from a different DI scope](https://learn.microsoft.com/aspnet/core/blazor/fundamentals/dependency-injection?view=aspnetcore-8.0#access-server-side-blazor-services-from-a-different-di-scope) However, prior to the fix in this PR, this feature didn't apply to the first interactive render after the initial page load. This means that when utilizing this feature to access Blazor services from a different DI scope, the service might only become accessible after subsequent renders, not the initial render. This PR makes the following changes: * Updated `CircuitHost` to invoke inbound activity handlers on circuit initialization * Added an extra test to verify that inbound activity handlers work on the initial page load * Updated existing Blazor Web tests to reuse test logic from the non-web tests * This helps to ensure that the feature works the same way on Blazor Server and Blazor Web Fixes #57481 ## Customer Impact The [initial issue report](#57481) was from a customer who was impacted experiencing this problem in their app. The problem does not inherently cause an app to stop working, but if the application code has made the (rightful) assumption that the service accessor is initialized, then session may crash. The workaround is to upgrade the app to use the "Blazor Web App" pattern, although this can be a fairly large change. ## Regression? - [ ] Yes - [X] No The problem has existed since the introduction of the feature in .NET 8. ## Risk - [ ] High - [ ] Medium - [X] Low The change is straightforward, and new tests have been added to ensure that it addresses the issue. Existing tests verify that a new regression is not introduced. ## Verification - [X] Manual (required) - [X] Automated ## Packaging changes reviewed? - [ ] Yes - [ ] No - [x] N/A
1 parent 23cb501 commit 30ef19c

File tree

6 files changed

+39
-38
lines changed

6 files changed

+39
-38
lines changed

src/Components/Server/src/Circuits/CircuitHost.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ public Task InitializeAsync(ProtectedPrerenderComponentApplicationStore store, C
103103
{
104104
Log.InitializationStarted(_logger);
105105

106-
return Renderer.Dispatcher.InvokeAsync(async () =>
106+
return HandleInboundActivityAsync(() => Renderer.Dispatcher.InvokeAsync(async () =>
107107
{
108108
if (_initialized)
109109
{
@@ -164,7 +164,7 @@ public Task InitializeAsync(ProtectedPrerenderComponentApplicationStore store, C
164164
UnhandledException?.Invoke(this, new UnhandledExceptionEventArgs(ex, isTerminating: false));
165165
await TryNotifyClientErrorAsync(Client, GetClientErrorMessage(ex), ex);
166166
}
167-
});
167+
}));
168168
}
169169

170170
// We handle errors in DisposeAsync because there's no real value in letting it propagate.

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

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -22,28 +22,39 @@ public CircuitContextTest(
2222
{
2323
}
2424

25-
protected override void InitializeAsyncCore()
25+
[Fact]
26+
public void ComponentMethods_HaveCircuitContext()
2627
{
2728
Navigate(ServerPathBase, noReload: false);
2829
Browser.MountTestComponent<CircuitContextComponent>();
29-
Browser.Equal("Circuit Context", () => Browser.Exists(By.TagName("h1")).Text);
30+
TestCircuitContextCore(Browser);
3031
}
3132

3233
[Fact]
33-
public void ComponentMethods_HaveCircuitContext()
34+
public void ComponentMethods_HaveCircuitContext_OnInitialPageLoad()
3435
{
35-
Browser.Click(By.Id("trigger-click-event-button"));
36+
// https://github.com/dotnet/aspnetcore/issues/57481
37+
Navigate($"{ServerPathBase}?initial-component-type={typeof(CircuitContextComponent).AssemblyQualifiedName}");
38+
TestCircuitContextCore(Browser);
39+
}
40+
41+
// Internal for reuse in Blazor Web tests
42+
internal static void TestCircuitContextCore(IWebDriver browser)
43+
{
44+
browser.Equal("Circuit Context", () => browser.Exists(By.TagName("h1")).Text);
45+
46+
browser.Click(By.Id("trigger-click-event-button"));
3647

37-
Browser.True(() => HasCircuitContext("SetParametersAsync"));
38-
Browser.True(() => HasCircuitContext("OnInitializedAsync"));
39-
Browser.True(() => HasCircuitContext("OnParametersSetAsync"));
40-
Browser.True(() => HasCircuitContext("OnAfterRenderAsync"));
41-
Browser.True(() => HasCircuitContext("InvokeDotNet"));
42-
Browser.True(() => HasCircuitContext("OnClickEvent"));
48+
browser.True(() => HasCircuitContext("SetParametersAsync"));
49+
browser.True(() => HasCircuitContext("OnInitializedAsync"));
50+
browser.True(() => HasCircuitContext("OnParametersSetAsync"));
51+
browser.True(() => HasCircuitContext("OnAfterRenderAsync"));
52+
browser.True(() => HasCircuitContext("InvokeDotNet"));
53+
browser.True(() => HasCircuitContext("OnClickEvent"));
4354

4455
bool HasCircuitContext(string eventName)
4556
{
46-
var resultText = Browser.FindElement(By.Id($"circuit-context-result-{eventName}")).Text;
57+
var resultText = browser.FindElement(By.Id($"circuit-context-result-{eventName}")).Text;
4758
var result = bool.Parse(resultText);
4859
return result;
4960
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using Components.TestServer.RazorComponents;
55
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure;
66
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures;
7+
using Microsoft.AspNetCore.Components.E2ETests.ServerExecutionTests;
78
using Microsoft.AspNetCore.E2ETesting;
89
using Microsoft.AspNetCore.Testing;
910
using OpenQA.Selenium;
@@ -1168,8 +1169,7 @@ public void NavigationManagerCanRefreshSSRPageWhenServerInteractivityEnabled()
11681169
public void InteractiveServerRootComponent_CanAccessCircuitContext()
11691170
{
11701171
Navigate($"{ServerPathBase}/interactivity/circuit-context");
1171-
1172-
Browser.Equal("True", () => Browser.FindElement(By.Id("has-circuit-context")).Text);
1172+
CircuitContextTest.TestCircuitContextCore(Browser);
11731173
}
11741174

11751175
[Fact]

src/Components/test/testassets/BasicTestApp/Index.razor

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
@using Microsoft.AspNetCore.Components.Rendering
2+
@using System.Web
3+
@inject NavigationManager NavigationManager
24
<div id="test-selector">
35
Select test:
46
<select id="test-selector-select" @bind=SelectedComponentTypeName>
@@ -135,6 +137,15 @@
135137
Type SelectedComponentType
136138
=> SelectedComponentTypeName == "none" ? null : Type.GetType(SelectedComponentTypeName, throwOnError: true);
137139

140+
protected override void OnInitialized()
141+
{
142+
var uri = new Uri(NavigationManager.Uri);
143+
if (HttpUtility.ParseQueryString(uri.Query)["initial-component-type"] is { Length: > 0 } initialComponentTypeName)
144+
{
145+
SelectedComponentTypeName = initialComponentTypeName;
146+
}
147+
}
148+
138149
void RenderSelectedComponent(RenderTreeBuilder builder)
139150
{
140151
if (SelectedComponentType != null)

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
</Router>
1818
<script src="_framework/blazor.web.js" autostart="false" suppress-error="BL9992"></script>
1919
<script src="_content/TestContentPackage/counterInterop.js"></script>
20+
<script src="js/circuitContextTest.js"></script>
2021
<script>
2122
const enableClassicInitializers = sessionStorage.getItem('enable-classic-initializers') === 'true';
2223
const suppressEnhancedNavigation = sessionStorage.getItem('suppress-enhanced-navigation') === 'true';
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,3 @@
11
@page "/interactivity/circuit-context"
2-
@rendermode RenderMode.InteractiveServer
3-
@inject TestCircuitContextAccessor CircuitContextAccessor
42

5-
<h1>Circuit context</h1>
6-
7-
<p>
8-
Has circuit context: <span id="has-circuit-context">@_hasCircuitContext</span>
9-
</p>
10-
11-
@code {
12-
private bool _hasCircuitContext;
13-
14-
protected override async Task OnAfterRenderAsync(bool firstRender)
15-
{
16-
if (firstRender)
17-
{
18-
await Task.Yield();
19-
20-
_hasCircuitContext = CircuitContextAccessor.HasCircuitContext;
21-
22-
StateHasChanged();
23-
}
24-
}
25-
}
3+
<CircuitContextComponent @rendermode="RenderMode.InteractiveServer" />

0 commit comments

Comments
 (0)