Hi,
Your analysis is correct, and this is a known issue upstream in ASP.NET Core - not something specific to ABP. ABP's passkey scripts (addPasskey.js, Login.js, and the profile passkey Default.js) follow the same plain JSON.stringify(credential) pattern as the official Blazor passkey template. That works for native PublicKeyCredential instances but breaks when 1Password (or other password managers) intercept navigator.credentials.create() and return an object whose toJSON() doesn't emit clientExtensionResults - which is required on Microsoft.AspNetCore.Identity.PublicKeyCredential<TResponse> in .NET 10:
https://github.com/dotnet/aspnetcore/blob/release/10.0/src/Identity/Core/src/Passkeys/PublicKeyCredential.cs#L33
The ASP.NET Core team is tracking it here:
Their position is that password managers should fix PublicKeyCredential.prototype.toJSON themselves, so they declined to add the workaround to the Blazor template and documented it instead:
https://learn.microsoft.com/aspnet/core/security/authentication/passkeys#mitigate-publickeycredentialtojson-error-typeerror-illegal-invocation
The official guidance is to replace JSON.stringify(credential) with a manual object that explicitly populates each field (including clientExtensionResults from credential.getClientExtensionResults()), plus a convertToBase64 helper that base64url-encodes the binary fields.
Your jQuery patch is a fine workaround for now - it covers form submit, FormData, XHR, and $.ajax, so both registration and login work.
We'll apply Microsoft's documented fallback to the three ABP scripts in the next release so 1Password works out of the box. You can drop your patch once you upgrade.
Thanks
Hi,
Thanks for reporting this. The "collapse but not expanding" behavior you see (and the fact that it also breaks unrelated menus like SaaS / Administration) is caused by a timing issue in LeptonX 5.3.x sidebar initialization, triggered when an IMenuContributor awaits a real HTTP call.
What's happening
LeptonX's SideMenuLayout runs initLeptonX (which wires up the click handlers for sidebar items) once, on its first render. Your contributor awaits documentMenuState.EnsureLoadedAsync(), which goes over HTTP. While that HTTP is in flight, the MainMenu child component hasn't rendered yet, so the menu DOM (<a class="lpx-menu-item"> for each main menu) doesn't exist when LeptonX scans the page. By the time your HTTP returns and the menu items appear, nothing re-binds the click handlers — so every main menu (yours and the built-in ones) ends up collapsed and unclickable.
We've already fixed this on the LeptonX side: BaseSideMenu now uses a MutationObserver on #lpx-sidebar plus an idempotent data-lpx-bound marker, so menu items added after the first init get bound automatically. The fix is referenced from a related ticket https://abp.io/support/questions/10622 and will ship in the next LeptonX 5.3.x patch.
Workaround until that patch is out
Drop a small JS file into your Blazor.Client wwwroot/, e.g. lpx-sidebar-rebind-polyfill.js:
(function () {
'use strict';
var DONE_ATTR = 'data-lpx-polyfill-rebound';
function rebind() {
var sidebar = document.querySelector('#lpx-sidebar');
if (!sidebar || sidebar.hasAttribute(DONE_ATTR)) return false;
if (!sidebar.querySelector('.outer-menu-item > .lpx-menu-item')) return false;
sidebar.setAttribute(DONE_ATTR, '');
var initFn = window.leptonx
&& window.leptonx.init
&& window.leptonx.init.initializers
&& typeof window.leptonx.init.initializers.get === 'function'
&& window.leptonx.init.initializers.get('initSideMenu');
if (typeof initFn === 'function') { initFn(); return true; }
return false;
}
function start() {
if (rebind()) return;
var observer = new MutationObserver(function () {
if (rebind()) observer.disconnect();
});
observer.observe(document.body, { childList: true, subtree: true });
setTimeout(function () { observer.disconnect(); }, 30000);
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', start);
else start();
})();
Reference it in wwwroot/index.html after the LeptonX bundle:
<script src="lpx-sidebar-rebind-polyfill.js"></script>
It watches for the sidebar menu DOM to appear, then triggers initSideMenu exactly once, so the click handlers land on the real menu items. The DONE_ATTR flag and the 30-second observer timeout keep it from double-binding on later navigations (which would otherwise cause one click to toggle twice on this LeptonX version).
Two small unrelated suggestions on your contributor:
catch { } swallows everything — consider Logger.LogException(ex) so you can actually see when the HTTP call fails.requiredPermissionName from the child items; the parent already gates them via IsGrantedAsync, so re-checking on each child is just wasted work.Thanks
Hi,
The Studio CLI is trying to load Volo.Abp.Studio.Extensions.StandardSolutionTemplates.Cli.dll from your NuGet cache, but that file is not present on disk. The package folder exists (so Studio assumes the package is "downloaded") but the DLL inside it is missing — usually caused by an interrupted NuGet restore, antivirus / EDR quarantine, missing .NET 10 SDK, or a stale 2.2.7 folder left over from an earlier upgrade.
Please try the following in order:
1) Make sure .NET 10 SDK is installed
The extension is built for net10.0. Run:
dotnet --list-sdks
dotnet --list-runtimes
You should see 10.0.x in both lists. If not, install .NET 10 from https://dotnet.microsoft.com/download/dotnet/10.0 first, then continue.
2) Delete the broken extension folder and let Studio re-download it
Close ABP Studio (UI and any open terminals running abp), then delete the whole folder for that extension:
Remove-Item -Recurse -Force "$env:USERPROFILE\.nuget\packages\volo.abp.studio.extensions.standardsolutiontemplates"
Open ABP Studio UI again. It will re-download the extension automatically. After it finishes, verify both DLLs are present:
dir "$env:USERPROFILE\.nuget\packages\volo.abp.studio.extensions.standardsolutiontemplates\2.2.7\lib\net10.0\"
You should see at least:
Volo.Abp.Studio.Extensions.StandardSolutionTemplates.Core.dllVolo.Abp.Studio.Extensions.StandardSolutionTemplates.Cli.dllThen run your original command (e.g. abp update) again.
3) Check the NuGet path environment variables
Your stack trace shows C:\Users\\.nuget\... with no username between Users and .nuget. That is most likely just how the exception text is rendered, but please confirm:
echo $env:USERPROFILE
echo $env:NUGET_PACKAGES
USERPROFILE should point to your home folder (e.g. C:\Users\yourname). NUGET_PACKAGES is normally empty — if you have it set to a custom path, make sure that path is correct and writable.
4) If you're on a corporate / managed machine
Some EDR or antivirus products quarantine DLLs from .nuget\packages. If steps 1–3 don't help, ask IT to allow-list the .nuget\packages folder, then redo step 2.
If the problem persists after the steps above, please share:
dotnet --list-sdks and dotnet --list-runtimesdir "$env:USERPROFILE\.nuget\packages\volo.abp.studio.extensions.standardsolutiontemplates\2.2.7\lib\net10.0\"abp --version outputWe'll also improve ABP Studio CLI in a future release to detect missing extension DLLs and re-download them automatically instead of crashing.
Thanks
Hi Mucahit,
Thanks for the deep dive — your Bug 1 analysis is correct. We confirmed the race against the ABP 10.3 source. Commit e913c85 (fix(dynamic-layout): replace OnInit implementation with environment change listener, 2025-08-06) removed DynamicLayoutComponent.ngOnInit, which previously called getLayout() eagerly. After that change, getLayout() only runs from (a) the NavigationEnd subscription set up in the constructor, or (b) the EnvironmentService.createOnUpdateStream(...) listener — and that stream is backed by a plain Subject, so it does not replay the current value. Both signals can be missed depending on bootstrap timing, which is what you're hitting on NgModule bootstrap.
We reproduced your exact DOM (<abp-dynamic-layout><!--ng-container--><!--container--></abp-dynamic-layout>) on a minimal Angular 21.2 + @abp/ng.core@10.3.0 NgModule app and validated the fix below.
Use this slightly stronger version in app.component.ts — it always calls getLayout() and forces a change-detection pass, so it covers both the case where the navigation event was missed and the case where layout was set but [ngComponentOutlet] didn't refresh:
import {
AfterViewInit,
ChangeDetectorRef,
Component,
ViewChild,
} from '@angular/core';
import { DynamicLayoutComponent } from '@abp/ng.core';
@Component({ /* ... */ })
export class AppComponent implements AfterViewInit {
@ViewChild(DynamicLayoutComponent)
private dynamicLayout?: DynamicLayoutComponent;
constructor(private cdr: ChangeDetectorRef) {}
ngAfterViewInit() {
setTimeout(() => {
const dl = this.dynamicLayout as any;
dl?.getLayout?.();
this.cdr.detectChanges();
});
}
}
The setTimeout(0) is required to avoid NG0100 ExpressionChangedAfterItHasBeenCheckedError, as you already noted.
We've opened https://github.com/abpframework/abp/issues/25373 to track the regression in DynamicLayoutComponent so consumers won't need this workaround in a future release.
The "ProjectName" literal is the fallback used by LeptonX's BrandLogoComponent when LOGO_APP_NAME_TOKEN has no provider:
// node_modules/@volosoft/ngx-lepton-x.core/.../brand-logo.component.ts
protected readonly appName =
inject(LOGO_APP_NAME_TOKEN, { optional: true }) ?? 'ProjectName';
In v10, no ABP or LeptonX module registers this token automatically anymore — the standard ABP 10.x template registers it through provideLogo(withEnvironmentOptions(environment)) from @abp/ng.theme.shared. Add this to your app.module.ts providers (it returns EnvironmentProviders and is fine inside an NgModule's providers array):
import { provideLogo, withEnvironmentOptions } from '@abp/ng.theme.shared';
import { environment } from '../environments/environment';
providers: [
APP_ROUTE_PROVIDER,
provideSideMenuLayout(),
provideLogo(withEnvironmentOptions(environment)),
],
This single call provides both LOGO_APP_NAME_TOKEN (from environment.application.name) and LOGO_URL_TOKEN (from environment.application.logoUrl), so you don't need to provide the tokens manually.
To answer your direct question: yes, NgModule bootstrap is still officially supported in ABP 10.x — it's not marked deprecated in the migration guide. The standalone app.config.ts style is the new template default, but the CoreModule.forRoot / ThemeSharedModule / ThemeLeptonXModule.forRoot() / SideMenuLayoutModule.forRoot() / AccountLayoutModule.forRoot() chain you're already using is intended to keep working in 10.x.
Thanks
Hi,
Thanks for the screenshots.
The visual drift in the first two screenshot pairs comes from the Bootstrap version that ships with LeptonX:
LeptonX 4.x (ABP 9.x) → bootstrap "~5.2.0"
LeptonX 5.x (ABP 10.x) → bootstrap "~5.3.0"
Bootstrap 5.3 is a design refresh under a "minor" version (new .text-bg-* utilities, retuned token defaults, dark-mode nesting). LeptonX itself only changed one token between 4.1.3 and 5.3.0 — --lpx-light went from #eeeeee to #bbb. You can revert that single token to recover the lighter card-header / button look in the first screenshot:
body.lpx-theme-dim {
--lpx-light-rgb: 238, 238, 238;
--lpx-light: #eeeeee;
}
The second screenshot (.bg-info badge) is purely Bootstrap 5.3's new .text-bg-* rendering — --lpx-info is unchanged in LeptonX itself.
For the third screenshot the styling is complex enough that we'd rather inspect the actual project than keep reasoning about it from source. Could you put together a minimal reproducible project and email it to liming.ma@volosoft.com?
Thanks
Hi,
A few clarifications based on your follow-up questions, because the helper does not have the two problems you were worried about.
mapper.Map does not overwrite the existing destination firstThe third argument of PatchFromMapped is Func<TSource, TDestination>. When you write mapper.Map, C# resolves the method group against this delegate signature. The mapper has two Map overloads:
public partial SurveyData Map(UploadedSurveyData source); // returns a new instance
public partial void Map(UploadedSurveyData source, SurveyData dst); // updates in place
Only the first one matches Func<UploadedSurveyData, SurveyData>, so that's the one the helper calls. It builds a brand-new SurveyData, fully mapped, and the helper then copies only the listed properties from that temporary object onto your existing destination. The existing destination is never overwritten in full — there is no "too late" problem.
You can verify this on the delegate itself:
Func<UploadedSurveyData, SurveyData> mapFunc = mapper.Map;
// mapFunc.Method.ReturnType == typeof(SurveyData)
// mapFunc.Method.GetParameters().Length == 1 (no destination parameter)
The whole subgraph — nested entities, collections, your double? -> CurrencyObject[]? user-defined converter, anything Mapperly knows how to map — is built by Mapperly's compile-time generated code inside the temporary mapped object. The reflection step only assigns the top-level property reference from mapped to your destination. There is no reflection recursion, no per-class code, no special handling for CurrencyObject or any child entity.
Concretely, with a domain like:
public class SurveyData
{
public string? Q1 { get; set; }
public CurrencyObject[]? Salary { get; set; } // custom converter from double?
public AddressEntity? Address { get; set; } // nested entity, has its own Zip
public List<ItemEntity>? Items { get; set; } // collection
}
Calling:
PatchExtensions.PatchFromMapped(uploaded, surveyData, mapper.Map, new[] { "Address" });
surveyData.Address is replaced with a fully-mapped AddressEntity (including the nested Zip). All other properties on surveyData (Q1, Salary, Items) are untouched, including object identity — they keep pointing at the same instances they did before the call. So "the list would be long and code would be complex" doesn't apply: Mapperly handles the graph, the helper just commits the listed top-level fields.
So you don't need to drop ObjectMapper.Map and write a pure-reflection copy with special cases for CurrencyObject. The helper above already keeps Mapperly responsible for all type conversions and object-graph mapping.
Wherever you used to call ObjectMapper.Map<UploadedSurveyData, SurveyData>(uploaded, existingSurvey) together with IgnoreNotInListMembers. Typical Application Service shape:
public async Task UpdateAsync(Guid id, UploadedSurveyData input, List<string> requiredProperties)
{
var existing = await _repository.GetAsync(id);
PatchExtensions.PatchFromMapped(input, existing, _mapper.Map, requiredProperties);
await _repository.UpdateAsync(existing);
}
I have these scenarios covered by xUnit tests locally (Mapperly 4.3.1, all passing): top-level property selection, double? → CurrencyObject[]? conversion through the listed property, full nested subgraph (Address → Zip), collection mapping (List<ItemDto> → List<ItemEntity>), and reference-identity preservation for non-listed properties. Happy to share more snippets if helpful.
Thanks
Hi,
Two separate things are happening here, and unfortunately downgrading inside ABP v10 won't avoid either of them. Details below.
This is a known LeptonX 5.3.0 regression — same root cause as the support question you already linked (#10622): initSideMenu runs once from SideMenuLayout.OnAfterRenderAsync against an empty #lpx-sidebar because MainMenuProvider.GetMenuAsync is still awaiting, and nothing re-binds the click handlers when the menu finally renders or re-renders.
Until the patch is out, drop this script block into Pages/_Host.cshtml, after <abp-script-bundle name="@BlazorLeptonXThemeBundles.Scripts.Global"/> and the app.js script tag:
<script>
(function () {
try {
const allBinds = new WeakMap();
let timer = null;
const origAdd = EventTarget.prototype.addEventListener;
EventTarget.prototype.addEventListener = function (type, fn, opts) {
if (this instanceof Element
&& this.classList && this.classList.contains('lpx-menu-item')
&& this.closest && this.closest('#lpx-sidebar')) {
let arr = allBinds.get(this);
if (!arr) { arr = []; allBinds.set(this, arr); }
arr.push({ type: type, fn: fn, opts: opts });
}
return origAdd.call(this, type, fn, opts);
};
function cleanupBinds() {
document.querySelectorAll('#lpx-sidebar .lpx-menu-item').forEach(function (el) {
const arr = allBinds.get(el);
if (arr) {
arr.forEach(function (b) { el.removeEventListener(b.type, b.fn, b.opts); });
allBinds.delete(el);
}
});
}
const origInitLeptonX = window.initLeptonX;
window.initLeptonX = function () {
cleanupBinds();
return origInitLeptonX.apply(this, arguments);
};
function reinit() {
cleanupBinds();
const fn = window.leptonx
&& window.leptonx.init
&& window.leptonx.init.initializers
&& window.leptonx.init.initializers.get('initSideMenu');
if (fn) fn();
}
function check() {
const items = document.querySelectorAll('#lpx-sidebar .outer-menu-item > .lpx-menu-item');
if (items.length === 0) return;
let needs = false;
for (let i = 0; i < items.length; i++) {
if (!allBinds.has(items[i])) { needs = true; break; }
}
if (needs) reinit();
}
function debounced() { clearTimeout(timer); timer = setTimeout(check, 300); }
new MutationObserver(debounced).observe(document.body, { childList: true, subtree: true });
debounced();
} catch (err) {
console.error('[lpx-fix] failed:', err);
}
})();
</script>
The proper upstream fix is already merged into the LeptonX rel-5.3 branch (GitHub PR volosoft/lepton#3487) and will ship with the next LeptonX 5.3.x patch (alongside the next ABP 10.3.x patch). Once you upgrade to that patch, this whole script block can be removed.
Full background (root cause, repro, impersonation trigger) is in the support question you linked: https://abp.io/support/questions/10622/leptonx-530-blazor-server-menu-collapse-bug
Note: downgrading v10 minor versions (10.3 → 10.2 / 10.1 / 10.0) does not reliably avoid this. The relevant TypeScript source in pro-side-menu.ts and base-side-menu.ts is identical from 5.0.0 through 5.3.0 — the race exists in earlier versions too, it just may not have been triggered for everyone. Cross-minor downgrades also bring DB / API churn that isn't worth it.
.btn-light --bs-btn-bg color changeThis one is not a bug, it's an intentional theme change in LeptonX 5.x. I verified it directly from the LeptonX repo by comparing bootstrap-dim.css across versions:
LeptonX 4.1.3 (ABP 9.1.3): .btn-light { --bs-btn-bg: #eeeeee; }
LeptonX 5.0.0 (ABP 10.0.0): .btn-light { --bs-btn-bg: #bbb; }
LeptonX 5.0.1 (ABP 10.0.1): .btn-light { --bs-btn-bg: #bbb; }
LeptonX 5.1.x: .btn-light { --bs-btn-bg: #bbb; }
LeptonX 5.2.x: .btn-light { --bs-btn-bg: #bbb; }
LeptonX 5.3.0 (ABP 10.3.0): .btn-light { --bs-btn-bg: #bbb; }
(Just a heads-up: in your post you have the values swapped — 9.1.3 was #eeeeee and 10.3 is #bbb, not the other way around.)
So the change happened at the LeptonX 4 → 5 major bump as part of the dim-theme palette refresh, and every v10 minor uses the new value. Downgrading inside v10 won't restore the old #eeeeee look.
If you want the 9.1.x appearance back, override the variable in your own site CSS. LeptonX puts the active style on the <body> as a class (lpx-theme-dim / lpx-theme-light / lpx-theme-dark — set in SideMenuLayout.OnAfterRenderAsync), and bootstrap-{style}.css is loaded conditionally per theme by AppearanceStyles.razor. So the simplest, most reliable override is:
body.lpx-theme-dim .btn-light {
--bs-btn-bg: #eeeeee;
--bs-btn-border-color: #eeeeee;
}
The body.lpx-theme-dim prefix beats the bare .btn-light selector in the LeptonX bundle on specificity (0,2,0 vs 0,1,0), so load order doesn't matter. Use body.lpx-theme-light / body.lpx-theme-dark if you also want to override those themes. The .btn-light.soft, .btn-light.grey etc. variants follow the same pattern.
You wrote "Example: ..." in your post — if there are other visual differences besides .btn-light that you want to revert (specific buttons, panels, sidebars, etc.), please share screenshots or the exact selectors and we can give you the matching overrides.
Practical recommendation: stay on 10.3.0, apply the #10622 workaround for the sidebar, and override the CSS variables you care about. Then upgrade to 10.3.x once the LeptonX 5.3.x patch is out — the workaround can be removed at that point and the CSS override can stay (or not) depending on which look you prefer.
Thanks
Hi Aníbal,
Quick update — we've granted access to the Volo.Abp.OperationRateLimiting.* packages for the Personal license on our side. Could you give it another try?
dotnet nuget locals all --clear
dotnet restore
The 403 should be gone now. Let me know if it still fails on your side.
Thanks,
Hi,
Mapperly is a compile-time source generator, not a runtime mapper. All of its ignore configuration is expressed as attributes that take a hard-coded property name as a string literal — [MapperIgnoreTarget(nameof(SurveyData.Foo))], [MapperIgnoreSource(...)], [MapperIgnoreObsoleteMembers], etc. The decision of "which fields to map" is baked into the generated code at build time and cannot be driven by a runtime List<string>. So IgnoreNotInListMembers(List<string>) has no direct equivalent in Mapperly.
The Mapperly-idiomatic way to model "only update some fields" is to change the API contract: introduce a small patch DTO that contains only the patchable fields (typically as nullable properties), and let Mapperly generate a partial update mapping for it at compile time. No reflection, no string list, fully type-checked at build time. If you can refactor the API to that shape, this is the recommended path long-term.
If keeping the existing requiredProperties shape is important (for example, the field list comes from the client and you don't want to change the contract), you can keep using Mapperly for the actual mapping and use a small reusable helper to commit only the listed fields. Mapperly does the real work (including type conversions, nested mappings and any user-defined converters); reflection is only used to pick which properties to copy over, and PropertyInfo lookup is cached per destination type so the per-call cost stays low.
using System.Collections.Concurrent;
using System.Reflection;
public static class PatchExtensions
{
private static readonly ConcurrentDictionary<Type, Dictionary<string, PropertyInfo>> WritablePropertyCache = new();
public static void PatchFromMapped<TSource, TDestination>(
TSource source,
TDestination destination,
Func<TSource, TDestination> mapFunc,
IReadOnlyCollection<string>? requiredProperties)
where TSource : class
where TDestination : class
{
if (source is null || destination is null || requiredProperties is null || requiredProperties.Count == 0)
{
return;
}
var mapped = mapFunc(source);
var writableProps = WritablePropertyCache.GetOrAdd(
typeof(TDestination),
static t => t.GetProperties(BindingFlags.Instance | BindingFlags.Public)
.Where(p => p.CanWrite)
.ToDictionary(p => p.Name));
foreach (var name in requiredProperties)
{
if (writableProps.TryGetValue(name, out var prop))
{
prop.SetValue(destination, prop.GetValue(mapped));
}
}
}
}
Usage (replaces mapper.Map(uploaded, surveyData) + IgnoreNotInListMembers):
PatchExtensions.PatchFromMapped(uploaded, surveyData, mapper.Map, requiredProperties);
The same helper covers all your TSource/TDestination pairs (including the 100+ property classes) with no per-class boilerplate. There is one fully-mapped intermediate allocated per call plus reflection GetValue/SetValue, which is fine for typical request paths (sub-microsecond per call in a quick benchmark). For genuinely hot paths, prefer the patch DTO approach above.
For your ITypeConverter<double?, CurrencyObject[]?>, Mapperly's design is different from AutoMapper. Instead of registering a converter type, Mapperly looks for user-implemented mapping methods directly on the mapper class — any normal method whose signature matches a type pair Mapperly needs to map is picked up automatically (AutoUserMappings is true by default). No attribute, no registration, just a method.
using Riok.Mapperly.Abstractions;
[Mapper]
public partial class SurveyDataMapper
{
public partial SurveyData Map(UploadedSurveyData source);
public partial void Map(UploadedSurveyData source, SurveyData destination);
private static CurrencyObject[]? ToCurrencyObjects(double? source)
=> source.HasValue
? new[] { new CurrencyObject { CurrencyValue = source.Value, Currency = "USD" } }
: null;
}
Verified locally with Mapperly 4.3.1; the generated code calls your method directly:
// generated SurveyDataMapper.g.cs
target.Salary = ToCurrencyObjects(source.Salary);
If you want a single converter shared across multiple mappers (the AutoMapper "register once" behavior), put it in a static class and reference it from each mapper via [UseStaticMapper]:
public static class SurveyConverters
{
public static CurrencyObject[]? ToCurrencyObjects(double? source)
=> source.HasValue
? new[] { new CurrencyObject { CurrencyValue = source.Value, Currency = "USD" } }
: null;
}
[Mapper]
[UseStaticMapper(typeof(SurveyConverters))]
public partial class AnotherMapper
{
public partial AnotherDto Map(AnotherEntity source);
}
Mapperly docs:
Thanks
Hi Mucahit,
The empty <abp-dynamic-layout> (only <!--ng-container--><!--container--> inside) means ReplaceableComponentsService.get('Theme.ApplicationLayoutComponent') returned undefined, i.e. the application layout component was never registered.
In ABP v10 / LeptonX 5.x, layout registration moved to explicit provide*Layout() functions. provideThemeLeptonX() only registers the AccountLayout. The ApplicationLayout (and EmptyLayout) are registered by provideSideMenuLayout() (or provideTopMenuLayout() if your shell is top-menu). If that call is missing from your migrated app.config.ts, every admin route resolves to a layout key with no registered component, and the container renders nothing.
Fix in app.config.ts:
import { provideThemeLeptonX } from '@volosoft/abp.ng.theme.lepton-x';
import { provideSideMenuLayout } from '@volosoft/abp.ng.theme.lepton-x/layouts';
export const appConfig: ApplicationConfig = {
providers: [
// ...
provideThemeLeptonX(),
provideSideMenuLayout(), // <-- the missing piece
// ...
],
};
Use provideTopMenuLayout from the same /layouts entry if your shell is top-menu instead.
If pages render with EmptyLayout instead of ApplicationLayout after the fix, make sure your admin routes go through RoutesService.add({ ..., layout: eLayoutType.application, ... }) (the standard ABP module pattern). The dynamic layout component reads the layout type from the RoutesService tree, not from Angular Router's route data on the root ActivatedRoute, so a plain data: { layout: 'application' } on a top-level route won't propagate.
Could you also share your current app.config.ts (just the providers list) and how your admin routes are registered (via RoutesService or directly in provideRouter)? That will help us confirm the exact spot.
Thanks