Activities of "AI-Bot"

  1. Root cause
  • Mapperly only maps properties that have a setter.
  • Your aggregate root’s Id comes from Entity<TPrimaryKey> / AggregateRoot<TPrimaryKey> and typically has a protected set – that’s fine for reading (Entity -> DTO), but:
    • For DTO -> Entity (especially updates) Mapperly is generating code that creates a new entity instance and does not (and should not) set Id.
    • When you set ParameterlessConstructor = false, Mapperly uses a constructor instead of the parameterless one, which again creates a new entity instance. That breaks EF Core tracking (original tracked entity vs new entity) and especially child collections.

In ABP, the recommended pattern is:

  • Never create a new entity instance when updating.
  • Never map the Id from DTO back to the entity. Id is taken from the route/method parameter and the existing entity instance, not from the DTO.

So the problem is not “Mapperly can’t map Id”, but that the mapping is being used in a way that replaces the tracked entity instance.


  1. Recommended fix (ABP + Mapperly + EF Core)

a) Define separate mappers for Create and Update

Create DTO → Entity (OK to create a new entity):

using Riok.Mapperly.Abstractions;
using Volo.Abp.Mapperly;

[Mapper]
public partial class MyEntityCreateDtoToMyEntityMapper 
    : MapperBase<MyEntityCreateDto, MyEntity>
{
    // For create, you usually don't have Id in DTO; EF or your code generates it.
    public partial MyEntity Map(MyEntityCreateDto source);
}

Update DTO → existing Entity (no new instance, no Id mapping):

using Riok.Mapperly.Abstractions;
using Volo.Abp.Mapperly;

[Mapper]
public partial class MyEntityUpdateDtoToMyEntityMapper 
    : MapperBase<MyEntityUpdateDto, MyEntity>
{
    // Important: map *into existing* entity
    public partial void Map(MyEntityUpdateDto source, MyEntity destination);
}

If your update DTO contains Id, explicitly ignore it:

[Mapper]
public partial class MyEntityUpdateDtoToMyEntityMapper 
    : MapperBase<MyEntityUpdateDto, MyEntity>
{
    [MapProperty(Ignore = true)]
    public long Id { get; set; } // or use [MapperIgnoreTarget(nameof(MyEntity.Id))] on the mapper

    public partial void Map(MyEntityUpdateDto source, MyEntity destination);
}

or with attribute on the entity side:

[Mapper]
public partial class MyEntityUpdateDtoToMyEntityMapper 
    : MapperBase<MyEntityUpdateDto, MyEntity>
{
    [MapperIgnoreTarget(nameof(MyEntity.Id))]
    public partial void Map(MyEntityUpdateDto source, MyEntity destination);
}

This ensures Id is never touched during update mappings.


b) Use the “map-into-existing” pattern in your AppService

Correct (preserves EF tracking and children):

public class MyEntityAppService :
    CrudAppService<
        MyEntity,
        MyEntityDto,
        Guid,
        PagedAndSortedResultRequestDto,
        MyEntityCreateDto,
        MyEntityUpdateDto>
{
    public MyEntityAppService(IRepository<MyEntity, Guid> repository)
        : base(repository)
    {
    }

    protected override async Task MapToEntityAsync(MyEntityUpdateDto input, MyEntity entity)
    {
        // entity is tracked by EF; just map into it
        ObjectMapper.Map(input, entity);
    }

    protected override Task<MyEntity> MapToEntityAsync(MyEntityCreateDto input)
    {
        // For create, Mapperly is allowed to create a new instance
        var entity = ObjectMapper.Map<MyEntityCreateDto, MyEntity>(input);
        return Task.FromResult(entity);
    }
}

Avoid patterns like:

// BAD for updates – creates a *new* instance
var entity = ObjectMapper.Map<MyEntityUpdateDto, MyEntity>(input);
await _repository.UpdateAsync(entity);

or relying on ParameterlessConstructor = false to select another constructor: both approaches replace the tracked instance and cause the detached/duplicate-tracking issues you’re seeing with sub-entities.


c) Do not try to force mapping to Id

Even if you expose Id with a public setter, you generally should not map it:

  • For create, Id is generated by EF Core or your own code (IGuidGenerator, sequences, etc.).
  • For update, Id comes from the route or method parameter and is used to load the existing entity; mapping it from DTO is at best redundant, at worst dangerous.

Let Mapperly ignore Id (via attributes as above), and rely on the existing aggregate root instance loaded from the repository.


  1. About EF Core change tracking & child collections

When you:

  • Load entity with GetAsync / FirstOrDefaultAsync inside the same UoW,
  • Use ObjectMapper.Map(input, existingEntity),

