Activities of "maliming"

Hi,

Short answer: Suite (v10.3) can't really help here. Its entity designer doesn't have a 1:1 option at all — it generates 1:N navigation properties (HasOne(...).WithMany().HasForeignKey(...)) and N:N connection tables. There's no template that emits WithOne(), so neither the PK-to-PK shape nor a "separate FK column with WithOne()" 1:1 can be generated by Suite directly. It's also not designed to "scaffold UI on top of an existing hand-written entity" — Suite generates the entity, EF config, application service, controller, and pages together from its own model.

So your two realistic options are:

  1. Hand-write Person with the PK-to-PK shape from my previous answer. This keeps the relationship a true 1:1, the handler stays clean (new Person(eventData.Id)), and you write any application service / UI yourself if you ever need them. In practice Person is a satellite of IdentityUser populated by the event handler and rarely needs its own admin CRUD page (user management lives in the Identity Pro UI), so this is what I'd recommend.

  2. Drop the strict 1:1 and use a separate UserId FK column with a unique index. This is what Suite can actually scaffold (Domain + Application + Controller + UI), but model-wise it's a 1:N relationship that you "force" into 1:1 with a unique index — not a true EF Core 1:1. You also pay an extra column and have to use _guidGenerator.Create() for Person.Id. Pick this only if you really want Suite to generate the full CRUD page for Person.

Thanks

👍

Hi,

Quick correction first — IdentityUserEto does not exist. The earlier AI answer was wrong. What you actually have in ABP Commercial is Volo.Abp.Identity.IdentityUserCreatedEto (in Volo.Abp.Identity.Pro.Application.Contracts). For the delete side, ABP Commercial does not ship a matching IdentityUserDeletedEto, so you use the framework-level EntityDeletedEto<UserEto> from Volo.Abp.Users.

Also you don't need to register EtoMappings.Add<IdentityUser, ...>() — AbpIdentityDomainModule already does that and turns on auto-event publishing for you.

EF Core 1:1 mapping — Person.Id should be the user's Id

For a real 1-to-1 with IdentityUser in EF Core, the cleanest approach is a PK-to-PK relationship (shared primary key): Person.Id is both the primary key and the foreign key to IdentityUser.Id. No separate UserId column, no extra unique index needed, and the relationship is automatically required.

public class Person : Entity<Guid>
{
    public string FullName { get; set; }

    protected Person() { }
    public Person(Guid identityUserId) : base(identityUserId) { }
}

This works directly with the standard ABP application template, because your MyProjectNameDbContext is already declared as [ReplaceDbContext(typeof(IIdentityDbContext))], implements IIdentityDbContext, and calls builder.ConfigureIdentity() — so IdentityUser is part of the same model and the fluent API below resolves the FK target without any extra setup.

builder.Entity<Person>(b =>
{
    b.ToTable("Persons");
    b.ConfigureByConvention();

    b.HasOne<IdentityUser>()
        .WithOne()
        .HasForeignKey<Person>(p => p.Id);
});

This means inside the event handlers below, you pass eventData.Id (the IdentityUser.Id) directly as the Person's primary key — no IGuidGenerator needed.

1) Create the Person when an IdentityUser is created

using System;
using System.Threading.Tasks;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Domain.Repositories;
using Volo.Abp.EventBus.Distributed;
using Volo.Abp.Identity;

public class PersonCreator :
    IDistributedEventHandler<IdentityUserCreatedEto>,
    ITransientDependency
{
    private readonly IRepository<Person, Guid> _personRepository;

    public PersonCreator(IRepository<Person, Guid> personRepository)
    {
        _personRepository = personRepository;
    }

    public async Task HandleEventAsync(IdentityUserCreatedEto eventData)
    {
        if (await _personRepository.AnyAsync(x => x.Id == eventData.Id))
        {
            return; // idempotent — important for distributed handlers
        }

        await _personRepository.InsertAsync(new Person(eventData.Id), autoSave: true);
    }
}

IdentityUserCreatedEto carries Id, TenantId and a Properties dictionary (SendConfirmationEmail, AppName). If you need UserName / Email / Name on the Person, fetch the user inside the handler via IIdentityUserRepository or IdentityUserManager.

