Skip to content

Commit

Permalink
[release/8.0] [Blazor] Invoke inbound activity handlers on circuit in…
Browse files Browse the repository at this point in the history
…itialization (#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
  • Loading branch information
github-actions[bot] authored Sep 13, 2024
1 parent 23cb501 commit 30ef19c
Show file tree
Hide file tree
Showing 6 changed files with 39 additions and 38 deletions.
4 changes: 2 additions & 2 deletions src/Components/Server/src/Circuits/CircuitHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ public Task InitializeAsync(ProtectedPrerenderComponentApplicationStore store, C
{
Log.InitializationStarted(_logger);

return Renderer.Dispatcher.InvokeAsync(async () =>
return HandleInboundActivityAsync(() => Renderer.Dispatcher.InvokeAsync(async () =>
{
if (_initialized)
{
Expand Down Expand Up @@ -164,7 +164,7 @@ public Task InitializeAsync(ProtectedPrerenderComponentApplicationStore store, C
UnhandledException?.Invoke(this, new UnhandledExceptionEventArgs(ex, isTerminating: false));
await TryNotifyClientErrorAsync(Client, GetClientErrorMessage(ex), ex);
}
});
}));
}

// We handle errors in DisposeAsync because there's no real value in letting it propagate.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,28 +22,39 @@ public CircuitContextTest(
{
}

protected override void InitializeAsyncCore()
[Fact]
public void ComponentMethods_HaveCircuitContext()
{
Navigate(ServerPathBase, noReload: false);
Browser.MountTestComponent<CircuitContextComponent>();
Browser.Equal("Circuit Context", () => Browser.Exists(By.TagName("h1")).Text);
TestCircuitContextCore(Browser);
}

[Fact]
public void ComponentMethods_HaveCircuitContext()
public void ComponentMethods_HaveCircuitContext_OnInitialPageLoad()
{
Browser.Click(By.Id("trigger-click-event-button"));
// https://github.com/dotnet/aspnetcore/issues/57481
Navigate($"{ServerPathBase}?initial-component-type={typeof(CircuitContextComponent).AssemblyQualifiedName}");
TestCircuitContextCore(Browser);
}

// Internal for reuse in Blazor Web tests
internal static void TestCircuitContextCore(IWebDriver browser)
{
browser.Equal("Circuit Context", () => browser.Exists(By.TagName("h1")).Text);

browser.Click(By.Id("trigger-click-event-button"));

Browser.True(() => HasCircuitContext("SetParametersAsync"));
Browser.True(() => HasCircuitContext("OnInitializedAsync"));
Browser.True(() => HasCircuitContext("OnParametersSetAsync"));
Browser.True(() => HasCircuitContext("OnAfterRenderAsync"));
Browser.True(() => HasCircuitContext("InvokeDotNet"));
Browser.True(() => HasCircuitContext("OnClickEvent"));
browser.True(() => HasCircuitContext("SetParametersAsync"));
browser.True(() => HasCircuitContext("OnInitializedAsync"));
browser.True(() => HasCircuitContext("OnParametersSetAsync"));
browser.True(() => HasCircuitContext("OnAfterRenderAsync"));
browser.True(() => HasCircuitContext("InvokeDotNet"));
browser.True(() => HasCircuitContext("OnClickEvent"));

bool HasCircuitContext(string eventName)
{
var resultText = Browser.FindElement(By.Id($"circuit-context-result-{eventName}")).Text;
var resultText = browser.FindElement(By.Id($"circuit-context-result-{eventName}")).Text;
var result = bool.Parse(resultText);
return result;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using Components.TestServer.RazorComponents;
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure;
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures;
using Microsoft.AspNetCore.Components.E2ETests.ServerExecutionTests;
using Microsoft.AspNetCore.E2ETesting;
using Microsoft.AspNetCore.Testing;
using OpenQA.Selenium;
Expand Down Expand Up @@ -1168,8 +1169,7 @@ public void NavigationManagerCanRefreshSSRPageWhenServerInteractivityEnabled()
public void InteractiveServerRootComponent_CanAccessCircuitContext()
{
Navigate($"{ServerPathBase}/interactivity/circuit-context");

Browser.Equal("True", () => Browser.FindElement(By.Id("has-circuit-context")).Text);
CircuitContextTest.TestCircuitContextCore(Browser);
}

[Fact]
Expand Down
11 changes: 11 additions & 0 deletions src/Components/test/testassets/BasicTestApp/Index.razor
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
@using Microsoft.AspNetCore.Components.Rendering
@using System.Web
@inject NavigationManager NavigationManager
<div id="test-selector">
Select test:
<select id="test-selector-select" @bind=SelectedComponentTypeName>
Expand Down Expand Up @@ -135,6 +137,15 @@
Type SelectedComponentType
=> SelectedComponentTypeName == "none" ? null : Type.GetType(SelectedComponentTypeName, throwOnError: true);

protected override void OnInitialized()
{
var uri = new Uri(NavigationManager.Uri);
if (HttpUtility.ParseQueryString(uri.Query)["initial-component-type"] is { Length: > 0 } initialComponentTypeName)
{
SelectedComponentTypeName = initialComponentTypeName;
}
}

void RenderSelectedComponent(RenderTreeBuilder builder)
{
if (SelectedComponentType != null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
</Router>
<script src="_framework/blazor.web.js" autostart="false" suppress-error="BL9992"></script>
<script src="_content/TestContentPackage/counterInterop.js"></script>
<script src="js/circuitContextTest.js"></script>
<script>
const enableClassicInitializers = sessionStorage.getItem('enable-classic-initializers') === 'true';
const suppressEnhancedNavigation = sessionStorage.getItem('suppress-enhanced-navigation') === 'true';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,25 +1,3 @@
@page "/interactivity/circuit-context"
@rendermode RenderMode.InteractiveServer
@inject TestCircuitContextAccessor CircuitContextAccessor

<h1>Circuit context</h1>

<p>
Has circuit context: <span id="has-circuit-context">@_hasCircuitContext</span>
</p>

@code {
private bool _hasCircuitContext;

protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
await Task.Yield();

_hasCircuitContext = CircuitContextAccessor.HasCircuitContext;

StateHasChanged();
}
}
}
<CircuitContextComponent @rendermode="RenderMode.InteractiveServer" />

0 comments on commit 30ef19c

Please sign in to comment.