EF Core:

  • keeps the original instance tracked,
  • tracks changes to scalar and navigation properties (including child collections),
  • will properly generate updates and maintain tracking for sub-entities.

Creating a new MyEntity during mapping always risks:

  • old instance stays tracked,
  • new one is added/attached separately,
  • child collections get attached twice or lost.

That’s exactly what your workaround with ParameterlessConstructor = false caused.


4. Summary

  • Keep Id protected and do not try to map it with Mapperly.
  • For updates, always:
    • load the existing entity from the repository,
    • use the Map(source, destination) pattern (no new entity),
    • ignore Id in the mapping.
  • For creates, use a separate mapper that returns a new aggregate instance; let EF or your domain logic set Id.

This pattern is aligned with ABP’s Mapperly integration and EF Core best practices, and it eliminates the EF change-tracking issues you’re seeing.


  1. Sources
  • ABP – Object to Object Mapping / Mapperly integration
    https://abp.io/docs/latest/framework/infrastructure/object-to-object-mapping

  • ABP – AutoMapper to Mapperly migration (Mapperly mapper patterns)
    https://abp.io/docs/latest/release-info/migration-guides/AutoMapper-To-Mapperly


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.

  1. Solution

The problem is purely on the Angular side:
you have a menu item pointing to /rag-engine, but there is no Angular route that matches this path, so the app falls back to the built‑in 404 page.

You already have RagEngineComponent in a separate Angular library (projects/rag-engine/...). Now you must expose it via routing and connect that route to your main app.

Below are two concrete ways, depending on whether your app uses standalone routing (app.routes.ts) or module routing (app-routing.module.ts).


A. If your app uses standalone routing (app.routes.ts)

  1. Create a route file for RagEngine

In angular/src/app/rag-engine/rag-engine.routes.ts:

import { Routes } from '@angular/router';
import { authGuard, permissionGuard } from '@abp/ng.core';

export const RAG_ENGINE_ROUTES: Routes = [
  {
    path: '',
    loadComponent: () =>
      import('../../projects/rag-engine/src/lib/components/rag-engine.component')
        .then(c => c.RagEngineComponent),
    canActivate: [authGuard, permissionGuard],
    // optional, if you want permission:
    // data: { requiredPolicy: 'RagEngine.RagEngine' },
  },
];
  1. Register it in app.routes.ts

In angular/src/app/app.routes.ts:

import { Routes } from '@angular/router';
import { RAG_ENGINE_ROUTES } from './rag-engine/rag-engine.routes';

export const APP_ROUTES: Routes = [
  {
    path: '',
    pathMatch: 'full',
    loadComponent: () =>
      import('./home/home.component').then(m => m.HomeComponent),
  },
  // ... other routes ...

  // >>> Add this block <<<
  {
    path: 'rag-engine',
    children: RAG_ENGINE_ROUTES,
  },
];
  1. Make sure the menu path matches

Where you added the menu item (usually route.provider.ts or wherever you create the main menu entry), ensure the path is exactly /rag-engine:

{
  path: '/rag-engine',
  name: '::Menu:RagEngine',
  iconClass: 'fa fa-database',
  layout: eLayoutType.application,
  // requiredPolicy: 'RagEngine.RagEngine', // if you added a policy
}

Now clicking RagEngine should route to RagEngineComponent instead of the 404 page.


B. If your app uses module-based routing (app-routing.module.ts)

If your Angular app still has AppModule and app-routing.module.ts:

  1. Add routing to the RagEngine library

Inside projects/rag-engine/src/lib/rag-engine-routing.module.ts:

import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { RagEngineComponent } from './components/rag-engine.component';
import { AuthGuard, PermissionGuard } from '@abp/ng.core';

const routes: Routes = [
  {
    path: '',
    component: RagEngineComponent,
    canActivate: [AuthGuard, PermissionGuard],
    // data: { requiredPolicy: 'RagEngine.RagEngine' },
  },
];

@NgModule({
  imports: [RouterModule.forChild(routes)],
  exports: [RouterModule],
})
export class RagEngineRoutingModule {}

And in projects/rag-engine/src/lib/rag-engine.module.ts (if not already):

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RagEngineComponent } from './components/rag-engine.component';
import { RagEngineRoutingModule } from './rag-engine-routing.module';

@NgModule({
  declarations: [RagEngineComponent],
  imports: [
    CommonModule,
    RagEngineRoutingModule,
  ],
})
export class RagEngineModule {}
  1. Lazy‑load the module from the main app

In angular/src/app/app-routing.module.ts:

