Id comes from Entity<TPrimaryKey> / AggregateRoot<TPrimaryKey> and typically has a protected set – that’s fine for reading (Entity -> DTO), but:
DTO -> Entity (especially updates) Mapperly is generating code that creates a new entity instance and does not (and should not) set Id.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:
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.
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.
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.
IdEven if you expose Id with a public setter, you generally should not map it:
Id is generated by EF Core or your own code (IGuidGenerator, sequences, etc.).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.
When you:
GetAsync / FirstOrDefaultAsync inside the same UoW,ObjectMapper.Map(input, existingEntity),EF Core:
Creating a new MyEntity during mapping always risks:
That’s exactly what your workaround with ParameterlessConstructor = false caused.
Id protected and do not try to map it with Mapperly.Map(source, destination) pattern (no new entity),Id in the mapping.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.
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.
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).
app.routes.ts)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' },
},
];
app.routes.tsIn 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,
},
];
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.
app-routing.module.ts)If your Angular app still has AppModule and app-routing.module.ts:
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 {}
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),
},
];
/rag-engine, same as in section A.RagEngineComponent builds without errors.APP_ROUTES or routes array has an entry for path 'rag-engine'.path: '/rag-engine'.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.
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.
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:
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”).
public class MySessionData
{
public string SomeValue { get; set; }
public int SomeNumber { get; set; }
// add what you need
}
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);
}
}
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.
Call MySessionService from controllers, Razor pages, app services, etc. to read/write session data anywhere in the app.
If a user can have multiple concurrent logins and you need truly per-session data, you can:
sessionId parameter above).Since you’ve disabled Dynamic Claims, you probably shouldn’t mutate claims after login; instead:
Then inject IHttpContextAccessor or your own helper to get the session Id and use it in GetCacheKey.
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,This approach is:
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.
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:
Instead of relying on TenantCreatedEto for this scenario, override the tenant creation application service so that:
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.
If you strongly prefer an event‑driven approach:
requiresNew: true UoW when updating the tenant and complete it).TenantDatabaseProvisionedEto).TenantDatabaseProvisionedEto with a handler that:
_currentTenant.Change(tenantId)requiresNew: true UoWPseudocode 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.
TenantDatabaseMigrationHandler: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.
You can get the LeptonX 4.2.1 source code only from your ABP/LeptonX license area
The official documentation describes this in the “Source-Code” section for LeptonX MVC UI.
How this helps with customizing the side menu layout
_Layout.cshtml, side menu partials, etc.).IMenuManager / ApplicationMenu.A common approach is:
Views/Shared or Themes/LeptonX/… compatible with ABP’s view lookup rules).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.
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:
Microsoft.Extensions.AI (IChatClient) and Semantic Kernel (Kernel) inside your ABP apps.In the AI Management Module roadmap, “MCP Support” is explicitly listed as a future goal, not something available now. That means:
If you want a local AI agent to “query ABP docs” today, you have to build it yourself by:
There is no officially supported, public ABP-hosted MCP endpoint that ships ABP documentation as a tool.
IChatClient, Semantic Kernel):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.
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:
Login as a host admin.
Navigate to:
https://<host>/SettingManagement
Go to the Account tab.
Find the Two factor authentication section and make sure:
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.”
Save the settings.
The same behavior can also be overridden per tenant:
As host admin, go to:
https://<host>/Saas/Host/Tenants
For the tenant you’re testing:
Then, login as a tenant admin and verify under:
https://<tenant-host-or-path>/SettingManagement → Account tab
that tenant‑side Two factor authentication is also Optional and Allow users to change their Two Factor is enabled.
Verify user‑side prerequisites
Even when the tab is present, a user can only use 2FA if:
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.
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:
/Account/Manage) is hidden.So the fix is to set behavior back to Optional and allow users to change it, at host and tenant level.
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.
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/users → IdentityUserCreateDtoPUT /api/identity/users/{id} → IdentityUserUpdateDtoBoth 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:
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.
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.
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.
IdentityUser)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.
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:
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.
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.
Anywhere the tutorial says things like:
Add
BookModuleto theimportsofAppModule.
In the standalone structure, the equivalent is:
SharedModule is:
imports: [...] array, orbootstrapApplication 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.
All backend-related parts of the BookStore tutorial (EF Core, application services, DTOs, permissions, etc.) are still correct and can be followed exactly:
ng generate proxy or existing @proxy usage).On the Angular side, only adapt:
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.
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:
AppModule, SharedModule, feature modules (like BookModule, AuthorModule, etc.).ng generate module ... and register routes in the generated routing modules.If you specifically want to adopt standalone components in your ABP Angular application:
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.
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.