Activities of "rcalv002"

Product / version: ABP Framework 10.7.0 (Volo.Abp.Core, Volo.Abp.AspNetCore). Also present on abpframework/abp dev at ec3203128f. Severity: memory leak; blocks large integration-test suites (out-of-memory).

Summary

Volo.Abp.Internal.Telemetry.TelemetryService.AddActivityAsync subscribes two handlers on every call, AppDomain.CurrentDomain.ProcessExit and Console.CancelKeyPress, and never removes them. Both lambdas share one closure with the Task.Run lambda, which uses _serviceScopeFactory, so the closure captures this. Each handler therefore roots:

static AppDomain.ProcessExit / Console.CancelKeyPress invocation list
  -> TelemetryService+<>c__DisplayClass7_0
  -> TelemetryService
  -> AutofacServiceScopeFactory
  -> root Autofac LifetimeScope (the whole container)
  -> ComponentRegistry -> ... -> AbpApplicationWithExternalServiceProvider

AbpApplicationBase calls AddActivityAsync(ActivityNameConsts.ApplicationRun) once per application at initialization. Shutdown() and Dispose() do not remove the handlers, so a disposed application, its container and every singleton in it stay reachable until the process exits.

Why it triggers in non-web processes

Telemetry runs only when IAbpHostEnvironment.IsDevelopment(). In a process with no web host (for example an AbpIntegratedTest<T> test host) whose module graph includes AbpAspNetCoreModule, AbpAspNetCoreModule.PreConfigureServices sets the environment from services.GetHostingEnvironment(). When no IWebHostEnvironment is registered, that returns an EmptyHostingEnvironment with EnvironmentName = Development. So every integration test runs in "Development", sends telemetry, and leaks its application.

Impact we measured

  • Our host EF Core integration test assembly has about 470 AbpIntegratedTest tests, run serially in one process.
  • Each test's application stays on the heap, about 17–19 MB per test, growing in a straight line.
  • At about 476 tests the process reaches .NET's default GC heap hard limit (75% of a 10 GiB container, 7.5 GiB). The last tests fail with OutOfMemoryException while the Autofac container builds.
  • A long-running web app pays less, but every AddActivityAsync / AddErrorActivityAsync call adds two more handlers that are never removed, so the ProcessExit invocation list grows for the life of the process.

Repro

  1. Create a module that depends on AbpAspNetCoreModule and AbpAutofacModule. Do not register IWebHostEnvironment, and do not set Abp:Telemetry:IsEnabled.
  2. In a loop: AbpApplicationFactory.Create<TModule>(o => o.UseAutofac()), Initialize(), Shutdown(), Dispose(), then keep a WeakReference to the application.
  3. Run GC.Collect() and GC.WaitForPendingFinalizers(). Every WeakReference is still alive.
  4. A heap root analysis (ClrMD GCRoot, or dotnet-gcdump) shows the path above. Setting Abp:Telemetry:IsEnabled=false makes the applications collectible.

Suggested fixes

  1. Do not subscribe per call. Track pending telemetry tasks in one place, for example a static or per-application collection, and wait for them once on ProcessExit / CancelKeyPress or in OnApplicationShutdown. At the least, unsubscribe the handlers when telemetryTask completes.
  2. Make sure the handlers capture only the task, not this. Today they share a closure with the Task.Run lambda.
  3. Unregister telemetry handlers in AbpApplicationBase.Shutdown() / Dispose().
  4. Consider not treating the EmptyHostingEnvironment fallback as "Development" for telemetry, or not sending telemetry from test hosts (AbpIntegratedTest, AbpWebApplicationFactoryIntegratedTest).

Our workaround

Test configuration sets Abp:Telemetry:IsEnabled=false in our AbpIntegratedTest base class (our module test bases already did).

LeptonX's FormSelect replaces every select.form-select with markup of its own

<div class="custom-select-wrapper" data-lpx-bound="true">
  <div class="custom-select-display form-select" role="combobox" aria-labelledby="{select id}">…</div>
  <select …>  <!-- Blazor owns only this -->
</div>

The wrapper is created by JS and is not part of Blazor's render tree. Its observer only ever reacts to additions:

setupMutationObserver() {
  observer = new MutationObserver(e => e.forEach(t => {
    …
    t.addedNodes.forEach(n => { … c.processSelect(a) })   // added only — no removedNodes branch
  }))
  observer.observe(document.body, { childList: true, subtree: true, … })
}

and there is no dispose/destroy/cleanup on the class (Object.getOwnPropertyNames(leptonx.FormSelect) has none).