const routes: Routes = [
  {
    path: '',
    pathMatch: 'full',
    loadChildren: () =>
      import('./home/home.module').then(m => m.HomeModule),
  },
  // ... other routes ...

  // >>> Add this block <<<
  {
    path: 'rag-engine',
    loadChildren: () =>
      import('rag-engine').then(m => m.RagEngineModule),
    // or, if you created it under src/app/rag-engine:
    // loadChildren: () =>
    //   import('./rag-engine/rag-engine.module').then(m => m.RagEngineModule),
  },
];
  1. Keep the menu path as /rag-engine, same as in section A.

C. Quick checklist to verify

  • [ ] RagEngineComponent builds without errors.
  • [ ] APP_ROUTES or routes array has an entry for path 'rag-engine'.
  • [ ] Your menu item uses path: '/rag-engine'.
  • [ ] If you use permissions, the current user has the required policy (or remove requiredPolicy while testing).

After these changes, run from the angular folder:

yarn        # or npm install
yarn start  # or npm start

Then click the RagEngine menu item; it should load your component instead of 404.


  1. Sources
  • ABP Angular quick start and routing patterns
    https://docs.abp.io/en/abp/latest/UI/Angular/Quick-Start

  • Community article showing adding a lazily loaded Angular module and registering route in app-routing.module.ts
    https://abp.io/community/articles/Using-DevExtremeAngularComponents-With-The-ABP-Framework

  • Standalone routing patterns used in latest ABP Angular templates
    https://abp.io/community/articles/zzi2rr2z
    https://abp.io/community/articles/7qu1m67s


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.

1. Solution

ABP does not use classic ASP.NET Core ISession in its templates, and there is no special “ABP session store” that you must use for a multi-instance/Kubernetes setup. In a multi-node environment, the recommended way to keep per-user/per-session state is:

  • Do NOT use in-memory state (won’t work across nodes).
  • Use a distributed store (e.g. Redis) via ABP’s caching abstractions.
  • Optionally, use claims (including Dynamic Claims) only for small, rarely changing data.

Since you explicitly set DynamicClaims = false and still want to hold some data for the duration of a user session, use the distributed cache keyed by user (and, if needed, by “session id”).


A. Basic pattern: per-user “session data” in distributed cache

  1. Define a DTO for the data you want to keep during the user’s session:
public class MySessionData
{
    public string SomeValue { get; set; }
    public int SomeNumber { get; set; }
    // add what you need
}
  1. Inject ABP’s typed distributed cache and ICurrentUser into an app service or other service:
using Volo.Abp.Caching;
using Volo.Abp.Users;

public class MySessionService : ITransientDependency
{
    private readonly IDistributedCache<MySessionData> _sessionCache;
    private readonly ICurrentUser _currentUser;

    public MySessionService(
        IDistributedCache<MySessionData> sessionCache,
        ICurrentUser currentUser)
    {
        _sessionCache = sessionCache;
        _currentUser = currentUser;
    }

    private string GetCacheKey(string? sessionId = null)
    {
        // User-based key; add a sessionId if you want multiple sessions per user.
        return $"MySessionData:{_currentUser.GetId()}{(sessionId == null ? "" : $":{sessionId}")}";
    }

    public async Task SetSessionDataAsync(MySessionData data, string? sessionId = null)
    {
        var key = GetCacheKey(sessionId);

        await _sessionCache.SetAsync(
            key,
            data,
            new DistributedCacheEntryOptions
            {
                // Expire when you consider the “session” to be over:
                AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(1)
            }
        );
    }

    public async Task<MySessionData?> GetSessionDataAsync(string? sessionId = null)
    {
        var key = GetCacheKey(sessionId);
        return await _sessionCache.GetAsync(key);
    }

    public async Task ClearSessionDataAsync(string? sessionId = null)
    {
        var key = GetCacheKey(sessionId);
        await _sessionCache.RemoveAsync(key);
    }
}
  1. Configure Redis (or another distributed cache) once (typically already done in tiered/Kubernetes setups). Because you’re using IDistributedCache<>, this will work transparently across all nodes.

  2. Call MySessionService from controllers, Razor pages, app services, etc. to read/write session data anywhere in the app.


B. If you need per-login session (not just per user)

If a user can have multiple concurrent logins and you need truly per-session data, you can:

  • Generate a session Id on login (e.g. GUID).
  • Store it once in a user claim or a cookie.
  • Use that session Id as part of the cache key (see sessionId parameter above).

Since you’ve disabled Dynamic Claims, you probably shouldn’t mutate claims after login; instead:

  • Add the session Id as a claim at login time (in your custom login logic), or
  • Store it in a secure cookie and read it server-side.

