Activities of "AI-Bot"

  1. Solution

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:

    • ASP.NET Core model binding (server side),
    • Auto API controller,
    • C# dynamic HTTP proxies.
  • However, the Angular proxy generator only switches to FormData when:

    • The controller/app-service parameter is directly IRemoteStreamContent / RemoteStreamContent, or
    • A simple collection (e.g. IEnumerable<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:


A. Keep your DTO on the server, use a custom Angular client (recommended for your current design)

You can continue to use your DTO structure exactly as-is on the backend, and just bypass the generated proxy for this method only.

  1. Keep your app service like:
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.)

  1. In Angular, write a small wrapper over 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.


B. Change the backend signature so the generator can use FormData

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


Direct answer to your request

I believe it should be possible to generate a function which accepts FormData instead…

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:

  • Your backend code with nested DTOs + IRemoteStreamContent is valid and supported on the server side.
  • It is also compatible with C# HTTP proxies.
  • The Angular proxy generator does not support this pattern and will continue to produce “unusable” JSON-based methods for it.
  • The only robust solutions today are:
    • Custom Angular FormData client for that method (A), or
    • Adjusted backend method signature (B).

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

  1. Sources
  • 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.

  1. Solution

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:


Option A – Change the backend signature to use direct stream parameters

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.


Option B – Keep your DTO, but bypass the generated proxy for that method

If you really need a DTO like:

public class MyUploadDto
{
    public string SomeField { get; set; }
    public IEnumerable<IRemoteStreamContent> Files { get; set; }
}

you must:

  1. Configure the server-side model binding for that DTO properly:
Configure<AbpAspNetCoreMvcOptions>(options =>
   {
       options.FormBodyBindingIgnoredTypes.Add(typeof(MyUploadDto));
   });

and bind IRemoteStreamContent from form fields (ABP handles this when you use form-data correctly).

  1. Do not use the generated Angular proxy for this method. Instead, write a small Angular service that posts 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:

  • What you see (JSON body for DTO with IRemoteStreamContent) is a current limitation of the Angular proxy generator in 9.3.x.
  • To make upload work:
    • Either change the app service signature to use direct IRemoteStreamContent parameters (Option A, fully supported by generator), or
    • Keep your DTO and bypass the generated proxy with a custom FormData request (Option B).
  1. Sources
  • ABP docs – Application Services, Working with Streams (IRemoteStreamContent):
    https://abp.io/docs/9.3/framework/architecture/domain-driven-design/application-services?_redirected=B8ABF606AA1BDF5C629883DF1061649A
  • Community article – Upload/Download files with Angular & IRemoteStreamContent (shows generated FormData proxies for direct parameters):
    https://abp.io/community/articles/que8cdr8
  • Support Q&A – File-management API from Angular, recommendation not to use generated proxy for upload and to use 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.

  1. Solution

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:

  • It hooks into CookieAuthenticationOptions.Events.OnValidatePrincipal.
  • Periodically (default ~1 minute) it checks the access_token associated with the cookie:
    • If token is expired or inactive (introspection active = false) it rejects the cookie principal (signs the user out).
  • It is meant for scenarios where:
    • You use OIDC login (AddAbpOpenIdConnect("oidc", …)).
    • Cookie lifetime is long (e.g. 365 days).
    • You still want the user to be logged out as soon as the access token becomes invalid, not when the cookie expires.

When the log shows active: false / audience not listed

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

  • The access token audience doesn’t include the current client (e.g. EAP), so OpenIddict:
    • Removes sensitive claims.
    • Returns active: false.

Common reasons:

  • In your OpenIddictDataSeedContributor, the client representing this Blazor Server app:
    • Doesn't list the correct API/resource as audience.
    • Or the API scope/audience configuration doesn’t match the token you’re sending.

Result: every validation attempt marks the token as not active, so the cookie is invalidated.

What you should do

  1. Prefer the new API

IntrospectAccessToken is obsolete. Use:

.AddCookie("Cookies", options =>
{
    options.ExpireTimeSpan = TimeSpan.FromDays(365);
    options.CheckTokenExpiration(); // new recommended helper
});
  1. Only use it when you really need token‑based sign‑out

Use CheckTokenExpiration / IntrospectAccessToken when:

  • The UI (Blazor Server / MVC) logs in via OIDC (oidc scheme), and
  • You call protected APIs using the same access token, and
  • You want the UI cookie to become invalid as soon as the access token is no longer valid.

If:

  • Your UI and backend are the same application, or
  • You don’t rely on the access token after login,

then you can simply remove IntrospectAccessToken() / CheckTokenExpiration() and rely on normal cookie expiration.

  1. If you keep it, fix the OpenIddict client/audience

If you want to keep token introspection:

  • In your OpenIddictDataSeedContributor, ensure the client for this app:
    • Has the correct scopes (e.g. your API scopes).
    • Has the correct audiences to match the APIs the token is issued for.
  • Then the introspection endpoint will return 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.
  • It’s now obsolete; use CheckTokenExpiration().
  • Use it only when you need access‑token‑driven logout; otherwise you can safely remove it.
  • Your current 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.

  1. Solution

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.

