Do all of the following:
Register every real redirect URI used by Hangfire in the remote environment:
Seed the Hangfire OpenIddict client with those exact values (seed order doesn’t matter, but strings must match 1:1 with what the browser will use):
await CreateOrUpdateApplicationAsync(
name: "Hangfire",
type: OpenIddictConstants.ClientTypes.Confidential,
consentType: OpenIddictConstants.ConsentTypes.Implicit,
displayName: "Hangfire Client",
secret: "1q2w3e*",
grantTypes:
[
OpenIddictConstants.GrantTypes.AuthorizationCode,
OpenIddictConstants.GrantTypes.ClientCredentials,
OpenIddictConstants.GrantTypes.RefreshToken,
// Remove Password & Implicit unless you really need them; not required for server-side MVC OIDC
"pushed_authorization"
],
scopes:
[
OpenIddictConstants.Scopes.OpenId,
OpenIddictConstants.Scopes.Profile,
OpenIddictConstants.Scopes.Email,
OpenIddictConstants.Scopes.Phone,
OpenIddictConstants.Scopes.OfflineAccess,
"roles",
"HangfireService"
],
redirectUris:
[
"https://treva-hangfire.tav.aero/signin-oidc",
"https://treva-web.tav.aero/signin-oidc",
"https://treva-webgateway.tav.aero/signin-oidc",
"https://treva-authserver.tav.aero/signin-oidc"
],
postLogoutRedirectUris:
[
"https://treva-hangfire.tav.aero/signout-callback-oidc",
"https://treva-web.tav.aero/signout-callback-oidc",
"https://treva-webgateway.tav.aero/signout-callback-oidc",
"https://treva-authserver.tav.aero/signout-callback-oidc"
]
);
Ensure the Hangfire Dashboard app challenges with the same authority and callback path:
If you enabled PAR (pushed_authorization):
Clear caches after seeding:
Common pitfalls to avoid:
Verify
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.
using Microsoft.Extensions.AI;
using OpenAI;
[DependsOn(
typeof(AbpAIModule), // AI infrastructure
typeof(AiManagementModule) // AI Management module
)]
public class MyProjectWebModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
var configuration = context.Services.GetConfiguration();
context.Services.AddSingleton(new OpenAIClient(configuration["AI:OpenAI:Key"]));
context.Services.AddChatClient(services =>
services.GetRequiredService<OpenAIClient>()
.AsChatClient(configuration["AI:OpenAI:Chat:ModelId"] ?? "gpt-4o-mini"));
}
}
- appsettings.json example:
{
"AI": {
"OpenAI": {
"Key": "YOUR-KEY",
"Chat": { "ModelId": "gpt-4o-mini" }
}
}
}
Current limitations to be aware of (preview):
Roadmap / planning
Custom chat client calling an external API (AI agents)
using System.Net.Http;
using System.Net.Http.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.AI;
public sealed class HttpForwardingChatClient : IChatClient
{
private readonly HttpClient _http;
private readonly string _endpoint; // e.g. https://customer-api.example.com/agent/chat
public HttpForwardingChatClient(HttpClient http, string endpoint)
{
_http = http;
_endpoint = endpoint;
}
public async Task<ChatCompletion> CompleteAsync(
ChatMessage message,
ChatOptions options = null,
CancellationToken cancellationToken = default)
{
var payload = new
{
input = message.Text,
// optionally map options (system prompt, temperature, tools, etc.)
};
var response = await _http.PostAsJsonAsync(_endpoint, payload, cancellationToken);
response.EnsureSuccessStatusCode();
var result = await response.Content.ReadFromJsonAsync<ExternalResponse>(cancellationToken: cancellationToken);
return new ChatCompletion(new ChatMessage(Role.Assistant, result?.Output ?? string.Empty));
}
// Implement other IChatClient members if needed by your use-cases (e.g., streaming)
private sealed class ExternalResponse
{
public string Output { get; set; }
}
}
Registering the custom client with ABP AI:
[DependsOn(typeof(AbpAIModule), typeof(AiManagementModule))]
public class MyProjectWebModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
var configuration = context.Services.GetConfiguration();
context.Services.AddHttpClient<HttpForwardingChatClient>(client =>
{
client.BaseAddress = new Uri(configuration["AI:ExternalAgent:BaseUrl"]);
});
context.Services.AddChatClient(sp =>
sp.GetRequiredService<HttpForwardingChatClient>());
}
}
After registration, you can inject IChatClient anywhere (app services, pages, components) and AI Management can treat it similarly to built-in providers, as the integration point is the same abstraction (IChatClient). If your scenario requires streaming, tools/functions, or multi-turn context, implement the corresponding members or map the needed options to your external API.
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.
Background
Part A: Host admin can still see “Management Service” when RequireFeatures is used
public class CarbonFeatureDefinitionProvider : FeatureDefinitionProvider
{
public override void Define(IFeatureDefinitionContext context)
{
var group = context.AddGroup("CarbonFeatures");
group.AddFeature("CarbonFeatures.Categories", defaultValue: "true"); // host-default
group.AddFeature("CarbonFeatures.Categories.Create", defaultValue: "true");
group.AddFeature("CarbonFeatures.Categories.Edit", defaultValue: "true");
group.AddFeature("CarbonFeatures.Categories.Delete", defaultValue: "true");
group.AddFeature("CarbonFeatures.CategoryTypes", defaultValue: "true");
group.AddFeature("CarbonFeatures.CategoryTypes.Create", defaultValue: "true");
group.AddFeature("CarbonFeatures.CategoryTypes.Edit", defaultValue: "true");
group.AddFeature("CarbonFeatures.CategoryTypes.Delete", defaultValue: "true");
}
}
public sealed class HostBypassFeatureChecker : ISimpleStateChecker<PermissionDefinition>
{
public async Task<bool> IsEnabledAsync(SimpleStateCheckerContext<PermissionDefinition> context)
{
var currentTenant = context.ServiceProvider.GetRequiredService<ICurrentTenant>();
if (!currentTenant.IsAvailable) // Host
return true;
var featureChecker = context.ServiceProvider.GetRequiredService<IFeatureChecker>();
// Map permission name to feature name as you need
var featureName = MapPermissionToFeature(context.State.Name);
return await featureChecker.IsEnabledAsync(featureName);
}
private static string MapPermissionToFeature(string permissionName)
{
// Example mapping: adjust to your permission constants
if (permissionName.StartsWith(ManagementServicePermissions.Categories.Default))
return "CarbonFeatures.Categories";
if (permissionName.StartsWith(ManagementServicePermissions.CategoryTypes.Default))
return "CarbonFeatures.CategoryTypes";
return permissionName; // fallback
}
}
// Apply to your permission definitions:
var category = myGroup.AddPermission(ManagementServicePermissions.Categories.Default, L("Permission:Categories"));
category.StateCheckers.Add(new HostBypassFeatureChecker());
This approach keeps the Host menu visible without requiring Host features to be explicitly enabled.
{
path: 'category-types',
component: CategoryTypesComponent,
canActivate: [PermissionGuard],
data: { requiredPolicy: 'ManagementService.CategoryTypes' }
}
- Menu item:
this.context.addItem({
id: 'ManagementService.CategoryTypes',
text: 'Category Types',
icon: 'fa fa-list',
order: 10,
requiredPolicy: 'ManagementService.CategoryTypes'
}, MANAGEMENT_MENU_NAME);
- If you need feature-level hiding too, add a feature check in the menu contributor and skip adding the item when feature is disabled for the current tenant.
2) Do not grant the permission in the tenant’s package if the page must be hidden. If you are generating grants dynamically, verify PermissionGrant table has no rows for the excluded pages. Your observation is correct: once the grants are removed, the menu is hidden provided you use requiredPolicy on the menu item.
3) If you keep using StateCheckers (bypass for host, check feature for tenant), keep menu items bound to those exact permissions. Since ABP UI reads permission states, menu entries disappear for tenants when the feature is disabled.
4) For microservice solutions with dynamic permissions: verify the PermissionDefinition names you bind in Angular match the definitions pushed by that microservice. A mismatch leads to menu items always visible due to missing policy binding.
Part C: Known edge cases and recommendations
Minimal example putting all together
public class ManagementServicePermissionDefinitionProvider : PermissionDefinitionProvider
{
public override void Define(IPermissionDefinitionContext context)
{
var group = context.AddGroup("ManagementService");
var categories = group.AddPermission("ManagementService.Categories", L("Permission:Categories"));
categories.StateCheckers.Add(new HostBypassFeatureChecker()); // host bypass
categories.RequireFeatures("CarbonFeatures.Categories"); // tenant feature
var categoryTypes = group.AddPermission("ManagementService.CategoryTypes", L("Permission:CategoryTypes"));
categoryTypes.StateCheckers.Add(new HostBypassFeatureChecker());
categoryTypes.RequireFeatures("CarbonFeatures.CategoryTypes");
}
}
// route
{ path: 'categories', component: CategoriesComponent, canActivate: [PermissionGuard], data: { requiredPolicy: 'ManagementService.Categories' } }
{ path: 'category-types', component: CategoryTypesComponent, canActivate: [PermissionGuard], data: { requiredPolicy: 'ManagementService.CategoryTypes' } }
// menu
this.context.addItem({
id: 'ManagementService',
text: 'ManagementService',
icon: 'fa fa-book',
order: 30
});
this.context.addItem({
id: 'ManagementService.Categories',
text: 'Categories',
parentId: 'ManagementService',
requiredPolicy: 'ManagementService.Categories'
});
this.context.addItem({
id: 'ManagementService.CategoryTypes',
text: 'Category Types',
parentId: 'ManagementService',
requiredPolicy: 'ManagementService.CategoryTypes'
});
public class CarbonFeatureDefinitionProvider : FeatureDefinitionProvider
{
public override void Define(IFeatureDefinitionContext context)
{
var g = context.AddGroup("CarbonFeatures");
g.AddFeature("CarbonFeatures.Categories", defaultValue: "true");
g.AddFeature("CarbonFeatures.CategoryTypes", defaultValue: "true");
}
}
This approach will:
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.
Please contact us at 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.
Cause: CMS Kit Blog pages ship their own Bootstrap container/row markup and some generic CSS. In LeptonX for Blazor, the application body already uses a flex/grid layout and spacing utilities. When the blog list and post components render inside the layout, duplicated container/row wrappers and a few global CSS classes from the Blog module can break the page width and push the sidebar/header out of alignment.
Fix 1 — Update LeptonX and CMS Kit to the latest compatible versions for ABP 9.3.x:
Fix 2 — Remove conflicting container wrappers in your CMS Kit page overrides:
<!-- Before -->
<div class="container">
<div class="row">
<div class="col-12">
@* Blog content *@
</div>
</div>
</div>
<!-- After -->
@* Use LeptonX content area; no extra container *@
@* Blog content *@
Fix 3 — Ensure you’re not importing Blog CSS globally:
Fix 4 — Verify LeptonX app layout and content container:
Fix 5 — Clear cached assets:
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.
Reason: The client (Login/Register/ChangePassword pages) adds its own validators. If you lower Identity password requirements on the server (e.g., RequiredLength = 3), but the UI still shows “min 6” (or other rules), it means the client-side validators are not refreshed or they are hard-coded/defaulted.
Fix (Angular UI):
Fix (MVC UI):
Notes:
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’t load Lepton (v1) and LeptonX themes at the same time. Both modules register the same keys and services (e.g., AbpErrorPageOptions.ErrorViewUrls["401"], DefaultTheme, toolbar contributors, bundle names), which causes “An item with the same key has already been added” and “service not registered” errors. ABP supports only one active MVC theme per application.
Pick one theme and remove the other theme module(s) and registrations:
Do not inherit one theme module from the other. Theme modules are independent and each configures its own services/options. Inheriting or registering both leads to duplicate option keys and missing service registrations for the “current theme” resolver.
After changes:
Code example (use LeptonX only):
using Volo.Abp.AspNetCore.Mvc.UI.Theming;
using Volo.Abp.Modularity;
using Volo.Abp.AspNetCore.Mvc.UI.Theme.LeptonX;
[DependsOn(
typeof(AbpAspNetCoreMvcUiLeptonXThemeModule)
// remove: typeof(AbpAspNetCoreMvcUiLeptonThemeModule)
)]
public class MyWebModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
Configure<AbpThemingOptions>(options =>
{
// Don’t add Lepton and LeptonX together
options.Themes.Add<LeptonXTheme>();
options.DefaultThemeName = LeptonXTheme.Name;
});
// Remove duplicate error page mapping if you had both:
// Configure<AbpErrorPageOptions>(options => { ... }); // Not needed for LeptonX
}
}
If you need to switch themes dynamically at runtime, implement a single theme that changes its resources/layouts based on a setting or cookie rather than loading two different theme modules simultaneously.
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.
Why this happens
Keep AppName visible and customize the logo Option A: Use LogoUrl only inside the app shell, not on the login page
@inject Volo.Abp.Ui.Branding.IBrandingProvider Branding
<div class="lpx-branding lpx-branding--login">
<img src="@Branding.LogoUrl" alt="@Branding.AppName" class="lpx-branding__logo" />
<div class="lpx-branding__title">@Branding.AppName</div>
</div>
@code { }
- Add minimal CSS to keep original sizing:
- Add to wwwroot/global-styles.css (or your bundle):
.lpx-branding--login {
display: flex;
flex-direction: column;
align-items: center;
gap: .5rem;
}
.lpx-branding__logo {
height: 56px; /* similar to default */
width: auto;
}
.lpx-branding__title {
font-size: 1.5rem;
font-weight: 600;
}
- This ensures both the logo and AppName are shown on login regardless of LogoUrl.
Option B: Do not override LogoUrl; use LeptonX CSS variables
:root .lpx-theme-light {
--lpx-logo: url('/images/logo/leptonx/icon-logo.svg');
--lpx-logo-icon: url('/images/logo/leptonx/icon.svg');
}
:root .lpx-theme-dark,
:root .lpx-theme-dim {
--lpx-logo: url('/images/logo/leptonx/icon-logo.svg');
--lpx-logo-icon: url('/images/logo/leptonx/icon.svg');
}
- Keep DefaultBrandingProvider.LogoUrl = null (do not override), and keep AppName override as desired:
[Dependency(ReplaceServices = true)]
public class AbpTabsBrandingProvider : DefaultBrandingProvider
{
public override string AppName => "AbpTest";
// Do not override LogoUrl -> keep base.LogoUrl (null) so AppName is displayed
}
- The theme will use your CSS-defined logos in the layout while still showing AppName on the login page.
/* If not overriding component, target default branding image on login */
.lpx-auth .lpx-branding img,
.lpx-auth .lpx-branding svg {
height: 56px;
width: auto;
}
If your asset is small intrinsically, set a consistent height in CSS as above.
Per-tenant branding
Summary of what to change
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.
Root cause: In ABP v9.2+ Angular UI packages were migrated to standalone. If your app still uses NgModule-based components, ABP Angular elements like abp-button (emitting abpClick) and abp-modal (content projection, submit wiring) will only work if their standalone imports/providers are added to your module (or component) explicitly. Using (click) works because it’s plain Angular, but abpClick is an ABP output from the ButtonComponent and requires importing the ABP button component/directives into the module where it’s used.
Fix: Import the ABP Angular standalone components into your module via the provided module-level re-export (for non-standalone apps), or import the components directly in a standalone component’s imports.
For a non-standalone component declared in an NgModule:
// app.module.ts (or a feature module where your modal component is declared)
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
// Bring ABP Angular shared directives/components into module-based apps:
import { AbpThemeSharedModule } from '@abp/ng.theme.shared'; // re-exported compatibility module
import { MyFeatureComponent } from './my-feature.component';
@NgModule({
declarations: [MyFeatureComponent],
imports: [
CommonModule,
AbpThemeSharedModule, // IMPORTANT: provides abp-button, abp-modal, directives like abpClose, abpPermission, etc.
],
})
export class MyFeatureModule {}
If you don’t have AbpThemeSharedModule (depending on version), import the specific components into your module via their modules (compatibility packages) or switch your component to standalone and import the components there:
// my-feature.component.ts (standalone variant)
import { Component } from '@angular/core';
import { ButtonComponent } from '@abp/ng.theme.shared/components/button';
import { ModalComponent } from '@abp/ng.theme.shared/components/modal';
@Component({
selector: 'app-my-feature',
standalone: true,
imports: [ButtonComponent, ModalComponent],
template: `
<abp-modal [(visible)]="isOpen">
<ng-template #abpHeader>Title</ng-template>
<ng-template #abpBody>Body</ng-template>
<ng-template #abpFooter>
<abp-button (abpClick)="onSubmit()">Submit</abp-button>
</ng-template>
</abp-modal>
`,
})
export class MyFeatureComponent {
isOpen = true;
onSubmit() { /* ... */ }
}
<!-- inside modal footer/body -->
<abp-button (abpClick)="onSubmit()" [buttonType]="'button'">
Save
</abp-button>
Double-check that there isn’t an overlay or disabled state blocking clicks:
If you are wiring form submit:
Summary checklist for module-based app:
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 9.x targets .NET 9. The official migration guide states that if you plan to use ABP 9.0, you need to migrate your solutions to .NET 9.0. Therefore, ABP 9.0/9.1 packages (for example, Volo.Saas.Host.Application 9.0.0) are not compatible with net8.0 and will produce NU1202 (exactly like in your screenshot), because those packages include net9.0 TFMs.
What you can do:
Minimal steps for Option B (upgrade to .NET 9 + ABP 9.x):
Why your error happens:
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.