Then inject IHttpContextAccessor or your own helper to get the session Id and use it in GetCacheKey.


C. Why not Dynamic Claims / classic ASP.NET Session?

  • Dynamic Claims: meant for small identity-like data; each request rebuilds claims from a cache/remote source. You’ve disabled this, and even if enabled, it’s not ideal for larger or frequently changing session data.
  • ASP.NET Core ISession: ABP’s MVC templates don’t rely on it; using it in a multi-instance environment still requires a distributed backing store (e.g. Redis). ABP’s own guidance is to use distributed cache abstractions instead.

So the built-in structure that is suitable for Kubernetes / multi-instance is:

  • IDistributedCache<T> (+ ICurrentUser) for per-user/per-session data,
  • optionally combined with a small claim or cookie to represent a “session Id”.

This approach is:

  • Stateless at the node level.
  • Safe for Kubernetes scaling.
  • Fully aligned with ABP’s abstractions and caching infrastructure.

2. Sources

  • Dynamic Claims fundamentals (for background on why you might avoid them for large session data):
    https://github.com/abpframework/abp/blob/HEAD/docs/en/framework/fundamentals/dynamic-claims.md

  • ABP distributed/event-driven architecture guidance (multi-instance friendly patterns):
    https://docs.abp.io/en/abp/latest/distributed-event-bus


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.

  1. Solution

There is no built‑in “auto‑provision DB + user + connection string + then run migrations” pipeline in ABP. The default MyAppTenantDatabaseMigrationHandler assumes the tenant’s connection string is already set before the TenantCreatedEto is published. In your flow it is not, so the handler behaves as designed and skips migrations.

You basically have two safe options:


Option A – Move provisioning + migrations into the tenant creation flow (recommended)

Instead of relying on TenantCreatedEto for this scenario, override the tenant creation application service so that:

  1. Tenant is created (host DB).
  2. DB/user is provisioned.
  3. Tenant connection string is updated and committed.
  4. Migrations are executed for that tenant.

The key point is to separate the “create tenant” transaction from the “provision + migrate” transaction so that the migration step sees a committed connection string.

A minimal outline:

public class MyTenantAppService : SaasTenantAppService
{
    private readonly ITenantRepository _tenantRepository;
    private readonly ICurrentTenant _currentTenant;
    private readonly IUnitOfWorkManager _unitOfWorkManager;
    private readonly IEnumerable<IMyAppDbSchemaMigrator> _dbSchemaMigrators;
    private readonly ITenantDatabaseProvisioner _provisioner;

    public MyTenantAppService(
        ITenantRepository tenantRepository,
        ICurrentTenant currentTenant,
        IUnitOfWorkManager unitOfWorkManager,
        IEnumerable<IMyAppDbSchemaMigrator> dbSchemaMigrators,
        ITenantDatabaseProvisioner provisioner,
        // ... base deps
        ) : base(/* ... */)
    {
        _tenantRepository = tenantRepository;
        _currentTenant = currentTenant;
        _unitOfWorkManager = unitOfWorkManager;
        _dbSchemaMigrators = dbSchemaMigrators;
        _provisioner = provisioner;
    }

    public override async Task<SaasTenantDto> CreateAsync(SaasTenantCreateDto input)
    {
        // 1. Create tenant in host DB
        var tenantDto = await base.CreateAsync(input);

        // 2. Provision DB/user + build connection string
        var tenantConnectionString =
            await _provisioner.ProvisionAsync(tenantDto.Id, /* host conn info, etc. */);

        // 3. Persist connection string in a SEPARATE UoW and commit
        using (var uow = _unitOfWorkManager.Begin(requiresNew: true))
        {
            var tenant = await _tenantRepository.GetAsync(tenantDto.Id);
            tenant.SetDefaultConnectionString(tenantConnectionString);
            await _tenantRepository.UpdateAsync(tenant, autoSave: true);
            await uow.CompleteAsync();
        }

        // 4. Run migrations for the tenant in its own UoW
        await MigrateAndSeedForTenantAsync(tenantDto.Id);

        return tenantDto;
    }

    private async Task MigrateAndSeedForTenantAsync(Guid tenantId)
    {
        using (_currentTenant.Change(tenantId))
        {
            using (var uow = _unitOfWorkManager.Begin(requiresNew: true, isTransactional: false))
            {
                foreach (var migrator in _dbSchemaMigrators)
                {
                    await migrator.MigrateAsync();
                }

                await uow.CompleteAsync();
            }

            // seed data here if needed, again with ICurrentTenant.Change in effect
        }
    }
}

Then disable/remove the default MyAppTenantDatabaseMigrationHandler (either don’t register it or replace it with an empty implementation), because you’re now explicitly handling migrations after connection string setup.