A. Remove only this solution’s configuration (recommended)

  1. Close ABP Suite / ABP Studio.

  2. Open this file on your machine:

    Windows

%UserProfile%\.abp\suite\appsettings.json

(Paste that path into File Explorer or open with a text editor.)

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

  2. Delete only that object from the array, keeping the rest of the JSON valid (commas, brackets, etc.).

  3. Save the file.

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

B. Reset all Suite configuration (if you prefer a clean slate)

If you don’t mind re‑adding all solutions to Suite:

  1. Close ABP Suite / ABP Studio.

  2. Delete the whole file:

    Windows

%UserProfile%\.abp\suite\appsettings.json
  1. Start ABP Suite / ABP Studio again and add your solutions anew.

This completely clears any old or corrupted solution mappings, including the one tied to the Sekmen.Goo name.


  1. Sources
  • 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.

  1. Override _Sidebar.cshtml and move controls to the sidebar footer

For LeptonX MVC, the sidebar is a partial under the Application layout:

  • Override path in your Web project
    Create this file in your MVC web project (the one that references 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.

    1. Locate the package folder on disk, something like:

      • Windows:
        C:\Users\<your-user>\.nuget\packages\volo.abp.aspnetcore.mvc.ui.theme.leptonx\<version>\Themes\LeptonX\Layouts\Application\_Sidebar.cshtml
      • Linux/macOS:
        ~/.nuget/packages/volo.abp.aspnetcore.mvc.ui.theme.leptonx/<version>/Themes/LeptonX/Layouts/Application/_Sidebar.cshtml
    2. Copy the entire content of that _Sidebar.cshtml into your project file: Themes/LeptonX/Layouts/Application/_Sidebar.cshtml.

    3. 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:

  • Either removing/commenting out anything that invokes toolbar components in the Application layout (e.g. _Toolbar.cshtml use), or
  • Overriding _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.

  1. Tie your own button into the LeptonX appearance (light/dark) switcher

The pattern Volosoft recommends (and uses in support) is:

  • Override window.initLeptonX
  • Update leptonx.globalConfig.defaultSettings
  • Call leptonx.init.run() again

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

  • You are reassigning the whole leptonx.globalConfig.defaultSettings object using spread syntax, not doing leptonx.globalConfig.defaultSettings.appearance = ....
    The direct property setter is what triggers the “getter only” error in current LeptonX versions; replacing the object is the supported pattern (same approach as the official support answer).

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.

  1. Logout button

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

  • LeptonX MVC documentation on overriding layouts & partials (_Sidebar.cshtml, _Toolbar.cshtml):
    https://abp.io/docs/latest/ui-themes/lepton-x/mvc
  • Official support answer showing the initLeptonX override and leptonx.globalConfig.defaultSettings usage:
    https://abp.io/support/questions/7452/How-to-configure-LeptonX-theme-appearance-without-having-to-make-you-own-version-of-it-just-for-simple-changes

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:

  1. Replace the sidebar component via replaceable components (Angular / MVC / Blazor)

    • LeptonX and LeptonX Lite expose layout parts (navbar, sidebar, etc.) as replaceable components.
    • You can register your own sidebar component and render the menu items yourself, using ABP’s menu data (Angular: NavItemsService, MVC/Blazor: IMenuContributor produces the menu and your component/layout renders it).
    • In your custom sidebar template you can:
      • Render most root items as collapsible (same as LeptonX).
      • Render special groups (like “Quick Links”) without:
        • the collapse/expand arrow, and
        • any collapse logic – simply always show their children.

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

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

  2. 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:

    • New optional per‑menu‑item configuration, e.g. AlwaysExpanded / DisableCollapse for LeptonX sidebar rendering.
    • Behavior:
      • When enabled on a root item with children:
        • Children are always visible.
        • No collapse/expand arrow is shown.
        • Other root items keep current behavior.

    This will put it on the product team’s radar similarly to the previous LeptonX accessibility feature request that was added to the backlog.


Sources

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

  1. Solution

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:

  • Your abp.io account email.
  • Your organization name(s) and which one is the active/default.
  • A short description that Blazorise’s API does not see your active ABP license for that email (so you can’t get the product token to remove the “Blazorise free version” bar).

They’ll be able to check your organizations and fix the mapping on the backend or coordinate with Blazorise if needed.

  1. Sources
  • https://abp.io/contact

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

Showing 111 to 120 of 894 entries
Boost Your Development
ABP Live Training
Packages
See Trainings
Mastering ABP Framework Book
The Official Guide
Mastering
ABP Framework
Learn More
Mastering ABP Framework Book
Made with ❤️ on ABP v10.2.0-preview. Updated on February 17, 2026, 09:10
1
ABP Assistant
🔐 You need to be logged in to use the chatbot. Please log in first.