So when Blazor removes a <select>, only the <select> goes. LeptonX's fully-rendered combobox stays behind — visible, clickable, with an aria-labelledby pointing at an id that no longer exists — and the next render binds the new select and builds another wrapper beside it.

Measured live: orphan wrappers climbed 4 → 5 → 6 → 7 while clicking between columns, and never dropped.

We run a non-tiered ABP Commercial Blazor Web App. The WASM/Auto island carries the commercial Pro admin surfaces (Account.Pro, Identity.Pro, Saas.Host, OpenIddict.Pro, AuditLogging, SettingManagement, FeatureManagement, LanguageManagement, TextTemplateManagement, Gdpr, plus the LeptonX WASM theme). We deploy as Linux containers (mcr.microsoft.com/dotnet/aspnet:10.0) via Docker Compose (Coolify) and GitHub Actions. Host is framework-dependent with PublishReadyToRun=true, SatelliteResourceLanguages=en; publish is dotnet publish -c Release -r linux-x64 --no-self-contained.

We want to reduce the WASM cold-start payload and improve production startup, and we need your official position on which .NET 10 publish optimizations are supported/recommended with ABP Commercial 10.4 before we change our Dockerfile/publish. We're aware trimming and single-file had problems with ABP in the past — we want to know what's changed for ABP 10 / .NET 10.

What we've already measured (untrimmed Release, brotli, our app):

Published _framework brotli total 14.3 MB (461 files); cold wire transfer 8.3 MB across 184 files, ~29.6 MB decoded. Full IL trimming reduces _framework brotli to 9.6 MB (−33%, 356 files) and the build succeeds and boots — but the trim-analysis pass (-p:SuppressTrimAnalysisWarnings=false) reports 918 IL2xxx warnings, ~490 of them inside ABP's reflection core: Castle.DynamicProxy (132), Autofac (95), Volo.Abp (273) — interceptors, dynamic HTTP client proxies, and reflection-based DI. The rest are app/3rd-party. TrimMode=partial is a no-op on Blazor WASM for us — measured identical size (9.6 MB) and identical 918 warnings to full trim. We must keep BlazorWebAssemblyLoadAllGlobalizationData=true (see Q8). Questions

A. IL Trimming (PublishTrimmed)

Is full IL trimming officially supported and tested for ABP Commercial 10.4 Blazor WASM, including the dynamic HTTP client proxies, Castle DynamicProxy interception, reflection-based DI, object mapping, and the Pro modules listed above? Does ABP ship (or plan to ship) trimmer-safety metadata for its reflection-heavy paths — [DynamicDependency], ILLink.Descriptors.xml, [DynamicallyAccessedMembers], [RequiresUnreferencedCode], or feature switches — so those ~490 Castle/Autofac/Volo.Abp warnings are annotated-safe rather than something each customer must validate and re-validate on every ABP bump? Our hard blocker is that trimming can silently drop un-rooted DTO JSON properties (no exception — just missing fields over the dynamic-proxy wire). What is the recommended pattern to guarantee Application.Contracts DTO round-trip safety under trimming — rooting Contracts via TrimmerRootAssembly, an ABP-provided descriptor, a System.Text.Json source-generator context, or something else? Since TrimMode=partial is a no-op on Blazor WASM, is there an ABP-sanctioned way to trim the .NET framework/runtime assemblies while leaving Volo.Abp.* and Castle.* whole, to capture the framework savings without the reflection risk surface? B. WASM AOT (RunAOTCompilation) 5. Is RunAOTCompilation=true supported/recommended for ABP Commercial 10.4 WASM? AOT requires trimming and disables runtime IL generation — does ABP's WASM interception/proxy path (Castle DynamicProxy uses Reflection.Emit) remain functional under AOT, or does ABP rely solely on static/source-generated client proxies there? What size and startup deltas have you measured on a representative commercial app?

C. Single-file & publish packaging 6. Is PublishSingleFile=true (and trimming the server host) supported for the ABP host, given the embedded resources / Virtual File System and the LeptonX bundling pipeline? Single-file historically broke embedded-resource/IFileProvider resolution — is that resolved on ABP 10 / .NET 10? 7. Our published _framework has 461 files (356 trimmed). Is WasmEnableWebcil (default) the recommended packaging, and is there a supported way to reduce WASM request count (assembly bundling) on ABP 10.4?

D. Globalization data 8. We keep BlazorWebAssemblyLoadAllGlobalizationData=true because ABP applies the user/tenant culture dynamically during WASM startup; with it false we get "Blazor detected a change in the application's culture that is not supported" on every load. Is there an ABP-supported way to do dynamic culture switching with a bounded culture set (predefined supported cultures / custom ICU shard) so we don't ship the full ICU dataset? We only need en and es-* (Latin America).