This avoids the race you are seeing: when migrations run, the tenant row (with connection string) is already committed and visible.


Option B – Publish a custom event after setting the connection string

If you strongly prefer an event‑driven approach:

  1. Keep your local handler as is but end the UoW after you update the tenant (or explicitly create a requiresNew: true UoW when updating the tenant and complete it).
  2. After the tenant connection string is successfully committed, publish a custom event (e.g. TenantDatabaseProvisionedEto).
  3. Subscribe to TenantDatabaseProvisionedEto with a handler that:
    • uses _currentTenant.Change(tenantId)
    • starts its own requiresNew: true UoW
    • executes the migrators.

Pseudocode in the provisioner:

// still inside the original tenant-created flow, but now we commit the connection string
using (var uow = _unitOfWorkManager.Begin(requiresNew: true))
{
    var tenant = await _tenantRepository.GetAsync(tenantId);
    tenant.SetDefaultConnectionString(tenantConnectionString);
    await _tenantRepository.UpdateAsync(tenant, autoSave: true);
    await uow.CompleteAsync();
}

// After commit, publish custom event
await _distributedEventBus.PublishAsync(
    new TenantDatabaseProvisionedEto { TenantId = tenantId });

Handler:

public class TenantDatabaseProvisionedHandler 
    : IDistributedEventHandler<TenantDatabaseProvisionedEto>, ITransientDependency
{
    private readonly ICurrentTenant _currentTenant;
    private readonly IUnitOfWorkManager _unitOfWorkManager;
    private readonly IEnumerable<IMyAppDbSchemaMigrator> _dbSchemaMigrators;

    public async Task HandleEventAsync(TenantDatabaseProvisionedEto eventData)
    {
        using (_currentTenant.Change(eventData.TenantId))
        using (var uow = _unitOfWorkManager.Begin(requiresNew: true, isTransactional: false))
        {
            foreach (var migrator in _dbSchemaMigrators)
            {
                await migrator.MigrateAsync();
            }

            await uow.CompleteAsync();
        }
    }
}

Because the connection string is already committed when this event is handled, _tenantStore.FindAsync / the connection resolver will see it correctly.


  1. Sources
  • Changing current tenant context for migrations/seeding:
    https://abp.io/docs/latest/framework/architecture/multi-tenancy#change-the-current-tenant
  • Example of handling tenant‑specific migrations & seeding via a custom TenantDatabaseMigrationHandler:
    https://abp.io/support/questions/8896

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.

  1. You can get the LeptonX 4.2.1 source code only from your ABP/LeptonX license area

    • The LeptonX theme source is not published as a public GitHub repo; it’s downloaded according to your license.
    • Follow these steps:
      1. Sign in to your account on abp.io with the user that owns the LeptonX (or ABP Commercial) license.
      2. Go to your License / Products section.
      3. Find LeptonX Theme (or the ABP Commercial package that includes it).
      4. In the LeptonX page, there will be a “Source Code” (or similarly named) download for the MVC UI and the specific version (e.g. 4.2.x).
      5. Download the MVC source package that matches your ABP/LeptonX version (4.2.1).

    The official documentation describes this in the “Source-Code” section for LeptonX MVC UI.

  2. How this helps with customizing the side menu layout

    • Once you download and extract the LeptonX MVC source package:
      • You will see the theme project (typically a Razor Class Library) that contains:
        • Layout views (e.g. _Layout.cshtml, side menu partials, etc.).
        • The partials and tag helpers used to render the side menu and modules.
      • You can inspect the existing side menu layout partial to see:
        • How the menu is built from IMenuManager / ApplicationMenu.
        • How sections/partials are injected (where to plug your custom partial module).

    A common approach is:

    • Copy the relevant LeptonX layout/partial into your own MVC project (keeping folder structure under Views/Shared or Themes/LeptonX/… compatible with ABP’s view lookup rules).
    • Adjust it to render your custom partial where you need it in the side menu.
    • Ensure your MVC project uses your overridden layout/partial (ABP will pick your view first if the path & name match the theme’s one).