2) Delete the Person when the IdentityUser is deleted

A note before the code: IdentityUser is soft-deleted by ABP (it inherits FullAuditedAggregateRoot<Guid> which implements ISoftDelete), so deleting a user only sets IsDeleted = true — the row stays in the database. That means a database-level ON DELETE CASCADE on the FK does not fire and you cannot rely on EF Core's cascade behavior to remove Person. You really do need an event handler:

using System;
using System.Threading.Tasks;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Domain.Entities.Events.Distributed;
using Volo.Abp.Domain.Repositories;
using Volo.Abp.EventBus.Distributed;
using Volo.Abp.Users;

public class PersonRemover :
    IDistributedEventHandler<EntityDeletedEto<UserEto>>,
    ITransientDependency
{
    private readonly IRepository<Person, Guid> _personRepository;

    public PersonRemover(IRepository<Person, Guid> personRepository)
    {
        _personRepository = personRepository;
    }

    public async Task HandleEventAsync(EntityDeletedEto<UserEto> eventData)
    {
        var person = await _personRepository.FindAsync(eventData.Entity.Id);
        if (person != null)
        {
            await _personRepository.DeleteAsync(person, autoSave: true);
        }
    }
}

EntityDeletedEto<UserEto> is auto-published by ABP for any IdentityUser deletion path, so no extra wiring is needed on the publisher side. If your Person also implements ISoftDelete, DeleteAsync will respect that and only flag it as deleted; otherwise it removes the row.

Important caveat about IdentityUserCreatedEto

It is only published from IIdentityUserAppService.CreateAsync (the Identity Pro management UI / API). It will NOT fire when a user is created via:

  • a direct IdentityUserManager.CreateAsync call (e.g. custom registration logic)
  • external login first-time user creation (Google / Microsoft / etc.)
  • data seeders / migration code

If you also need Person in those cases, swap the create handler to IDistributedEventHandler<EntityCreatedEto<UserEto>> — that one is auto-published for every IdentityUser insert path:

public class PersonCreator :
    IDistributedEventHandler<EntityCreatedEto<UserEto>>,
    ITransientDependency
{
    // ... same body, use eventData.Entity.Id
}

Put the handlers in your .Application project (it already references Volo.Abp.Identity.Pro.Application.Contracts for IdentityUserCreatedEto).

Thanks

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:

  • https://github.com/dotnet/aspnetcore/issues/62916 - "Passkeys - Illegal invocation with 1Password"
  • https://github.com/dotnet/aspnetcore/pull/62917 - community fallback PR (closed, not merged into the template)
  • https://github.com/dotnet/aspnetcore/issues/63078 - tracked as a docs-only fix

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:

  • The empty catch { } swallows everything — consider Logger.LogException(ex) so you can actually see when the HTTP call fails.
  • You can drop 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.dll
  • Volo.Abp.Studio.Extensions.StandardSolutionTemplates.Cli.dll

Then 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:

  • Output of dotnet --list-sdks and dotnet --list-runtimes
  • Output of dir "$env:USERPROFILE\.nuget\packages\volo.abp.studio.extensions.standardsolutiontemplates\2.2.7\lib\net10.0\"
  • The exact command you were running when the error appeared
  • Your abp --version output

We'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.

Bug 1 workaround

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.

Bug 2 fix

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.

NgModule bootstrap

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?

  • Start from the default ABP 10.3.0 Blazor Server template with the LeptonX SideMenu layout.
  • Add only what's needed to render the broken page from the third screenshot.
  • Strip out unrelated modules, custom auth, business data, etc. — keep dependencies as light as possible.

Thanks

Hi,

A few clarifications based on your follow-up questions, because the helper does not have the two problems you were worried about.

1. mapper.Map does not overwrite the existing destination first

The 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)

2. The helper does not need to recurse into the object graph

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.

3. Where to call the helper

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.

1) Sidebar dropdown not expanding

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.

2) .btn-light --bs-btn-bg color change

This 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.

On the downgrade question

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

Showing 541 to 550 of 12926 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 02, 2026, 06:56
1
ABP Assistant
🔐 You need to be logged in to use the chatbot. Please log in first.