E. Lazy loading Pro module assemblies 9. Can the commercial Pro module assemblies (Identity.Pro, Saas.Host, AuditLogging, OpenIddict.Pro, LanguageManagement, TextTemplateManagement, Gdpr — admin-only surfaces) be BlazorWebAssemblyLazyLoad-ed so they're fetched on-demand per route instead of in the cold-boot closure, given the ABP module system discovers/initializes modules from loaded assemblies at startup? If not supported today, is route-based code-splitting on the roadmap?

F. Compressed static assets & serving (we run behind a reverse proxy) 10. Is ABP's host static-file + LeptonX bundling pipeline fully compatible with .NET 10 compressed static web assets (build-time .br/.gz + fingerprinted import maps / MapStaticAssets)? In a containerized deploy behind a reverse proxy, should Kestrel serve the precompressed _framework/*.br, or should we offload compression/caching to the proxy? Recommended cache-control/immutable headers for the fingerprinted WASM assets?

G. Server image / runtime 11. Beyond PublishReadyToRun=true, what server-side production knobs do you recommend for the ABP host on .NET 10 containers (host trimming, R2R composite, tiered compilation, runtime/GC settings)? InvariantGlobalization is off the table for us (multi-culture) — anything else ABP-specific that interferes?

What would close this out for us: a concrete, supported set of publish properties we can pin in the .csproj / Dockerfile, and — ideally — a support matrix marking each of the above as supported / works-with-caveats / not supported on ABP Commercial 10.4 + .NET 10, plus a link to an official trimmed/AOT ABP Commercial Blazor WASM reference if one exists.

Hi Mali,

Thanks for your suggestion. AbpScriptsRenderMode worked as suggestion and the exception is gone

we are on Blazor WebApp with Parameters.InteractiveAuto = true and the standard bundle wiring, exactly as your reference, we checked by creating new project in web app mode to check before we ported from server to wasm pages. The divergence is a single page that is intentionally WebAssembly-only (new InteractiveWebAssemblyRenderMode(prerender: false)) rather than Interactive Auto, and that's the page that hits the race.

Why this page is WASM-only (prerender:false), not Auto by design, not oversight.

This page must not do the Interactive Auto server-first → WASM transition:

  • Its page-local UI services are registered only in the .Blazor.Client project. An Interactive Auto page renders server-first on a cold cache, which would require those client-only services (one of them an internal type) to also be registered server-side — which we can't do without a .Blazor.Server → .Blazor.Client reference we deliberately don't have.
  • The page is also required to run with no server-side /_blazor circuit it should be pure WebAssembly, not a server circuit that later hands off to WASM.