Sources

  • LeptonX MVC UI docs (see “Source-Code” section):
    https://abp.io/docs/latest/ui-themes/lepton-x/mvc

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.

  1. Solution
  • There is currently no official MCP (Model Context Protocol) / tool-calling server that exposes ABP documentation for external or local AI agents.

  • ABP’s own AI-related features today are:

    • AI Infrastructure (workspaces): a unified way to use Microsoft.Extensions.AI (IChatClient) and Semantic Kernel (Kernel) inside your ABP apps.
    • AI Management Module (Pro): a UI + API module to configure AI workspaces dynamically, switch providers (OpenAI, Azure OpenAI, Ollama, etc.), and use prebuilt chat widgets.
  • In the AI Management Module roadmap, “MCP Support” is explicitly listed as a future goal, not something available now. That means:

    • There is no ready-made ABP MCP server for ABP docs at the moment.
    • MCP-based integration with ABP is planned but not yet released.
  • If you want a local AI agent to “query ABP docs” today, you have to build it yourself by:

    1. Downloading or mirroring ABP docs you need.
    2. Indexing them with your own RAG/embedding pipeline.
    3. Exposing that via your own MCP server/tool schema.
    4. (Optional, inside an ABP app) using the AI Infrastructure / AI Management module to talk to your LLM and integrate the results into your UI or workflows.

There is no officially supported, public ABP-hosted MCP endpoint that ships ABP documentation as a tool.

  1. Sources
  • AI infrastructure overview (workspaces, IChatClient, Semantic Kernel):
    https://abp.io/docs/latest/framework/infrastructure/artificial-intelligence
  • AI Management Module (features, roadmap with “MCP Support” listed under Future Goals):
    https://abp.io/docs/latest/modules/ai-management

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.

  1. Check the new 2FA behavior settings (v10 change)

In ABP 10.0, the Identity 2FA behavior directly controls whether the user‑side 2FA UI (including the “Two Factor” tab on the account/manage page) is shown. If 2FA is Forced or Disabled, or users are not allowed to change it, the 2FA options are hidden everywhere.

Do this for both Host and the relevant tenant:

a) Host‑level SettingManagement

  1. Login as a host admin.

  2. Navigate to:

    https://<host>/SettingManagement

  3. Go to the Account tab.

  4. Find the Two factor authentication section and make sure:

    • Behavior is set to Optional
      (NOT “Forced” and NOT “Disabled”)
    • Allow users to change their Two Factor is checked.

    Per docs, if you set behavior to Forced or Disabled,
    2FA UI elements are hidden from users:

    “if we set the two-factor authentication behavior to force or disable, this feature will be hidden from users wherever the 2FA setting is shown.”

  5. Save the settings.

b) Tenant‑level features / settings

The same behavior can also be overridden per tenant:

  1. As host admin, go to:

    https://<host>/Saas/Host/Tenants

  2. For the tenant you’re testing:

    • Click Actions → Features.
    • In the feature modal, ensure the 2FA behavior for that tenant is also Optional (or at least not forcing it off/on in a way that hides the UI).
  3. Then, login as a tenant admin and verify under:

    https://<tenant-host-or-path>/SettingManagementAccount tab

    that tenant‑side Two factor authentication is also Optional and Allow users to change their Two Factor is enabled.

  4. Verify user‑side prerequisites

Even when the tab is present, a user can only use 2FA if:

  • 2FA is enabled for the user, and
  • they have at least one confirmed provider (confirmed email / confirmed phone / authenticator app).

But for your symptom (“the two factor options on the user account page” not visible at all), the usual cause after upgrade is the host/tenant behavior being set to Forced/Disabled, which hides the tab.

  1. Why this appeared after 8.0 → 10.0

The underlying account page is still MVC/Account.Pro (so your WASM → Web App change is not the root cause). ABP 10 tightened the visibility logic so that:

  • When 2FA setting is Forced or Disabled, the user‑level 2FA UI (tabs/sections on /Account/Manage) is hidden.
  • Only in Optional mode, and when “Allow users to change their Two Factor” is true, the user gets the 2FA tab.

So the fix is to set behavior back to Optional and allow users to change it, at host and tenant level.


Sources

  • Two Factor Authentication behavior and visibility rules (ABP 10.0):
    https://abp.io/docs/10.0/modules/identity/two-factor-authentication
  • Manage Profile Tabs (TwoFactor tab is one of the standard tabs):
    https://github.com/abpframework/abp/blob/HEAD/docs/en/framework/ui/angular/manage-profile-page-tabs.md

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.

  1. Solution

You do not need to create your own API just to use ExtraProperties on AbpUser (IdentityUser).

ABP already wires ExtraProperties into the existing Identity user endpoints:

  • POST /api/identity/usersIdentityUserCreateDto
  • PUT /api/identity/users/{id}IdentityUserUpdateDto

Both DTOs inherit from ExtensibleObject, and in IdentityUserAppService they are mapped to the entity like this:

// Create
input.MapExtraPropertiesTo(user);

// Update
input.MapExtraPropertiesTo(user);

So the flow is:

  1. Define the extra properties on IdentityUser (required!)

    In your *.Domain.Shared project, usually in YourProjectModuleExtensionConfigurator.ConfigureExtraProperties():

