Is there an existing issue for this?
Description
Environment
- ABP 10.6.0 (verified against
dev source and the shipped 10.6.0 assembly)
- Blazor Web App,
@rendermode="InteractiveAuto"
- Autofac (
Volo.Abp.Autofac 10.6.0)
- .NET SDK 10.0.400
Summary
Volo.Abp.AspNetCore.Components.Web.Theming is compiled with ConfigureAwait.Fody configured as
ContinueOnCapturedContext="false". Every await in the assembly — including in Blazor component
lifecycle methods — is therefore rewritten to ConfigureAwait(false).
As a result, PageHeader.OnParametersSetAsync resumes off the Blazor renderer's synchronization
context and then mutates PageLayout.ToolbarItems, an ObservableCollection<T> that other
components subscribe to. Those subscribers' CollectionChanged / PropertyChanged handlers are
consequently invoked on a thread-pool thread rather than on the renderer's dispatcher.
This is a Blazor threading-contract violation: state that triggers renders in other components is
being changed off-dispatcher.
Evidence
framework/src/Volo.Abp.AspNetCore.Components.Web.Theming/FodyWeavers.xml:
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
<ConfigureAwait ContinueOnCapturedContext="false" />
</Weavers>
Confirmed in the shipped 10.6.0 assembly: Volo.Abp.AspNetCore.Components.Web.Theming.dll
contains ConfiguredTaskAwaitable references, i.e. the weaving took effect. There is no explicit
ConfigureAwait(false) in the source — it is applied at the IL level.
framework/src/Volo.Abp.AspNetCore.Components.Web.Theming/Layout/PageHeader.razor.cs:
protected async override Task OnParametersSetAsync()
{
await base.OnParametersSetAsync();
if (Toolbar != null)
{
var toolbarItems = await PageToolbarManager.GetItemsAsync(Toolbar); // woven ConfigureAwait(false)
if (!ShouldRenderToolbarItems(toolbarItems))
{
return;
}
ToolbarItemRenders.Clear();
if (!Options.Value.RenderToolbar)
{
PageLayout.ToolbarItems.Clear(); // <-- off-dispatcher, raises CollectionChanged
foreach (var item in toolbarItems)
{
PageLayout.ToolbarItems.Add(item); // <-- off-dispatcher, raises CollectionChanged
}
return;
}
...
}
}
framework/src/Volo.Abp.AspNetCore.Components.Web.Theming/Layout/PageLayout.cs confirms the
observable contract and the scoped lifetime:
public class PageLayout : IScopedDependency, INotifyPropertyChanged
{
public virtual ObservableCollection<BreadcrumbItem> BreadcrumbItems { get; } = new();
public virtual ObservableCollection<PageToolbarItem> ToolbarItems { get; } = new();
public event PropertyChangedEventHandler? PropertyChanged;
public void Reset()
{
Title = string.Empty; // raises PropertyChanged
MenuItemName = string.Empty; // raises PropertyChanged
BreadcrumbItems.Clear(); // raises CollectionChanged
ToolbarItems.Clear(); // raises CollectionChanged
}
}
Interaction with Dispose (context, not a request to revert)
public void Dispose()
{
PageLayout.Reset();
ToolbarItemRenders.Clear();
}
PageHeader.Dispose() calls PageLayout.Reset(), which raises PropertyChanged and two
CollectionChanged events. When the component is disposed because the scope itself is being
torn down, those notifications reach subscribers whose service scope is already (or is about to
be) disposed.
To be clear: this behaviour is deliberate and I am not asking for it to be reverted — see
#21185 / #21888 (clear breadcrumbs and toolbar items on dispose) and #22725 (clear title on
dispose). I mention it only because it is a second path by which PageLayout notifications are
delivered at a point where subscribers may already be gone, and any fix for the off-dispatcher
issue above should keep this path in mind.
Note also the author's own caveat on #21888: "This may cause other issues with multiple
PageHeader component usage in a same page."
Impact
Any component subscribing to PageLayout receives notifications on a thread-pool thread and,
via Dispose, during scope teardown. Whether this is survivable depends entirely on how
defensively the subscriber is written.
The LeptonX theme's ContentToolbar and Breadcrumbs subscribe with anonymous async void
handlers and never unsubscribe. In a Blazor Web App using InteractiveAuto this combination
terminates the host process with an unhandled ObjectDisposedException raised on a
thread-pool thread:
Unhandled exception. System.ObjectDisposedException: Instances cannot be resolved and nested
lifetimes cannot be created from this LifetimeScope as it (or one of its parent scopes) has
already been disposed.
at Autofac.Extensions.DependencyInjection.AutofacServiceProvider.GetService(Type serviceType)
at Blazorise.ComponentActivator.CreateInstance(Type componentType)
...
at Microsoft.AspNetCore.Components.ComponentBase.StateHasChanged()
at Microsoft.AspNetCore.Components.Rendering.RendererSynchronizationContext.<InvokeAsync>g__Execute|8_0(...)
at Volo.Abp.AspNetCore.Components.Web.LeptonXTheme.Components.ApplicationLayout.Common.ContentToolbar.<obfuscated>(Object s, PropertyChangedEventArgs)
at System.Threading.Tasks.Task.<>c.<ThrowAsync>b__124_1(Object state)
at System.Threading.ThreadPoolWorkQueue.Dispatch()
The LeptonX side of this is being reported separately through commercial support, since that
package is not in this repository. This issue is about the framework side: the off-dispatcher
mutation that puts the subscriber on a thread-pool thread in the first place.
Prior related issues
I searched the tracker before filing; I could not find this reported. For reviewers, the
adjacent history is:
None of these concern the synchronization context the mutation happens on, which is what this
issue is about. A search for ContentToolbar in this repository returns no results, and I found
no issue describing off-dispatcher PageLayout mutation or the effect of the assembly-wide
ConfigureAwait(false) weaving on Blazor component lifecycle methods.
Suggested fix
Either of:
- Exclude Blazor component types (or the whole component assembly) from the blanket
ConfigureAwait(false) weaving — component lifecycle methods should resume on the dispatcher.
ConfigureAwait.Fody supports opting out per class/method with [ConfigureAwait(true)].
- Or apply the
PageLayout mutations through InvokeAsync(...) so they are marshalled back onto
the dispatcher regardless of how the preceding await resumed.
Option 1 is the more general fix; a blanket ConfigureAwait(false) across an assembly containing
ComponentBase subclasses is risky beyond this specific case.
Whichever route is taken, please consider the Dispose path described above as well: it delivers
the same notifications at teardown time, and it is intentional behaviour that should be preserved.
Reproduction Steps
No response
Expected behavior
PageHeader should mutate PageLayout on the renderer's synchronization context, so that
subscribers' change notifications run on the dispatcher like any other Blazor state change.
Actual behavior
No response
Regression?
No response
Known Workarounds
No response
Version
10.6.0
User Interface
Blazor Server
Database Provider
EF Core (Default)
Tiered or separate authentication server
None (Default)
Operation System
Windows (Default)
Other information
I ran into a problem within our application and Claude found two problems regarding LeptonX (reported on support page: https://abp.io/support/questions/10861/LeptonX-ContentToolbarBreadcrumbs-never-unsubscribe-from-PageLayout--async-void-handler-terminates-the-host-process-ObjectDisposedException) and the PageHeader/PageLayout components.
Is there an existing issue for this?
Description
Environment
devsource and the shipped 10.6.0 assembly)@rendermode="InteractiveAuto"Volo.Abp.Autofac10.6.0)Summary
Volo.Abp.AspNetCore.Components.Web.Themingis compiled with ConfigureAwait.Fody configured asContinueOnCapturedContext="false". Everyawaitin the assembly — including in Blazor componentlifecycle methods — is therefore rewritten to
ConfigureAwait(false).As a result,
PageHeader.OnParametersSetAsyncresumes off the Blazor renderer's synchronizationcontext and then mutates
PageLayout.ToolbarItems, anObservableCollection<T>that othercomponents subscribe to. Those subscribers'
CollectionChanged/PropertyChangedhandlers areconsequently invoked on a thread-pool thread rather than on the renderer's dispatcher.
This is a Blazor threading-contract violation: state that triggers renders in other components is
being changed off-dispatcher.
Evidence
framework/src/Volo.Abp.AspNetCore.Components.Web.Theming/FodyWeavers.xml:Confirmed in the shipped 10.6.0 assembly:
Volo.Abp.AspNetCore.Components.Web.Theming.dllcontains
ConfiguredTaskAwaitablereferences, i.e. the weaving took effect. There is no explicitConfigureAwait(false)in the source — it is applied at the IL level.framework/src/Volo.Abp.AspNetCore.Components.Web.Theming/Layout/PageHeader.razor.cs:framework/src/Volo.Abp.AspNetCore.Components.Web.Theming/Layout/PageLayout.csconfirms theobservable contract and the scoped lifetime:
Interaction with
Dispose(context, not a request to revert)PageHeader.Dispose()callsPageLayout.Reset(), which raisesPropertyChangedand twoCollectionChangedevents. When the component is disposed because the scope itself is beingtorn down, those notifications reach subscribers whose service scope is already (or is about to
be) disposed.
To be clear: this behaviour is deliberate and I am not asking for it to be reverted — see
#21185 / #21888 (clear breadcrumbs and toolbar items on dispose) and #22725 (clear title on
dispose). I mention it only because it is a second path by which
PageLayoutnotifications aredelivered at a point where subscribers may already be gone, and any fix for the off-dispatcher
issue above should keep this path in mind.
Note also the author's own caveat on #21888: "This may cause other issues with multiple
PageHeader component usage in a same page."
Impact
Any component subscribing to
PageLayoutreceives notifications on a thread-pool thread and,via
Dispose, during scope teardown. Whether this is survivable depends entirely on howdefensively the subscriber is written.
The LeptonX theme's
ContentToolbarandBreadcrumbssubscribe with anonymousasync voidhandlers and never unsubscribe. In a Blazor Web App using
InteractiveAutothis combinationterminates the host process with an unhandled
ObjectDisposedExceptionraised on athread-pool thread:
The LeptonX side of this is being reported separately through commercial support, since that
package is not in this repository. This issue is about the framework side: the off-dispatcher
mutation that puts the subscriber on a thread-pool thread in the first place.
Prior related issues
I searched the tracker before filing; I could not find this reported. For reviewers, the
adjacent history is:
PageHeadermadeIDisposableto clear breadcrumbs and toolbar items ondispose.
Titleon dispose.OnParametersSetAsyncmethod ofPageHeaderfrequently checks permissions. #24608 —PageHeader.OnParametersSetAsyncchecking permissions on every parameter set(origin of the current
ShouldRenderToolbarItemsshort-circuit).None of these concern the synchronization context the mutation happens on, which is what this
issue is about. A search for
ContentToolbarin this repository returns no results, and I foundno issue describing off-dispatcher
PageLayoutmutation or the effect of the assembly-wideConfigureAwait(false)weaving on Blazor component lifecycle methods.Suggested fix
Either of:
ConfigureAwait(false)weaving — component lifecycle methods should resume on the dispatcher.ConfigureAwait.Fody supports opting out per class/method with
[ConfigureAwait(true)].PageLayoutmutations throughInvokeAsync(...)so they are marshalled back ontothe dispatcher regardless of how the preceding
awaitresumed.Option 1 is the more general fix; a blanket
ConfigureAwait(false)across an assembly containingComponentBasesubclasses is risky beyond this specific case.Whichever route is taken, please consider the
Disposepath described above as well: it deliversthe same notifications at teardown time, and it is intentional behaviour that should be preserved.
Reproduction Steps
No response
Expected behavior
PageHeadershould mutatePageLayouton the renderer's synchronization context, so thatsubscribers' change notifications run on the dispatcher like any other Blazor state change.
Actual behavior
No response
Regression?
No response
Known Workarounds
No response
Version
10.6.0
User Interface
Blazor Server
Database Provider
EF Core (Default)
Tiered or separate authentication server
None (Default)
Operation System
Windows (Default)
Other information
I ran into a problem within our application and Claude found two problems regarding LeptonX (reported on support page: https://abp.io/support/questions/10861/LeptonX-ContentToolbarBreadcrumbs-never-unsubscribe-from-PageLayout--async-void-handler-terminates-the-host-process-ObjectDisposedException) and the PageHeader/PageLayout components.