So InteractiveWebAssemblyRenderMode(prerender: false) is the correct, intended render mode for it. That is precisely the mode in which the abp/LeptonX bundle is not prerendered, so on this page <AbpScripts> (whose @rendermode follows the page's) emits the bundle only after WASM activation. Your template doesn't reproduce it only because the default has no genuinely WASM-only page; the moment a page is WASM-only, SideMenuLayout races the bundle.

(1) App.razor — <AbpScripts> wiring:

<AbpScripts BundleName="@BlazorLeptonXThemeBundles.Scripts.Global"
            WebAssemblyScriptFiles="GlobalScripts"
            @rendermode="PageRenderMode" />

@code {
    // Per-route render mode. The affected page is WASM-only, so for it
    // PageRenderMode resolves to InteractiveWebAssemblyRenderMode(prerender:false).
    private IComponentRenderMode PageRenderMode =>
        IsWasmOnlyRoute ? new InteractiveWebAssemblyRenderMode(prerender: false)
        : IsAutoRoute   ? InteractiveAuto
                        : InteractiveServer;

    private List<string> GlobalScripts => ["global.js", "global-scripts.js"];
}

The routed page renders the same way: <ClientRoutes @rendermode="@(new InteractiveWebAssemblyRenderMode(prerender: false))" />.

(2) Configure<AbpBundlingOptions> note InteractiveAuto = true is present:

Configure<AbpBundlingOptions>(options =>
{
    options.Parameters["LeptonXTheme.Layout"] = "side-menu";
    options.Parameters.InteractiveAuto = true;   // present

    options.ScriptBundles.Configure(
        BlazorLeptonXThemeBundles.Scripts.Global,
        bundle => bundle.AddFiles("/global-scripts.js"));

    options.StyleBundles.Configure(
        BlazorLeptonXThemeBundles.Styles.Global,
        bundle => bundle.AddFiles("/blazor-global-styles.css"));
    // (MVC bundles omitted)
});

(3) Template: Blazor WebApp (non-tiered), LeptonX side-menu, OpenIddict in-host, .NET 10, ABP 10.4.1, LeptonX 5.4.1

A minimal repro isolating the framework layout.

To rule out our own code, we ran a controlled A/B: two trivial pages with identical content and the same InteractiveWebAssemblyRenderMode(prerender:false) only the layout differs:

  • Under SideMenuLayout → crashes on cold direct load (the JSException storm → fatal error bar).
  • Under a bare/empty layout → loads cleanly, no exceptions.

Same render mode, same bundle wiring; only the layout differs which isolates SideMenuLayout / LeptonXStyleProvider as the cause, independent of our app code.

What we're asking.

a. Framework hardening: can SideMenuLayout / LeptonXStyleProvider feature-detect window.abp (and the LeptonX globals), or await a "scripts ready" signal, before the cookie / addClassToTag / initLeptonX interop so a genuinely WASM-only page initializes the layout once, cleanly, after the bundle lands?

b. Supported workaround meanwhile: is there a supported way to get the abp/LeptonX bundle into the initial HTML for a WASM-only page without introducing a /_blazor server circuit on it (which, per above, this page must not have)? For example, prerendering only the <AbpScripts> tags while the routed page stays prerender:false WASM-only. If decoupling <AbpScripts>'s render mode from the page is the intended approach, we'd want to confirm it doesn't create a circuit or an asset-swap conflict.

Summary

On a Blazor WebApp page rendered with InteractiveWebAssembly(prerender: false), the LeptonX SideMenuLayout calls JS-interop functions from the abp / LeptonX script bundle during component initialization, before that bundle has loaded. This throws repeated Microsoft.JSInterop.JSExceptions that are caught by the router's ErrorBoundary, which auto-recovers and retries — re-mounting the whole layout dozens of times for ~10–15s until the WASM script bundle finishes loading, then self-heals.

This is reproducible in a clean template (not specific to our code) and is caused by two framework components together:

  1. AbpScripts.razor emits the WebAssembly script bundle (the one that defines window.abp and the LeptonX globals) only when OperatingSystem.IsBrowser() is true — i.e. after the WASM runtime boots and the component re-renders.
  2. SideMenuLayout (and LeptonXStyleProvider) call into that bundle from OnInitializedAsync and OnAfterRenderAsync, with no guard for the script not yet being loaded.

On a prerender:false page there is no server-prerendered copy of the bundle, so (2) runs before (1) has injected/loaded the scripts.

Pages rendered with prerender (the default) do not hit this, because the bundle is written into the initial HTML and window.abp exists before the layout becomes interactive.

Environment

  • ABP: 10.4.1
  • LeptonX (Blazor) theme: 5.4.1
  • .NET: 10.0
  • UI: Blazor WebApp (Interactive Auto / WebAssembly islands), Side-menu layout
  • Auth: non-tiered, single host (OpenIddict in-host)
  • Render mode of the affected page: new InteractiveWebAssemblyRenderMode(prerender: false)

Steps to reproduce

  1. Create a Blazor WebApp (non-tiered, LeptonX, side-menu layout).
  2. Add a page rendered WebAssembly-only with prerender disabled, e.g.: <MyPage @rendermode="new InteractiveWebAssemblyRenderMode(prerender: false)" /> (or set prerender: false on the component/route). Keep the default App.razor wiring of <AbpScripts ... WebAssemblyScriptFiles="..." @rendermode="..." />.
  3. Wrap the router in an ErrorBoundary (as the default template does) so the exception is observable rather than fatal.
  4. Hard-reload that page on a cold cache (Ctrl+Shift+R) so the WASM runtime boots fresh.
  5. Watch the browser console during the WASM warm-up window.

Expected: the side-menu layout initializes once the page is interactive, with no exceptions.

Actual: repeated JSExceptions during warm-up (see below), each caught by the ErrorBoundary and auto-recovered, re-mounting the layout many times until the abp/LeptonX bundle loads.

Exception message and full stack trace

(1) During OnInitializedAsync:

Microsoft.JSInterop.JSException: Could not find 'abp.utils.getCookieValue' ('abp' was undefined).
Error: Could not find 'abp.utils.getCookieValue' ('abp' was undefined).
at Microsoft.JSInterop.JSRuntime.<InvokeAsync>d__23`1[[System.String, System.Private.CoreLib]].MoveNext()
at Volo.Abp.AspNetCore.Components.Web.CookieService.GetAsync(String key)
at Volo.Abp.AspNetCore.Components.WebAssembly.LeptonXTheme.LeptonXStyleProvider.GetSideMenuStateAsync()
at Volo.Abp.AspNetCore.Components.Web.LeptonXTheme.Components.ApplicationLayout.SideMenuLayout.OnInitializedAsync()
at Microsoft.AspNetCore.Components.ComponentBase.RunInitAndSetParametersAsync()
at Microsoft.AspNetCore.Components.RenderTree.Renderer.GetErrorHandledTask(Task taskToHandle, ComponentState owningComponentState)

(2) During OnAfterRenderAsync (after (1) is worked around, or in parallel):

Microsoft.JSInterop.JSException: The value 'abp.utils.addClassToTag' is not a function.
Error: The value 'abp.utils.addClassToTag' is not a function.
at Microsoft.JSInterop.JSRuntime.&lt;InvokeAsync&gt;d__23`1[[Microsoft.JSInterop.Infrastructure.IJSVoidResult, Microsoft.JSInterop]].MoveNext()
at Microsoft.JSInterop.JSRuntimeExtensions.InvokeVoidAsync(IJSRuntime jsRuntime, String identifier, Object[] args)
at Volo.Abp.AspNetCore.Components.Web.LeptonXTheme.Components.ApplicationLayout.SideMenuLayout.OnAfterRenderAsync(Boolean firstRender)
at Microsoft.AspNetCore.Components.RenderTree.Renderer.GetErrorHandledTask(Task taskToHandle, ComponentState owningComponentState)

SideMenuLayout.OnAfterRenderAsync also invokes initLeptonX and afterLeptonXInitialization (theme initialization) from the same bundle, which fail the same way until the bundle is loaded.

Root cause

AbpScripts.razor resolves WebAssemblyScriptFiles only under if (OperatingSystem.IsBrowser() && WebAssemblyScriptFiles != null), so on a prerender:false page the script bundle is injected only after WASM activation. SideMenuLayout/LeptonXStyleProvider then call abp.utils.getCookieValue, abp.utils.addClassToTag, initLeptonX, and afterLeptonXInitialization from OnInitializedAsync/OnAfterRenderAsync without verifying the bundle is loaded, so they throw until it is.

Suggested fix

SideMenuLayout / LeptonXStyleProvider should make their JS interop resilient to the script bundle not yet being present under InteractiveWebAssembly(prerender: false) — e.g. feature-detect window.abp before calling, await a "scripts ready" signal, or defer the cookie/class/init interop until the abp + LeptonX bundle is guaranteed loaded.

Workarounds we evaluated

  • Shimming the individual abp.utils.* functions early (in a plain <head> script) does not work: it just moves the failure to the next call, and initLeptonX is the theme's real initialization, which cannot be stubbed.

Summary

ContentToolbar in Volo.Abp.AspNetCore.Components.Web.LeptonXTheme (LeptonX 5.4.1) subscribes two async lambdas to the scoped PageLayout service's events in OnInitializedAsync and never unsubscribes (the component does not implement IDisposable):

// 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();
}