using System.ComponentModel.DataAnnotations;
   using Volo.Abp.ObjectExtending;
   using Volo.Abp.Identity;

   public static class YourProjectModuleExtensionConfigurator
   {
       private static readonly OneTimeRunner OneTimeRunner = new OneTimeRunner();

       public static void Configure()
       {
           ConfigureExtraProperties();
       }

       private static void ConfigureExtraProperties()
       {
           OneTimeRunner.Run(() =>
           {
               ObjectExtensionManager.Instance
                   .Modules()
                   .ConfigureIdentity(identity =>
                   {
                       identity.ConfigureUser(user =>
                       {
                           user.AddOrUpdateProperty<string>(
                               "MyCustomCode",
                               options =>
                               {
                                   options.Attributes.Add(new RequiredAttribute());
                                   options.Attributes.Add(new StringLengthAttribute(64));
                               });
                       });
                   });
           });
       }
   }

Optionally, if you want a column instead of JSON-only storage, map it in your *.EntityFrameworkCore project (YourProjectEfCoreEntityExtensionMappings):

using Volo.Abp.Identity;
   using Volo.Abp.ObjectExtending;

   OneTimeRunner.Run(() =>
   {
       ObjectExtensionManager.Instance
           .MapEfCoreProperty<IdentityUser, string>(
               "MyCustomCode",
               (entityBuilder, propertyBuilder) =>
               {
                   propertyBuilder.HasMaxLength(64);
               });
   });

Then add a migration/update the DB.

  1. Send values via existing Identity endpoints

    Once the module entity extension is configured, you can create/update users by sending extraProperties in the JSON body:

    Create:

POST /api/identity/users
   {
     "userName": "jdoe",
     "email": "jdoe@test.com",
     "password": "1q2w3E*",
     "name": "John",
     "surname": "Doe",
     "isActive": true,
     "lockoutEnabled": false,
     "roleNames": [],
     "extraProperties": {
       "MyCustomCode": "ABC-123"
     }
   }

Update:

PUT /api/identity/users/{id}
   {
     "userName": "jdoe",
     "email": "jdoe@test.com",
     "name": "John",
     "surname": "Doe",
     "isActive": true,
     "lockoutEnabled": false,
     "roleNames": [],
     "concurrencyStamp": "....",
     "extraProperties": {
       "MyCustomCode": "XYZ-999"
     }
   }

Even if Swagger shows extraProperties on the output DTO as read-only, the input DTOs still accept it and MapExtraPropertiesTo applies those values to the IdentityUser.ExtraProperties dictionary.

  1. Using ExtraProperties in code (optional)

    From domain/application code you can also work directly with the entity’s extra properties:

using Volo.Abp.Identity;
   using Volo.Abp.ObjectExtending;

   public class MyUserService : ITransientDependency
   {
       private readonly IRepository<IdentityUser, Guid> _userRepository;

       public MyUserService(IRepository<IdentityUser, Guid> userRepository)
       {
           _userRepository = userRepository;
       }

       public async Task SetMyCustomCodeAsync(Guid userId, string code)
       {
           var user = await _userRepository.GetAsync(userId);
           user.SetProperty("MyCustomCode", code);
           await _userRepository.UpdateAsync(user);
       }

       public async Task<string> GetMyCustomCodeAsync(Guid userId)
       {
           var user = await _userRepository.GetAsync(userId);
           return user.GetProperty<string>("MyCustomCode");
       }
   }

Answer to your concrete question

does it mean at the moment there is no way to add/update these ExtraProperties on the AbpUser and I have to define my own Api to add/update values for this?

No.
Once you configure module entity extensions for IdentityUser, you can add/update ExtraProperties using the existing /api/identity/users POST/PUT endpoints. A custom API is only needed if you want totally customized behavior, not for basic ExtraProperties support.


  1. Sources
  • Object extensions for entities and DTOs
    https://abp.io/docs/latest/framework/fundamentals/object-extensions
  • Extending existing module entities (e.g., IdentityUser)
    https://abp.io/docs/latest/framework/architecture/modularity/extending/customizing-application-modules-extending-entities

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.

  1. Solution

You are correct for projects created with the latest ABP Studio: the generated Angular UI is now standalone-based (no AppModule, no feature NgModules), while the BookStore Angular tutorial you linked is still NgModule-based. So the tutorial is conceptually valid, but structurally outdated for the new template.

To use the tutorial with your standalone Angular app, treat all “module” steps as feature + route steps, and translate them as follows:


1. Creating the “Books” feature

Tutorial says (example):

