Second part of the Bugreport (related issue but located in ABP source instead of LeptonX) can be found here: https://github.com/abpframework/abp/issues/26075
Iam not sure if this is the correct place for a LeptonX Bugreport or if i should open a bug issue on github abpframework/abp issue page.
i ran into a bug inside our application and after multiple debug sessions, me and claude found the problem. We also could reproduce the problem in a blank ABP project created with ABP Studio.
ContentToolbar and Breadcrumbs subscribe to the scoped PageLayout with anonymous
async void handlers and never unsubscribe (neither component implements IDisposable).
On a Blazor Web App using InteractiveAuto, a layout change can reach a component the renderer
has already discarded. Its render then resolves child components from a disposed Autofac scope,
and because the handler is async void the resulting ObjectDisposedException is rethrown on a
thread-pool thread — which terminates the whole host process, not just the circuit.
This is not a failed render. dotnet run exits; in production the container dies and restarts.
High. Unhandled process termination triggered by ordinary navigation, with no application code at fault and no way for the application to catch it.
| | |
|---|---|
| ABP Commercial | 10.6.0 (also reproduced on 10.5.0) |
| Volo.Abp.AspNetCore.Components.Server.LeptonXTheme | 5.6.0 (also 5.5.0) |
| UI | Blazor Web App, @rendermode="InteractiveAuto", LeptonX side-menu layout |
| DI | Autofac (Volo.Abp.Autofac 10.6.0) |
| Runtime | .NET SDK 10.0.400, Windows |
.Blazor.Client project as Pages/Repro.razor.DbMigrator, start the host, log in as admin./repro.The page renders, then the host process exits.
@page "/repro"
@using AbpDefaultSolution.Books
@using AbpDefaultSolution.Permissions
@using Microsoft.AspNetCore.Authorization
@using Volo.Abp.Application.Dtos
@using Volo.Abp.AspNetCore.Components.Web.Theming.Layout
@using Volo.Abp.AspNetCore.Components.Web.Theming.PageToolbars
@using Volo.Abp.BlazoriseUI.Components
@inherits AbpDefaultSolutionComponentBase
@inject IBookAppService BookAppService
@inject IAuthorizationService AuthorizationService
<PageHeader Title="Repro" BreadcrumbItems="BreadcrumbItems" Toolbar="Toolbar"></PageHeader>
@code {
protected List<Volo.Abp.BlazoriseUI.BreadcrumbItem> BreadcrumbItems = [];
protected PageToolbar Toolbar { get; } = new();
protected override void OnInitialized()
{
BreadcrumbItems.Add(new Volo.Abp.BlazoriseUI.BreadcrumbItem("Repro"));
Toolbar.AddButton("New", () => Task.CompletedTask, IconName.Add,
requiredPolicyName: AbpDefaultSolutionPermissions.Books.Create);
}
// Several sequential awaits, as ABP Suite generates for a CRUD page.
protected override async Task OnInitializedAsync()
{
await AuthorizationService.IsGrantedAsync(AbpDefaultSolutionPermissions.Books.Default);
await AuthorizationService.IsGrantedAsync(AbpDefaultSolutionPermissions.Books.Create);
await AuthorizationService.IsGrantedAsync(AbpDefaultSolutionPermissions.Books.Edit);
await AuthorizationService.IsGrantedAsync(AbpDefaultSolutionPermissions.Books.Delete);
await BookAppService.GetListAsync(new PagedAndSortedResultRequestDto());
}
}
I tested the following on the same clean template and all of them survived, including those that
render a PageHeader toolbar:
/Identity/Users, /Identity/Roles, /Saas/Tenants, /AuditLogs, /TextTemplates,
/LanguageManagement/Languages, /OpenIddict/Applications, /FileManagement
The multi-await OnInitializedAsync in the repro page is what widens the window enough for the
InteractiveAuto hand-off to dispose the circuit scope while the theme's handler is still queued.
In our production application, every ABP Suite generated CRUD page crashes the host this way.
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.Core.Lifetime.LifetimeScope.ThrowDisposedException()
at Autofac.Core.Lifetime.LifetimeScope.ResolveComponent(ResolveRequest& request)
at Autofac.Extensions.DependencyInjection.AutofacServiceProvider.GetService(Type serviceType)
at Blazorise.ComponentActivator.CreateInstance(Type componentType)
at Microsoft.AspNetCore.Components.ComponentFactory.InstantiateComponent(IServiceProvider serviceProvider, Type componentType, IComponentRenderMode callerSpecifiedRenderMode, Nullable`1 parentComponentId)
at Microsoft.AspNetCore.Components.RenderTree.Renderer.InstantiateChildComponentOnFrame(RenderTreeFrame[] frames, Int32 frameIndex, Int32 parentComponentId)
at Microsoft.AspNetCore.Components.RenderTree.RenderTreeDiffBuilder.InitializeNewComponentFrame(DiffContext& diffContext, Int32 frameIndex)
...
at Microsoft.AspNetCore.Components.RenderTree.Renderer.ProcessRenderQueue()
at Microsoft.AspNetCore.Components.ComponentBase.StateHasChanged()
at Microsoft.AspNetCore.Components.Rendering.RendererSynchronizationContext.<InvokeAsync>g__Execute|8_0(ValueTuple`3 state)
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.QueueUserWorkItemCallback.Execute()
at System.Threading.ThreadPoolWorkQueue.Dispatch()
at System.Threading.PortableThreadPool.WorkerThread.WorkerThreadStart()
at System.Threading.Thread.StartCallback()
Client side, the corrupted render batch surfaces as:
Error: There was an error applying batch N
SyntaxError: Unexpected number in JSON at position 1 (line 1 column 2)
A layout change reaching a disposed component should be a no-op. Under no circumstances should a theme component be able to terminate the host process.
Volo.Abp.AspNetCore.Components.Web.LeptonXTheme/Components/ApplicationLayout/Common/ContentToolbar.razor.cs:
protected override Task OnInitializedAsync()
{
PageLayout.ToolbarItems.CollectionChanged += async (s, e) => await RenderAsync();
PageLayout.PropertyChanged += async (s, e) => await InvokeAsync(StateHasChanged);
return base.OnInitializedAsync();
}
Three problems compound:
async void — an exception escapes to the thread pool and kills the process.Dispose, and PageLayout is scoped — so it holds every
ContentToolbar instance created in that scope alive, including discarded ones.Breadcrumbs.razor.cs has the identical defect:
PageLayout.BreadcrumbItems.CollectionChanged += async (s, e) => await InvokeAsync(StateHasChanged);
PageLayout.PropertyChanged += async (s, e) => await InvokeAsync(StateHasChanged);
For contrast, SideMenu/Navigation/MainMenuItem.razor.cs does this correctly — named handler,
IDisposable, unsubscribes. TopMenu/Navigation/MainMenuItem.razor.cs subscribes in
OnParametersSet and never unsubscribes (a leak rather than a crash, since its handler is not
async).
For both ContentToolbar and Breadcrumbs:
IDisposable and unsubscribe there;async void, so a failed render is observed on a task instead of
crashing the host.Replace both components via DI:
services.Replace(ServiceDescriptor.Transient<ContentToolbar, MySafeContentToolbar>());
services.Replace(ServiceDescriptor.Transient<Breadcrumbs, MySafeBreadcrumbs>());
with subclasses that override OnInitializedAsync without calling base — so the anonymous
handlers are never attached — subscribe named handlers instead, and unsubscribe them in
Dispose. Skipping base is safe: it only chains to ComponentBase.OnInitializedAsync(), which
returns a completed task, and the base OnAfterRenderAsync still performs the initial render.
Implemented as a plain .cs subclass (not a .razor file) so the base component's generated
markup is inherited unchanged.
Verified across all affected pages in our application: zero process terminations afterwards.
For a Blazor Web App the replacement must be registered in both the server module and the
.Client module, since the layout renders in both.