This causes two problems:

  1. Crash under Blazor Web App prerendering: during the static prerender pass, the component subscribes to the request-scoped PageLayout. The request's DI scope is disposed when the response completes, but a pending event-handler continuation can still execute afterwards. Its StateHasChanged triggers a render in which Blazorise's ComponentActivator resolves toolbar item component types from the now-disposed Autofac lifetime scope → ObjectDisposedException (stack trace below). Component disposal is not ordered before scope disposal, so this is a race that fires intermittently on any prerendered page using the application layout.
  2. Subscription leak in interactive sessions: because the handlers are anonymous lambdas and never removed, every ContentToolbar instance stays referenced by the scoped PageLayout for the lifetime of the circuit/scope.

Exception message and full stack trace

System.ObjectDisposedException
  Message=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.
  Source=Autofac
   at Autofac.Core.Lifetime.LifetimeScope.ThrowDisposedException()
   at Autofac.Extensions.DependencyInjection.AutofacServiceProvider.GetService(Type serviceType)
   at Blazorise.ComponentActivator.CreateInstance(Type componentType)
   at Microsoft.AspNetCore.Components.ComponentFactory.InstantiateComponent(...)
   at Microsoft.AspNetCore.Components.RenderTree.Renderer.InstantiateChildComponentOnFrame(...)
   at Microsoft.AspNetCore.Components.RenderTree.RenderTreeDiffBuilder.InitializeNewComponentFrame(...)
   ...
   at Microsoft.AspNetCore.Components.ComponentBase.StateHasChanged()
   at Microsoft.AspNetCore.Components.Rendering.RendererSynchronizationContext.<InvokeAsync>g__Execute|8_0(ValueTuple`3 state)
--- End of stack trace from previous location ---
   at Volo.Abp.AspNetCore.Components.Web.LeptonXTheme.Components.ApplicationLayout.Common.ContentToolbar.&lt;&lt;OnInitializedAsync&gt;b__7_1>d.MoveNext()
   at System.Threading.Tasks.Task.&lt;&gt;c.&lt;ThrowAsync&gt;b__124_1(Object state)

Steps to reproduce the issue

  1. Create a Blazor WebApp solution (non-tiered, LeptonX) on ABP 10.4.1 / LeptonX 5.4.1 — prerendering is enabled by default on the InteractiveAuto render mode.
  2. Add pages that use the application layout and set PageLayout toolbar items / title (e.g. PageToolbar items added from OnAfterRenderAsync, as in standard CRUD pages).
  3. Navigate to such a page via full document loads repeatedly (each load runs a prerender pass).
  4. Intermittently (it is a race between the event continuation and request-scope disposal), the ObjectDisposedException above is thrown from the prerender pass. With a debugger attached it breaks every time it occurs; without one it surfaces as unobserved-task exceptions / log noise on every unlucky prerender.

Environment

  • ABP Commercial 10.4.1, LeptonX 5.4.1 (Volo.Abp.AspNetCore.Components.Server.LeptonXTheme + Volo.Abp.AspNetCore.Components.WebAssembly.LeptonXTheme)
  • .NET 10 (10.0.9 shared framework), Blazor Web App hosting model, InteractiveServer + InteractiveAuto render modes with prerendering
  • Autofac (AbpAutofacModule), Blazorise 2.0.4

Suggested fix

In ContentToolbar (and any other LeptonX components subscribing to scoped services from lifecycle methods):

  1. Skip the event subscriptions entirely when RendererInfo.IsInteractive == false — a static prerender produces a single snapshot, so change events can never affect its output.
  2. Implement IDisposable, store the handlers in fields, and unsubscribe on dispose (fixes the leak for interactive sessions).

We have verified this exact change resolves the crash in our application by replacing the component via [ExposeServices(typeof(ContentToolbar))] / [Dependency(ReplaceServices = true)]. Note that a "disposed" flag alone is not sufficient — component disposal is not guaranteed to happen before the DI scope is disposed under prerendering, so the not-yet-subscribing gate (or catching ObjectDisposedException) is required.

Sample workaround class


using System;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Threading.Tasks;

using Volo.Abp.AspNetCore.Components.Web.LeptonXTheme.Components.ApplicationLayout.Common;
using Volo.Abp.DependencyInjection;

namespace Cns.Cloud.Apps.Blazor.Client;

// Replaces LeptonX's ContentToolbar (5.4.1): the stock component subscribes async lambdas to the
// scoped PageLayout's events in OnInitializedAsync and never unsubscribes. Under Blazor Web App
// prerendering (the Interactive Auto island), a continuation can fire after the request's DI scope
// is disposed, and the resulting StateHasChanged render throws ObjectDisposedException out of
// Autofac when Blazorise's activator tries to resolve toolbar item components.
[ExposeServices(typeof(ContentToolbar))]
[Dependency(ReplaceServices = true)]
public class SafeContentToolbar : ContentToolbar, IDisposable
{
    private bool _disposed;
    private NotifyCollectionChangedEventHandler? _collectionChanged;
    private PropertyChangedEventHandler? _propertyChanged;

    protected override Task OnInitializedAsync()
    {
        // Deliberately does NOT call base.OnInitializedAsync(): the base body subscribes the
        // unguarded, never-removed lambdas this class exists to replace.
        //
        // Static prerender (RendererInfo.IsInteractive == false) produces a single HTML snapshot:
        // change events can never affect the output, but their continuations CAN outlive the
        // request's DI scope and throw ObjectDisposedException (component disposal is not ordered
        // before scope disposal, so a _disposed guard alone is insufficient). Don't subscribe.
        if (!RendererInfo.IsInteractive)
        {
            return Task.CompletedTask;
        }

        _collectionChanged = async (_, _) =>
        {
            if (!_disposed)
            {
                await RenderAsync();
            }
        };
        _propertyChanged = async (_, _) =>
        {
            if (!_disposed)
            {
                await InvokeAsync(StateHasChanged);
            }
        };

        PageLayout.ToolbarItems.CollectionChanged += _collectionChanged;
        PageLayout.PropertyChanged += _propertyChanged;
        return Task.CompletedTask;
    }

    protected override async Task RenderAsync()
    {
        if (_disposed)
        {
            return;
        }

        await base.RenderAsync();
    }

    public void Dispose()
    {
        _disposed = true;

        if (_collectionChanged is not null)
        {
            PageLayout.ToolbarItems.CollectionChanged -= _collectionChanged;
        }

        if (_propertyChanged is not null)
        {
            PageLayout.PropertyChanged -= _propertyChanged;
        }
    }
}

[AI-Bot] said:


This response was generated by AI. If it didn’t solve your problem, feel free to post a new answer — our support team will jump in to help you soon.

theres no content

LeptonX 5.3.0 Custom-Select Overlay Drops <optgroup> and Breaks Inside <Modal> (Blazor Server)

Summary

LeptonX's custom-select overlay (the Bootstrap-style <button class="select-button"> / <ul class="dropdown-menu"> overlay that LeptonX paints over every native <select> / Blazorise <Select> on the page) has two distinct rendering bugs that surface in a Blazor Server app running ABP 10.3.0 + LeptonX 5.3.0 + Blazorise 2.0.4:

  1. <optgroup> headers are dropped — when a Blazorise <Select> contains <SelectGroup> (or a native <select> contains <optgroup>), the overlay flattens all options into a single ungrouped list. The group labels are gone and the user cannot tell which entry belongs to which group.
  2. Options never render when the <Select> is inside a <Modal> — the overlay button paints (showing the placeholder text) but the dropdown panel is empty when opened, no matter how many <SelectItem>s the underlying <select> has. This appears specific to Blazorise modals, where the <select> element is moved/wrapped after LeptonX has run its initial overlay pass.

Environment

| Component | Version | | --- | --- | | .NET | 10.0 | | ABP Framework | 10.3.0 | | Volo.Abp.AspNetCore.Components.Server.LeptonXTheme | 5.3.0 | | Volo.Abp.AspNetCore.Components.Web.LeptonXTheme | 5.3.0 (transitive) | | Volo.Abp.AspNetCore.Mvc.UI.Theme.LeptonX | 5.3.0 | | Blazorise | 2.0.4 | | LeptonX layout | LeptonXBlazorLayouts.SideMenu | | Browser | Chrome (latest), reproduced in Edge |

Confirmed on LeptonX 5.3.0 with Blazorise 2.0.4 (our 10.3 baseline), so the bugs are LeptonX-side, not Blazorise.

Bug 1 — <optgroup> headers dropped

Reproduction

  1. ABP Blazor Server 10.3 with the LeptonX SideMenu layout.
  2. Add a Blazorise <Select> outside any modal with grouped options, e.g.:
<Select TValue="string" @bind-Value="@SelectedLayoutCode">
    <SelectItem Value="@string.Empty">Select report or layout</SelectItem>
    @foreach (var reportType in ReportCollection)
    {
        <SelectGroup Label="@reportType.TypeName">
            @foreach (var reportLayout in reportType.ReportLayoutList ?? [])
            {
                <SelectItem Value="@reportLayout.LayoutCode">@reportLayout.LayoutName</SelectItem>
            }
        </SelectGroup>
    }
</Select>
  1. Run the app and open the dropdown.

Expected (and what the underlying <select> actually emits):

[Select report or layout]
─── A/R Aging ───
   AR_Aging_Standard
   AR_Aging_Detailed
─── Sales ───
   Sales_Order_Open
   Sales_Order_Closed

Actual: the overlay strips the group rows; the user sees a flat list:

[Select report or layout]
AR_Aging_Standard
AR_Aging_Detailed
Sales_Order_Open
Sales_Order_Closed

Verification — the underlying DOM is correct

Inspect <select> in DevTools and the <optgroup label="…"> elements are present and well-formed. Native browser <select> rendering (when LeptonX's overlay JS is removed) shows the groups correctly. Bug is in the overlay's DOM walk; it iterates <option> only and ignores <optgroup>.

Affected page in our app

"Reports and Layouts" picker. Layouts are grouped by report type (e.g., A/R Aging, Sales). With the overlay, every layout name appears flattened with no indication of which report family it belongs to.

Bug 2 — <Select> content empty when hosted inside <Modal>

Reproduction

  1. Same project as above.
  2. Place a Blazorise <Select> inside a Blazorise <Modal>:
<Modal @ref="MyModal" Centered="true">
    <ModalContent>
        <ModalBody>
            <Select TValue="string" @bind-Value="@SelectedCurrency">
                <SelectItem Value="">Select currency</SelectItem>
                @foreach (var c in AvailableCurrencies)
                {
                    <SelectItem Value="@c.Code">@c.Code - @c.Name</SelectItem>
                }
            </Select>
        </ModalBody>
    </ModalContent>
</Modal>
  1. Open the modal and click the select.

Expected: the dropdown panel lists every currency.

Actual: the overlay button shows the placeholder ("Select currency"), but the dropdown panel is empty — no <li> rows are rendered, even though the underlying <select> is fully populated.

Verification

  • Inspect the <select> in DevTools while the modal is open: every <option> is present and has the right value/text content.
  • Submit the form by typing in the (functioning) overlay search input or by pressing Tab and arrow keys — keyboard navigation also fails because the overlay's <ul> is empty.
  • Replace the Blazorise <Select> with a native <select class="form-control"> in the same modal: the overlay still paints, but its <ul> is now populated correctly. This is the smoking gun: the overlay successfully consumes a static native <select> but fails when the <select> is the one Blazorise rendered. Our hypothesis is that LeptonX runs its overlay-decoration pass once on modal open, before Blazorise has finished rendering the <select>'s <option> children, and never re-runs after Blazorise's second render pass.

Workaround

Replace Blazorise <Select> with native <select class="form-control"> elements (with explicit @onchange handlers, since @bind does not work the same on native <select> for non-string values). The native <select> keeps its native behaviour underneath the overlay, and either renders correctly (groups visible) or, when LeptonX's overlay still mis-renders, the native control behind it remains usable as a fallback.

Bug 1 workaround (groups)

@* Native <select> — LeptonX's custom-select overlay ignores <optgroup> *@
<select class="form-control" value="@Filter.LayoutCode" @onchange="OnLayoutCodeChanged" disabled="@IsLoading">
    <option value="">@(IsLoading ? L["LoadingWithThreeDot"] : L["SelectReportOrLayout"])</option>
    @foreach (var reportType in ReportCollection)
    {
        <optgroup label="@reportType.TypeName">
            @foreach (var reportLayout in reportType.ReportLayoutList ?? [])
            {
                <option value="@reportLayout.LayoutCode">@reportLayout.LayoutName</option>
            }
        </optgroup>
    }
</select>
private async Task OnLayoutCodeChanged(ChangeEventArgs e)
{
    var layoutCode = e.Value?.ToString() ?? string.Empty;
    Filter.LayoutCode = layoutCode;
    await LoadLayoutParametersAsync(layoutCode);
}

Bug 2 workaround (modal)

@* Native <select> — LeptonX's custom-select overlay breaks inside modals *@
<select class="form-control" value="@NewSyncJob.BaseCurrency" @onchange="OnBaseCurrencyChanged" disabled="@(!AvailableCurrencies.Any())">
    <option value="">@L["SelectCurrency"]</option>
    @foreach (var currency in AvailableCurrencies)
    {
        <option value="@currency.Code">@currency.Code - @currency.Name</option>
    }
</select>
private void OnBaseCurrencyChanged(ChangeEventArgs e)
{
    NewSyncJob.BaseCurrency = e.Value?.ToString() ?? string.Empty;
}

For nullable enums in modals (e.g., DayOfWeek?), the value round-trips as the integer string:

<select class="form-control" value="@((int?)NewSyncJob.SyncDayOfWeek)" @onchange="OnDayOfWeekChanged" disabled="@(NewSyncJob.Frequency != SyncFrequency.Weekly)">
    <option value="">@L["SelectDay"]</option>
    @foreach (var day in Enum.GetValues<DayOfWeek>())
    {
        <option value="@((int)day)">@L[$"Enum:DayOfWeek.{day}"]</option>
    }
</select>
private void OnDayOfWeekChanged(ChangeEventArgs e)
{
    var val = e.Value?.ToString();
    NewSyncJob.SyncDayOfWeek = string.IsNullOrEmpty(val) ? null : (DayOfWeek)int.Parse(val);
}

We too are waiting on abp delivered pro module for elsa, we'd love to offload our other workflow solutions to an integrated abp module.

Showing 1 to 10 of 119 entries
Boost Your Development
ABP Live Training
Packages
See Trainings
Mastering ABP Framework Book
The Official Guide
Mastering
ABP Framework
Learn More
Mastering ABP Framework Book
Made with ❤️ on ABP v10.8.0-preview. Updated on November 12, 2026, 06:07
1
ABP Assistant
🔐 You need to be logged in to use the chatbot. Please log in first.