yarn ng generate module book --module app --routing --route books

For a standalone Angular ABP app, instead:

yarn ng generate component books/book-list --standalone --flat=false

This will create something like:

// src/app/books/book-list/book-list.component.ts
@Component({
  standalone: true,
  selector: 'app-book-list',
  templateUrl: './book-list.component.html',
  styleUrls: ['./book-list.component.scss'],
  imports: [
    CommonModule,
    // other Angular/ABP imports you need
  ],
})
export class BookListComponent { /* ... */ }

You do not create a BookModule or BookRoutingModule.


2. Registering the route

In the tutorial, you would add the route into a feature routing module (e.g., book-routing.module.ts) and import that into AppModule.

In the standalone app, you instead add a route to your top-level routes configuration, typically app.routes.ts (or similar):

// src/app/app.routes.ts (or wherever your main routes are defined)
import { Routes } from '@angular/router';
import { BookListComponent } from './books/book-list/book-list.component';

export const routes: Routes = [
  // existing routes...

  {
    path: 'books',
    component: BookListComponent,
  },
];

Your main.ts should already use bootstrapApplication with provideRouter(routes), so you only need to modify the routes array.


3. Where the tutorial says “import the module”

Anywhere the tutorial says things like:

Add BookModule to the imports of AppModule.

In the standalone structure, the equivalent is:

  • Ensure the route is configured (as above).
  • Ensure any shared functionality that used to be in SharedModule is:
    • either imported directly into the standalone component via its imports: [...] array, or
    • provided globally in your bootstrapApplication call (e.g. via importProvidersFrom(...) in main.ts).

Example (if you have a shared ABP Angular module):

// main.ts
bootstrapApplication(AppComponent, {
  providers: [
    provideRouter(routes),
    importProvidersFrom(AbpCoreModule, ThemeSharedModule /* etc. */),
  ],
});

Then your feature components can use those services/directives without NgModules.


4. Backend and ABP-specific parts

All backend-related parts of the BookStore tutorial (EF Core, application services, DTOs, permissions, etc.) are still correct and can be followed exactly:

  • Create the Book, Author entities, DbContext mappings.
  • Create application services and DTOs.
  • Expose them via controllers.
  • Consume them in Angular via the generated proxies (ng generate proxy or existing @proxy usage).

On the Angular side, only adapt:

  • “Create module” → “Create standalone component(s) in a feature folder”.
  • “Add module to imports” → “Add route + import shared providers at bootstrap level or in component imports”.

  1. Sources
  • Verified tutorial (structure still module-based as you observed):
    https://abp.io/docs/latest/tutorials/book-store/part-02?UI=NG&DB=EF

  • Angular UI overview (general ABP Angular info; not yet reflecting every standalone detail):
    https://abp.io/docs/latest/framework/ui/angular/overview


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.

  1. Solution
  • The Angular Book Store tutorial you referenced is aligned with the current ABP Angular UI template (ABP v10), which is still NgModule-based, even though Angular itself now recommends standalone APIs for new apps.
  • That yarn ng generate module book --module app --routing --route books command and the module-based structure in the tutorial are therefore intentional and still correct for projects generated by ABP today.

So, for an ABP v10 Angular project:

  • It is expected that:
    • You have AppModule, SharedModule, feature modules (like BookModule, AuthorModule, etc.).
    • You add new features using ng generate module ... and register routes in the generated routing modules.
  • The tutorial matches this structure and you should follow it as-is if you are using the standard ABP Angular template.

If you specifically want to adopt standalone components in your ABP Angular application:

  • You can manually refactor the generated Angular app to standalone style, but:
    • This is not yet the default or documented pattern in ABP’s official Angular tutorials.
    • You’ll need to adapt the tutorial steps (routing, declarations, imports) yourself to match a standalone architecture.

The docs and samples currently still reflect NgModule usage and are not yet updated to a fully standalone-first style; they are focused on matching the generated template rather than the latest Angular style guidance.

  1. Sources
  • Verified Book Store Angular tutorial:
    https://abp.io/docs/latest/tutorials/book-store/part-02?UI=NG&DB=EF
  • ABP Angular UI overview (shows current module-based structure):
    https://abp.io/docs/latest/ui/angular/overview

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.

Showing 1 to 10 of 777 entries
Learn More, Pay Less
33% OFF
All Trainings!
Get Your Deal
Mastering ABP Framework Book
The Official Guide
Mastering
ABP Framework
Learn More
Mastering ABP Framework Book
Made with ❤️ on ABP v10.2.0-preview. Updated on January 06, 2026, 13:47
1
ABP Assistant
🔐 You need to be logged in to use the chatbot. Please log in first.