OVERRIDE_LEPTON_X_USER_MENU_PROVIDERS is correctly placed in app.config.ts.UserMenuService in all cases. The LeptonX user dropdown (“context menu”) can be built from LeptonX’s own userActionGroups (coming from the LeptonX toolbar container / user profile service), so removing items via UserMenuService won’t affect what that LeptonX context menu renders.ABP’s own LeptonX Lite docs explicitly say the user menu is customizable by replacing the CurrentUser slot:
this.replaceableComponents.add({
component: YourNewCurrentUserComponent,
key: eThemeLeptonXComponents.CurrentUser,
});
So the reliable solution in v10 is to replace the component that renders the dropdown, and inside it, you can show whatever items you want (only Logout, etc.). This bypasses the mismatch between UserMenuService and LeptonX context menu groups.
If you want to keep LeptonX toolbar but customize the profile dropdown, ABP support guidance also uses Toolbar replacement (replace eThemeLeptonXComponents.Toolbar with a custom toolbar container that renders the context menu). That approach is consistent with what you already discovered (creating your own dropdown).
provideSideMenuLayout() + provideThemeLeptonX() as-is.UserMenuService.removeItem(...) for LeptonX’s profile dropdown.eThemeLeptonXComponents.CurrentUser if you only replace the user area.eThemeLeptonXComponents.Toolbar if you need broader toolbar control.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.
Not rolling back does not, by itself, create a “zombie transaction”. A “zombie” session/transaction (long-running sleeping session holding locks, blocking others) is typically caused by a transaction that remains open for a long time (e.g., long-running UOW, dead code path that never completes, waiting on external I/O inside a transactional UOW, etc.).
In the pattern you showed earlier (catch + swallow inside a [UnitOfWork] method), what usually happens is:
CompleteAsync() automatically.CompleteAsync() will commit. That means the transaction ends, so it should not become a zombie because of the swallow.The swallow pattern can still indirectly contribute to long blocking because it may:
Manual UOW started but never completed
SaveChangesAsync() but forgetting CompleteAsync() is a classic cause of “open transaction until dispose/timeout”.CompleteAsync() in a transactional UOW, and if CompleteAsync() is not called the transaction is rolled back when it ends—however, if the scope is kept alive too long, it can hold locks too long.Long-running work inside a transactional UOW
Calling UpdateAsync from a frequently hit GET
[UnitOfWork] methods: do not swallow exceptions unless the failure is truly optional; if you swallow, consider explicitly calling await CurrentUnitOfWork.RollbackAsync() to guarantee no commit when transactional.using (var uow = _unitOfWorkManager.Begin(isTransactional: true))
{
// db work
await uow.CompleteAsync();
}
Reference (verified):
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.
With [UnitOfWork], ABP will automatically call CompleteAsync() if your method returns successfully (i.e., no exception escapes the method). If an exception escapes, ABP will not complete it; if it was transactional, it will roll back when disposed.
In your code, the exception is swallowed, so from ABP’s perspective the method completed successfully. That means:
So: this does terminate “gracefully” from ABP’s viewpoint, but it does not roll back. It commits whatever is in the UOW unless you explicitly roll it back or rethrow.
Option A (recommended): log + rethrow
[UnitOfWork]
public virtual async Task<SaveResult> UpdateDocumentsAsync(Guid id, Guid[] checklistDocumentIds)
{
try
{
// db operations
return SaveResult.Success();
}
catch (Exception ex)
{
_logger.LogException(ex, LogLevel.Error);
throw; // ensure ABP doesn't complete/commit the UOW
}
}
Option B: if you intentionally want to swallow, explicitly rollback (only meaningful if the UOW is transactional)
using Volo.Abp.Uow;
[UnitOfWork]
public virtual async Task<SaveResult> UpdateDocumentsAsync(Guid id, Guid[] checklistDocumentIds)
{
try
{
// db operations
return SaveResult.Success();
}
catch (Exception ex)
{
_logger.LogException(ex, LogLevel.Error);
await CurrentUnitOfWork.RollbackAsync(); // prevents commit
return SaveResult.Failed(ex.Message);
}
}
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.
Yes. When you use the [UnitOfWork] attribute, ABP handles the Unit of Work lifecycle for you via interceptors: it begins the UOW, runs your method, and if the method finishes successfully it calls CompleteAsync() automatically. If the method throws an exception, ABP does not complete the UOW; if it was transactional, it will be rolled back when the UOW is disposed.
You should not manually call CompleteAsync() in a method that is already managed by [UnitOfWork] (or by the application service UOW convention). You only call CompleteAsync() when you start a UOW manually with IUnitOfWorkManager.Begin(...).
Important: for [UnitOfWork] to work, the method generally must be interceptable (commonly: public virtual in a DI-resolved class).
Sources (validated):
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.
[UnitOfWork] on the method, ABP will create/participate in a UOW scope for that method (assuming interception works: the method should be virtual if needed per ABP rules). Whether it is transactional depends on ABP’s transaction behavior (default is Auto for HTTP requests: GET is non-transactional, others transactional). If you want it guaranteed transactional, specify it:[UnitOfWork(IsTransactional = true)]
public virtual async Task Create30DaysReminderAsync(Guid fileId)
{
// ...
}
[UnitOfWork]
public async Task Create30DaysReminderAsync(Guid fileId)
{
try
{
// some Db operations;
await CurrentUnitOfWork.SaveChangesAsync();
}
catch
{
throw;
}
}
Rollback / commit behavior:
If an exception is thrown out of the method (and you throw; does exactly that), then CompleteAsync() will not be successfully executed, so:
SaveChangesAsync() are already persisted and cannot be rolled back (by definition of non-transactional UOW).Your try/catch { throw; } is effectively redundant (same as not catching), but it does not harm rollback behavior.
Important nuance from ABP docs: SaveChangesAsync() (and repository autoSave) flushes changes, but they are not permanently committed until CompleteAsync() is called successfully in a transactional UOW. If CompleteAsync() is not called (or the method exits by exception), ABP rolls back the transaction.
Validated source:
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.
UserMenuService modifications are not being applied at the right time (or they get reset/rebuilt after you apply them). This is a known timing issue especially around login/tenant context changes.Apply the customization when currentUser becomes authenticated (not only in an app initializer), and do it idempotently (so it can safely run multiple times):
import { inject, Injectable } from '@angular/core';
import { ConfigStateService } from '@abp/ng.core';
import { UserMenuService } from '@abp/ng.theme.shared';
import { eUserMenuItems } from '@volosoft/abp.ng.theme.lepton-x';
import { Router } from '@angular/router';
@Injectable({ providedIn: 'root' })
export class UserMenuCustomizer {
private applied = false;
private configState = inject(ConfigStateService);
private userMenu = inject(UserMenuService);
private router = inject(Router);
init() {
this.configState.getOne$('currentUser').subscribe(u => {
if (!u?.isAuthenticated) {
this.applied = false;
return;
}
if (this.applied) return;
this.applied = true;
[
eUserMenuItems.LinkedAccounts,
eUserMenuItems.SecurityLogs,
eUserMenuItems.AuthorityDelegation,
eUserMenuItems.ExternalLogins,
eUserMenuItems.Sessions,
].forEach(i => this.userMenu.removeItem(i));
this.userMenu.patchItem(eUserMenuItems.MyAccount, {
action: () => this.router.navigate(['/account/manage']),
});
});
}
}
Register it (your existing provideAppInitializer is fine, but call the service’s init() so it subscribes and runs at the right moment):
import { provideAppInitializer, inject } from '@angular/core';
import { UserMenuCustomizer } from './user-menu-customizer';
export const OVERRIDE_LEPTON_X_USER_MENU_PROVIDERS = [
provideAppInitializer(() => {
inject(UserMenuCustomizer).init();
}),
];
This avoids the “run too early” problem and ensures the changes are applied once right after login.
Notes:
patchItem(eUserMenuItems.MyAccount, ...) over removing + re-adding MyAccount.(Also: I can’t access ABP’s indexed GitHub repos right now due to an upstream Cloudflare tunnel error from the SourceBot code-search service, so I can’t provide verified source-code links for the internal rebuild timing. The official docs below cover UserMenuService usage.)
Sources (validated)
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.
UserMenuService (remove/add/patch).addItems() code is wrong.Here’s the ABP/LeptonX-supported way to solve it without relying on “custom toolbar hacks”:
MyAccount; patch itIn LeptonX, the UI expects certain built-in items to exist. Removing + re-adding with the same id can lead to unexpected behavior depending on when LeptonX builds the dropdown.
Use patchItem to change the action, and removeItem only for the items you truly want to hide:
import { inject } from '@angular/core';
import { Router } from '@angular/router';
import { ConfigStateService } from '@abp/ng.core';
import { UserMenuService } from '@abp/ng.theme.shared';
import { eUserMenuItems } from '@volosoft/abp.ng.theme.lepton-x';
export function configureUserMenu() {
const userMenu = inject(UserMenuService);
const router = inject(Router);
const configState = inject(ConfigStateService);
return () => {
// Ensure it runs after authentication state exists
configState.getOne$('currentUser').subscribe(currentUser => {
if (!currentUser?.isAuthenticated) return;
userMenu.removeItem(eUserMenuItems.LinkedAccounts);
userMenu.removeItem(eUserMenuItems.SecurityLogs);
userMenu.removeItem(eUserMenuItems.AuthorityDelegation);
userMenu.removeItem(eUserMenuItems.ExternalLogins);
userMenu.removeItem(eUserMenuItems.Sessions);
userMenu.patchItem(eUserMenuItems.MyAccount, {
action: () => router.navigate(['/account/manage']),
});
});
};
}
Then register it via provideAppInitializer the way you already do.
LeptonX documents that the User Menu is part of the CurrentUser component area (toolbar, top-right). If that component is broken or not matching after upgrade, you should replace that slot (not Toolbar/NavItems etc.):
this.replaceableComponents.add({
component: YourNewCurrentUserComponent,
key: eThemeLeptonXComponents.CurrentUser,
});
This is the official LeptonX Lite guidance and applies conceptually to LeptonX Angular customization as well.
If your goal is only changing where “My Account” goes, ABP also supports overriding the NAVIGATE_TO_MANAGE_PROFILE token instead of rewriting menu items (this avoids timing issues after login).
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.
try { ... } catch { } and swallow the exception, then execution continues and you later call await uow.CompleteAsync().So this pattern can lead to longer transactions and more lock holding, which matches “zombie” sessions / timeouts.
A) If an exception means the business operation should fail → do NOT swallow
using (var uow = _unitOfWorkManager.Begin(isTransactional: true))
{
// db ops...
try
{
// db ops...
}
catch
{
// optionally log
throw; // important: abort the operation
}
// db ops...
await uow.CompleteAsync();
}
B) If you intentionally want “best effort” and still commit other changes Then don’t keep everything in one big transactional UOW. Either:
requiresNew: true UOW for the part that can fail independently.Example (separate UOW for the optional part):
using (var uow = _unitOfWorkManager.Begin(isTransactional: true))
{
// main db ops...
try
{
using (var innerUow = _unitOfWorkManager.Begin(requiresNew: true, isTransactional: true))
{
// optional db ops...
await innerUow.CompleteAsync();
}
}
catch
{
// swallow only if it is truly optional (but log it)
}
// continue main db ops...
await uow.CompleteAsync();
}
SaveChangesAsync() / autoSave does not finalize changes permanently until CompleteAsync() succeeds. So swallowing exceptions and still calling CompleteAsync() can cause “commit despite internal failures” patterns and longer locking windows.CompleteAsync() is mandatory to properly finish it.Links (validated):
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.
TenantUserSharingStrategy.Shared), the user identity is global, so security settings like 2FA are evaluated at the Host side (not per-tenant). This is expected behavior and is how ABP is designed for shared accounts.What to do (staging fix checklist that typically resolves “login loops” specifically when 2FA is enabled):
app.UseForwardedHeaders();
(Plus the corresponding `ForwardedHeadersOptions` configuration if needed in your environment.)
Verify Data Protection key persistence across instances/restarts
If you are using OpenIddict / separated AuthServer, ensure all auth-related hosts share consistent settings
Confirm 2FA configuration is done at the correct level
No verified source available for a specific “shared accounts + 2FA causes redirect loop” known issue; the above items are the standard, proven causes in staging/production for “back to login page” loops during multi-step sign-in flows (like 2FA).
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.
Yes — not calling CompleteAsync() after SaveChangesAsync() in a manually started Unit of Work is a real bug and can absolutely contribute to “zombie”/long-running transactions and blocking. In ABP, SaveChangesAsync() flushes changes to the database within the current UOW, but the transaction is only permanently committed when CompleteAsync() is successfully executed. If CompleteAsync() is never called, the UOW is not considered completed; if it’s transactional, it won’t commit (and may hold resources until disposed), and ABP also won’t run the “end-of-UOW” pipeline (events, cleanup, etc.).
This is consistent with your symptom: a simple concurrency update (... WHERE Id AND ConcurrencyStamp) timing out because it’s blocked by another session/UOW that is kept open too long.
What you should change in your code (manual UOW usage)
using Volo.Abp.Uow;
using (var uow = _unitOfWorkManager.Begin(isTransactional: true))
{
// ... do repository/DbContext operations ...
await uow.SaveChangesAsync(); // optional (flush mid-way)
await uow.CompleteAsync(); // mandatory (commit + finalize UOW)
}
using (var uow = _unitOfWorkManager.Begin(isTransactional: false))
{
// operations
await uow.CompleteAsync();
}
Is this “more visible” in ABP 10 because of UOW enhancements?
CompleteAsync() is the method that finalizes a UOW (commit if transactional + executes UOW end tasks). If your code previously “seemed to work” in ABP 8, it was still incorrect; ABP 10 + newer EF Core / higher load patterns can make the consequences (blocking/timeouts) show up more often.Verified reference
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.