CheckTokenExpiration() and IntrospectAccessToken() are just two different ABP helpers that plug into the cookie authentication pipeline:
IntrospectAccessToken() (obsolete)
ControlCenter (or some other client/resource).APP‑2 introspects it with its own ClientId, OpenIddict sees that this ClientId is not in aud/azp and rejects the request:
The introspection request was rejected because the access token was issued to a different client or for another resource server.active: false even when the token itself is valid.CheckTokenExpiration() (new, recommended)
Because you have many apps (Portal, APP‑1/2/3) with SSO and different OpenIddict clients, IntrospectAccessToken() will always be fragile unless all access tokens are issued with all your frontend ClientIds as audiences.
The workaround suggested in the support thread is:
options.IntrospectAccessToken() from all apps.CookieAuthenticationOptionsExtensions.cshttps://github.com/abpframework/abp/blob/dev/framework/src/Volo.Abp.AspNetCore/Microsoft/Extensions/DependencyInjection/CookieAuthenticationOptionsExtensions.csCheckTokenExpiration/validation logic, change the ClientId used for introspection:
ClientId, take the ClientId (or audience) from the access token being validated, or use a single technical client that is always present in aud.ValidateIntrospectionRequest handler.If you do this:
IntrospectAccessToken (user is redirected to login when token is invalid/expired).Regarding your last comment:
Using
CheckTokenExpirationlets users access internal URLs without logging in, butIntrospectAccessTokendoes not.
That difference comes from the exact validation logic inside the ABP extension. Once you copy the extension code and adapt the introspection part (ClientId/audience logic) you can:
ApplicationName, Redis Protection-Keys shared) architecture.CookieAuthenticationOptionsExtensionsThis 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.
Your expectation is reasonable, but with ABP 9.3.x what you want is not supported by the Angular proxy generator.
More precisely:
IRemoteStreamContent itself is supported by:
However, the Angular proxy generator only switches to FormData when:
IRemoteStreamContent / RemoteStreamContent, orIEnumerable<IRemoteStreamContent>) is a top-level parameter.It does not inspect complex DTO graphs like:
public class TestUploadInput
{
public Guid RecordId { get; set; }
public TestUploadInputDocument[] Documents { get; set; } = [];
}
public class TestUploadInputDocument
{
public Guid DocumentId { get; set; }
public IEnumerable<IRemoteStreamContent> Files { get; set; } = [];
}
public void Upload([FromForm] TestUploadInput input) ...
and therefore always generates JSON (or query params) for TestUploadInput, not FormData.
There is no configuration switch in 9.3 to tell the Angular proxy generator “for this DTO, use FormData”.
So what you see:
uploadByInput = (input: TestUploadInput, ...) =>
this.restService.request<any, void>({
method: 'POST',
url: '/api/app/ownership-change-flow/upload',
body: input, // or params: { ... }
}, ...);
is exactly the current limitation, not a misconfiguration on your side.
Given that, the only fully working approaches in 9.3.6 are:
You can continue to use your DTO structure exactly as-is on the backend, and just bypass the generated proxy for this method only.
public class OwnershipChangeFlowAppService : ApplicationService
{
public void Upload([FromForm] TestUploadInput input)
{
// input.RecordId
// input.Documents[i].DocumentId
// input.Documents[i].Files (IEnumerable<IRemoteStreamContent>)
}
}
(And ensure you configure MVC so this DTO isn’t JSON-bound, e.g. with FormBodyBindingIgnoredTypes if needed, as described in the docs.)
RestService that sends the FormData the way the backend expects:import { Injectable } from '@angular/core';
import { RestService } from '@abp/ng.core';
@Injectable({ providedIn: 'root' })
export class OwnershipChangeFlowUploadClient {
constructor(private restService: RestService) {}
upload(input: {
recordId: string;
documents: { documentId: string; files: File[] }[];
}) {
const form = new FormData();
form.append('recordId', input.recordId);
input.documents.forEach((doc, docIndex) => {
form.append(`documents[${docIndex}].documentId`, doc.documentId);
doc.files.forEach(file => {
// Name must match DTO structure that ASP.NET Core can bind to.
form.append(`documents[${docIndex}].files`, file);
});
});
return this.restService.request<any, void>(
{
method: 'POST',
url: '/api/app/ownership-change-flow/upload',
body: form,
},
{ apiName: 'Default' },
);
}
}
ABP support has already confirmed in a previous ticket that for such file upload scenarios you should not use the generated Angular proxy when it does not produce FormData, and instead send FormData manually with RestService/HttpClient.
FormDataIf you want to rely strictly on the generated Angular proxy, you must change your method into a shape the generator understands, e.g. flattening out the file parameters so they are top-level IRemoteStreamContent / IEnumerable<IRemoteStreamContent> and using a simpler DTO for the rest of the data.
For example (conceptually):
public class TestUploadFlatInput
{
public Guid RecordId { get; set; }
public Guid[] DocumentIds { get; set; } = [];
}
public Task UploadAsync(
[FromForm] TestUploadFlatInput input,
IEnumerable<IRemoteStreamContent> files)
{
// correlate input.DocumentIds[i] with files.ElementAt(i) yourself
}
For such signatures the Angular proxy generator can generate a method that takes FormData as body. But with nested IRemoteStreamContent inside DTOs/child DTOs, it cannot.
I believe it should be possible to generate a function which accepts
FormDatainstead…
From a design perspective: yes, it could be implemented in the generator.
In ABP 9.3.6 however, this is not implemented, and there is no extension point to change the Angular proxy generator’s behavior for this specific method or DTO.
So currently:
IRemoteStreamContent is valid and supported on the server side.FormData client for that method (A), orIf this is blocking and you want first-class generator support for nested DTO uploads, the next step would be to open an issue on ABP’s GitHub repo or via the ABP support portal so the team can treat it as a feature/bug for a future version.
Working with Streams (IRemoteStreamContent, DTO + FormBodyBindingIgnoredTypes):
https://abp.io/docs/9.3/framework/architecture/domain-driven-design/application-services#working-with-streams
ABP support ticket where the team recommends not using the generated Angular proxy for file uploads and instead sending FormData manually:
https://abp.io/support/questions/5731
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 9.3.x the Angular proxy generator only knows how to switch to FormData when the service method parameter itself is IRemoteStreamContent / RemoteStreamContent (or a similar direct stream parameter).
It does not detect IRemoteStreamContent (or collections like IEnumerable<IRemoteStreamContent>) inside a DTO, therefore it generates:
body: request // JSON body
instead of a FormData upload. That’s a limitation of the current proxy generator – your observation is correct.
Because of this, there are only two practical options in 9.3.6:
If you want to keep using generated proxies, the easiest way is to avoid wrapping streams in a DTO for this method.
Backend app service:
public class MyUploadAppService : ApplicationService
{
// Single file + extra data
public async Task UploadAsync(
IRemoteStreamContent file,
string someExtraField,
int anotherField)
{
using var stream = file.GetStream();
// handle file + fields …
}
// Multiple files
public async Task UploadManyAsync(
IEnumerable<IRemoteStreamContent> files,
string someExtraField)
{
foreach (var f in files)
{
using var stream = f.GetStream();
// handle each file…
}
}
}
Then run:
abp generate-proxy -t ng
The Angular proxy for UploadAsync will look like:
upload = (file: FormData, someExtraField: string, anotherField: number, config?: Partial<Rest.Config>) =>
this.restService.request<any, void>({
method: 'POST',
url: '/api/app/my-upload/upload',
body: file,
params: { someExtraField, anotherField },
},
{ apiName: this.apiName, ...config });
And you use it like:
const form = new FormData();
form.append('file', inputFile); // `file` must match param name
this.myUploadService.upload(form, 'value', 123).subscribe();
For multiple files, the generated method will take FormData as well; you append multiple file entries (e.g. form.append('files', file) for each).
This keeps everything within the auto API + proxy system and avoids the DTO-with-stream case the generator cannot handle.
If you really need a DTO like:
public class MyUploadDto
{
public string SomeField { get; set; }
public IEnumerable<IRemoteStreamContent> Files { get; set; }
}
you must:
Configure<AbpAspNetCoreMvcOptions>(options =>
{
options.FormBodyBindingIgnoredTypes.Add(typeof(MyUploadDto));
});
and bind IRemoteStreamContent from form fields (ABP handles this when you use form-data correctly).
FormData manually:import { Injectable } from '@angular/core';
import { RestService } from '@abp/ng.core';
@Injectable({ providedIn: 'root' })
export class MyUploadClient {
constructor(private restService: RestService) {}
upload(dto: { someField: string; files: File[] }) {
const formData = new FormData();
formData.append('someField', dto.someField);
dto.files.forEach(file => {
formData.append('files', file); // 'files' matches DTO property name
});
return this.restService.request<any, void>(
{
method: 'POST',
url: '/api/app/my-upload/upload',
body: formData, // FormData, not JSON
},
{ apiName: 'Default' },
);
}
}
ABP support has explicitly recommended not using the generated proxy for file upload scenarios when it doesn’t produce FormData, and using a custom FormData+RestService/HttpClient call instead.
In summary:
IRemoteStreamContent) is a current limitation of the Angular proxy generator in 9.3.x.IRemoteStreamContent parameters (Option A, fully supported by generator), orFormData request (Option B).IRemoteStreamContent (shows generated FormData proxies for direct parameters):FormData manually: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.
options.IntrospectAccessToken() is an ABP helper extension for cookie authentication:
context.Services.AddAuthentication(options =>
{
options.DefaultScheme = "Cookies";
options.DefaultChallengeScheme = "oidc";
})
.AddCookie("Cookies", options =>
{
options.ExpireTimeSpan = TimeSpan.FromDays(365);
options.IntrospectAccessToken(); // = old helper
})
.AddAbpOpenIdConnect("oidc", options =>
{
// ...
});
From the ABP source:
/// <summary>
/// Check the access_token is expired or inactive.
/// </summary>
[Obsolete("Use CheckTokenExpiration method instead.")]
public static CookieAuthenticationOptions IntrospectAccessToken(
this CookieAuthenticationOptions options,
string oidcAuthenticationScheme = "oidc")
{
return options.CheckTokenExpiration(oidcAuthenticationScheme, null, TimeSpan.FromMinutes(1));
}
So:
CookieAuthenticationOptions.Events.OnValidatePrincipal.AddAbpOpenIdConnect("oidc", …)).active: false / audience not listedYour log:
Potentially sensitive application claims were excluded from the introspection response as the client 'EAP' was not explicitly listed as an audience.
"active": false
This happens when the extension calls the introspection endpoint of OpenIddict and:
EAP), so OpenIddict:
active: false.Common reasons:
Result: every validation attempt marks the token as not active, so the cookie is invalidated.
IntrospectAccessToken is obsolete. Use:
.AddCookie("Cookies", options =>
{
options.ExpireTimeSpan = TimeSpan.FromDays(365);
options.CheckTokenExpiration(); // new recommended helper
});
Use CheckTokenExpiration / IntrospectAccessToken when:
oidc scheme), andIf:
then you can simply remove IntrospectAccessToken() / CheckTokenExpiration() and rely on normal cookie expiration.
If you want to keep token introspection:
OpenIddictDataSeedContributor, ensure the client for this app:
active: true while the token is valid and the cookie will not be cleared.In summary:
IntrospectAccessToken is used to periodically verify the OIDC access token behind the cookie and log the user out when that token is no longer valid.CheckTokenExpiration().active: false error is due to OpenIddict audience/scope configuration, not ABP itself.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 caused by a stale / wrong Suite configuration cached for the solution name Sekmen.Goo. ABP Suite keeps these configurations in its own appsettings.json. You can reset it as follows.
Close ABP Suite / ABP Studio.
Open this file on your machine:
Windows
%UserProfile%\.abp\suite\appsettings.json
(Paste that path into File Explorer or open with a text editor.)
In appsettings.json, locate the Solutions (or similar) array and find the entry whose "Name" or "ProjectNameWithCompanyName" is "Sekmen.Goo" and/or whose "Path" points to your Sekmen.Goo.sln.
Delete only that object from the array, keeping the rest of the JSON valid (commas, brackets, etc.).
Save the file.
Re‑open ABP Suite / ABP Studio, go to the Suite home page, and Add an existing solution again by selecting Sekmen.Goo.sln.
This forces Suite to re‑discover the project structure for Sekmen.Goo, so it no longer looks for Sekmen.Goo.HttpApi.Host when you have a non‑tiered MVC UI.
If you don’t mind re‑adding all solutions to Suite:
Close ABP Suite / ABP Studio.
Delete the whole file:
Windows
%UserProfile%\.abp\suite\appsettings.json
This completely clears any old or corrupted solution mappings, including the one tied to the Sekmen.Goo name.
ABP Suite settings file location and reset guidance
https://abp.io/support/questions/721
Suite configuration overview
https://abp.io/docs/latest/suite/configuration
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.
_Sidebar.cshtml and move controls to the sidebar footerFor LeptonX MVC, the sidebar is a partial under the Application layout:
Volo.Abp.AspNetCore.Mvc.UI.Theme.LeptonX):/Themes/LeptonX/Layouts/Application/_Sidebar.cshtml
ABP will automatically prefer your file over the one embedded in the theme, as described in the LeptonX MVC docs.
Get the original _Sidebar.cshtml as a starting point
You don’t need to decompile anything; the .cshtml files are shipped inside the NuGet package.
Locate the package folder on disk, something like:
C:\Users\<your-user>\.nuget\packages\volo.abp.aspnetcore.mvc.ui.theme.leptonx\<version>\Themes\LeptonX\Layouts\Application\_Sidebar.cshtml~/.nuget/packages/volo.abp.aspnetcore.mvc.ui.theme.leptonx/<version>/Themes/LeptonX/Layouts/Application/_Sidebar.cshtmlCopy the entire content of that _Sidebar.cshtml into your project file:
Themes/LeptonX/Layouts/Application/_Sidebar.cshtml.
Make sure you have a _ViewImports.cshtml in Themes/LeptonX/Layouts/Application (or above) with at least:
@using System.Globalization
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@addTagHelper *, Volo.Abp.AspNetCore.Mvc.UI.Bootstrap
@addTagHelper *, Volo.Abp.AspNetCore.Mvc.UI.Bundling
(Same pattern as in the LeptonX Account layout override example.)
Add a footer section with your buttons
In your copied _Sidebar.cshtml, find the outermost sidebar container (it will be obvious once you open the original).
At the bottom of that container, add something like:
<div class="lpx-sidebar-footer mt-3 pt-3 border-top">
<div class="d-flex justify-content-between align-items-center">
<!-- Light/Dark toggle button -->
<button type="button"
class="btn btn-sm btn-outline-secondary"
onclick="abpLeptonxToggleAppearance()">
<i class="bi bi-moon-stars"></i>
Toggle theme
</button>
<!-- Logout link/button (adjust route if you use a custom account module) -->
<a href="~/Account/Logout" class="btn btn-sm btn-outline-danger">
<i class="bi bi-box-arrow-right"></i>
Logout
</a>
</div>
</div>
You can then completely remove the toolbar by:
_Toolbar.cshtml use), or_Toolbar.cshtml with an empty partial in the same folder (Themes/LeptonX/Layouts/Application/_Toolbar.cshtml) if you haven’t already.The general override mechanism and the partial names _Sidebar.cshtml / _Toolbar.cshtml are documented here.
The pattern Volosoft recommends (and uses in support) is:
window.initLeptonXleptonx.globalConfig.defaultSettingsleptonx.init.run() againThis is shown in the (Blazor) support answer you linked; the same JavaScript object (leptonx.globalConfig) is used for MVC as well.
Step 1 – Add a global script
Create wwwroot/global.js in your MVC app:
// Hook initLeptonX once at startup
$(function () {
if (!window.initLeptonX || !window.leptonx) {
return; // LeptonX scripts not loaded (safety guard)
}
const originalInitLeptonX = window.initLeptonX;
window.initLeptonX = function (layout, defaultStyle) {
// Call original initializer first
originalInitLeptonX(layout, defaultStyle);
// Optionally set your initial defaults (one-time)
leptonx.globalConfig.defaultSettings = {
...leptonx.globalConfig.defaultSettings,
appearance: "dark", // "light" | "dark" | "dim" | "system"
containerWidth: "full" // "full" | "boxed"
};
leptonx.init.run();
};
// Expose a helper you can call from your sidebar button
window.abpLeptonxToggleAppearance = function () {
const current = leptonx.globalConfig.defaultSettings.appearance;
// Simple 2‑state toggle; extend as needed
const next =
current === "dark"
? "light"
: "dark";
leptonx.globalConfig.defaultSettings = {
...leptonx.globalConfig.defaultSettings,
appearance: next
};
// Re-apply LeptonX with new appearance
leptonx.init.run();
};
});
Notice the important difference from what you tried:
leptonx.globalConfig.defaultSettings object using spread syntax, not doing leptonx.globalConfig.defaultSettings.appearance = ....Step 2 – Register the script in LeptonX’s global bundle
In your web module (e.g. YourProjectWebModule.cs), inside ConfigureServices:
Configure<AbpBundlingOptions>(options =>
{
options.ScriptBundles.Configure(
LeptonXThemeBundles.Scripts.Global,
bundle =>
{
bundle.AddFiles("/global.js");
});
});
This ensures global.js is loaded on all LeptonX pages so that:
initLeptonX is wrapped,abpLeptonxToggleAppearance() is globally available for your sidebar button.Now the button you added in _Sidebar.cshtml:
<button type="button"
class="btn btn-sm btn-outline-secondary"
onclick="abpLeptonxToggleAppearance()">
<i class="bi bi-moon-stars"></i>
Toggle theme
</button>
will correctly change the LeptonX appearance between light and dark.
The logout action is provided by the Account module. In the default MVC templates this is exposed under /Account/Logout. Using a plain anchor is usually sufficient:
<a href="~/Account/Logout" class="btn btn-sm btn-outline-danger">
<i class="bi bi-box-arrow-right"></i>
Logout
</a>
If you use a different route or customized Account module, point it to your own logout URL.
Sources
_Sidebar.cshtml, _Toolbar.cshtml):initLeptonX override and leptonx.globalConfig.defaultSettings usage: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 built‑in way in LeptonX to mark a root menu item with children as “non‑collapsible” or “always expanded”. All root items that have children are rendered as collapsible groups by the theme, and there is no documented flag such as disableCollapse / alwaysExpanded / expandedByDefault per menu item.
To achieve the behavior you want (e.g. a “Quick Links” group that is always expanded and has no toggle arrow), you need to customize the sidebar rendering instead of relying on the default LeptonX sidebar component.
A practical approach is:
Replace the sidebar component via replaceable components (Angular / MVC / Blazor)
NavItemsService, MVC/Blazor: IMenuContributor produces the menu and your component/layout renders it).Conceptually (Angular example outline, not tied to a specific key name):
// in your theme customization module
import { NgModule } from '@angular/core';
import { ReplaceableComponentsService } from '@abp/ng.core';
import { eThemeLeptonXComponents } from '@abp/ng.theme.lepton-x';
import { CustomSidebarComponent } from './custom-sidebar.component';
@NgModule({
declarations: [CustomSidebarComponent],
exports: [CustomSidebarComponent],
})
export class ThemeCustomizationModule {
constructor(rc: ReplaceableComponentsService) {
rc.add({
key: eThemeLeptonXComponents.Sidebar, // use the actual sidebar key for your version
component: CustomSidebarComponent,
});
}
}
In CustomSidebarComponent’s template, you render the menu tree manually. For a special “Quick Links” root item you simply don’t add the collapse arrow and collapse behavior, and always show its children:
<!-- Pseudocode for the idea -->
<li class="lpx-menu-item quick-links">
<div class="lpx-menu-title">
Quick Links
<!-- no arrow icon here -->
</div>
<ul class="lpx-menu-children">
<!-- these links are always visible -->
<li *ngFor="let item of quickLinksChildren">
<a [routerLink]="item.route">{{ item.displayName }}</a>
</li>
</ul>
</li>
Other groups can keep the standard LeptonX pattern (arrow + collapsible list).
Alternative: fully custom sidebar (like the AdminLTE example)
If you don’t need to keep the LeptonX sidebar behavior at all, you can do what the AdminLTE sample does: completely replace the layout and render your own sidebar HTML and CSS while still using ABP’s application infrastructure. That gives total freedom over which groups are collapsible vs. always expanded, at the cost of more custom UI work.
Requesting this as a first‑class LeptonX feature
Since this is a theme‑level capability (per‑item “non‑collapsible” flag), the right way to request it as a built‑in, opt‑in feature is to open an issue in the ABP Framework GitHub repo or, for ABP Commercial customers, open a feature request via the support portal. Describe it as:
AlwaysExpanded / DisableCollapse for LeptonX sidebar rendering.This will put it on the product team’s radar similarly to the previous LeptonX accessibility feature request that was added to the backlog.
LeptonX / LeptonX Lite theming and replaceable components (pattern for customizing Angular layout parts):
https://abp.io/docs/latest/themes/lepton-x/angular
https://abp.io/docs/latest/themes/lepton-x-lite/angular
ABP Framework GitHub (for opening theme/feature issues):
https://github.com/abpframework/abp
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.
This is a licensing / account–integration issue between your ABP commercial license and Blazorise’s license API, and it requires access to your organization and license details on abp.io (and sometimes direct coordination with Blazorise).
Such account‑specific and license‑related problems are handled only by the ABP support team via the official contact channel.
Please reach out there and include:
They’ll be able to check your organizations and fix the mapping on the backend or coordinate with Blazorise if needed.
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.
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.