Activities of "maliming"

Hi,

Confirmed — SwitchTenant is registered with the OpenIddict server but its handler is missing from the ABP token-grant dispatcher, that's why you see "not implemented".

Please add the following to your AuthServer module's ConfigureServices as a workaround:

using Volo.Abp.Account.Web.ExtensionGrants;
using Volo.Abp.OpenIddict.ExtensionGrantTypes;

public override void ConfigureServices(ServiceConfigurationContext context)
{
    // ... existing code ...

    Configure<AbpOpenIddictExtensionGrantsOptions>(options =>
    {
        options.Grants[SwitchTenantExtensionGrant.ExtensionGrantName] = new SwitchTenantExtensionGrant();
    });
}

After this, the SwitchTenant flow will dispatch correctly. We'll ship a permanent fix on our side in the next ABP release.

Thanks

Hi,

This is by design, not a bug. Adding a NuGet <PackageReference> is not the same as installing a module on the solution from ABP Suite's perspective — Suite only auto-loads entities from modules that were registered via "Install module on solution".

For custom modules, Suite provides an official extension point: the .suite/module-entity-extension.json file at the root of your module solution. Add the Domain DLL file names of the ABP modules you want to expose as navigation property targets. Example:

{
  "Modules": [
    { "DomainProjectDllFileName": "Volo.Abp.Identity.Pro.Domain.dll" }
  ]
}

Then in the Add Navigation Property dialog, check the Include entities from ABP modules checkbox — IdentityUser (and other entities from the listed DLLs) will appear in the dropdown. Suite will automatically build the solution before scanning, so you don't need to build manually, but the DLL has to physically exist somewhere under <solutionRoot>/src/**/bin/... for Assembly.LoadFrom to pick it up. If your module solution has no host project that copies NuGet dependencies to bin, this method won't find the DLL.

If the DLL approach doesn't work for your project layout, the always-working fallback is to add the navigation property manually — type the target type (Volo.Abp.Identity.IdentityUser), FK property (Guid UserId), display property, etc. directly in the dialog without picking from the dropdown.

Docs:

  • https://abp.io/docs/latest/suite/generating-crud-page#extending-with-custom-module-entities
  • https://abp.io/docs/latest/suite/generating-crud-page#adding-an-existing-entity-as-a-navigation-property

Thanks

Hi,

Glad the NoSmtpIdentityEmailSender part is working.

The SwitchTenant error is unrelated to UserSharingStrategy = Shared — that switch is correct and required, but the actual blocker is that the Angular OpenIddict application doesn't have the gt:SwitchTenant permission in the database.

You probably added SwitchTenant to ExtensionGrantTypes in the OpenIddict admin UI, but the next DbMigrator run wipes it out. Look at OpenIddictDataSeeder.cs in your DbMigrator project — CreateApplicationAsync does this:

if (!HasSameScopes(client, application))
{
    client.Permissions = JsonSerializer.Serialize(application.Permissions.Select(q => q.ToString()));
    await _applicationManager.UpdateAsync(client.ToModel());
}

HasSameScopes actually compares the full Permissions JSON, so any permission you add manually in the UI (like gt:SwitchTenant) makes the comparison fail and the seeder overwrites the whole permission list with what's defined in code.

Fix: add "SwitchTenant" to the Angular client's grantTypes list in OpenIddictDataSeeder.cs, then re-run DbMigrator:

//Angular Client
await CreateApplicationAsync(
    name: "Angular",
    type: OpenIddictConstants.ClientTypes.Public,
    consentType: OpenIddictConstants.ConsentTypes.Implicit,
    displayName: "Angular Client",
    secret: null,
    grantTypes: new List<string> {
        OpenIddictConstants.GrantTypes.AuthorizationCode,
        OpenIddictConstants.GrantTypes.RefreshToken,
        OpenIddictConstants.GrantTypes.Password,
        "LinkLogin",
        "Impersonation",
        "SwitchTenant"   // <-- add this
    },
    scopes: ...,
    ...
);

After DbMigrator runs, the OpenIddictApplicationPermissions table for the Angular client should contain a row gt:SwitchTenant. Then the Switch tenant action in Angular will succeed.

You can also add new grant types to an application directly from the OpenIddict admin UI (OpenIddict → Applications → edit → ExtensionGrantTypes). It's persisted to the database immediately, but as shown above it will be wiped on the next DbMigrator run unless you keep OpenIddictDataSeeder.cs in sync. So use the UI for quick experiments, and update the seeder for the permanent fix.

Thanks

Hi,

Sorry to hear about the trouble. To narrow this down on 8.3.0 we'll need a bit more info from your side:

  1. Which background job provider are you using? The default Volo.Abp.BackgroundJobs store, Hangfire, Quartz, or RabbitMQ?
  2. Deployment topology — single instance or multi-instance? Hosted in IIS / Docker / Kubernetes / Azure App Service / etc.? Any recent infra changes around April 22 (database failover, Redis restart, scaling, network changes)?
  3. BackgroundJobOptions configuration — anything you've customized (e.g. IsJobExecutionEnabled, custom JobExecutionTimeout, DefaultTimeout)?
  4. State of the AbpBackgroundJobs table — a snapshot of a few stuck rows showing IsAbandoned, NextTryTime, TryCount, LastTryTime would help a lot.
  5. Application logs around April 22 — especially anything at Error / Warning level from Volo.Abp.BackgroundJobs.* or Volo.Abp.DistributedLocking.*.
  6. Debug logs — please enable Debug level logging for the background job namespaces and capture a few minutes of output while jobs are sitting in the queue. Steps here: https://abp.io/support/questions/8622/How-to-enable-Debug-logs-for-troubleshoot-problemse

A few common causes we've seen for this exact symptom:

  • The worker host process is up but the background job worker isn't actually running (e.g. IsJobExecutionEnabled = false got set, or only the API host is running and the job host isn't).
  • A distributed lock (IAbpDistributedLock) is stuck — leftover lock row in DB / Redis that never expires, blocking the periodic queue poller.
  • Long-running DB transaction / blocking on the AbpBackgroundJobs table preventing the poller from reading the queue.
  • Job records all marked IsAbandoned = true after exceeding MaxTryCount (default 3), which is why "previously failing" jobs stopped retrying.

If you can share the logs and a sample of the AbpBackgroundJobs rows, we can dig in from there.

Thanks

Hi,

Yes, you can keep using AutoMapper. Starting from ABP 10.3.0, we shipped a new package Volo.Abp.LuckyPenny.AutoMapper exactly for this case — it's a drop-in replacement built on the commercial AutoMapper (15.x+) by LuckyPenny Software, which is the patched version (the free 14.x has a DoS vulnerability that won't be fixed).

Migration is just two steps in your own modules:

// *.csproj
-<PackageReference Include="Volo.Abp.AutoMapper" />
+<PackageReference Include="Volo.Abp.LuckyPenny.AutoMapper" />
// YourModule.cs
-[DependsOn(typeof(AbpAutoMapperModule))]
+[DependsOn(typeof(AbpLuckyPennyAutoMapperModule))]

All the types stay in the same namespaces (AbpAutoMapperOptions, IMapperAccessor, etc.), so your existing Profiles and CreateMap calls keep working as-is. License key goes through AbpAutoMapperOptions.Configurators:

Configure<AbpAutoMapperOptions>(options =>
{
    options.Configurators.Add(ctx =>
    {
        ctx.MapperConfiguration.LicenseKey = "YOUR_LICENSE_KEY";
    });
});

Note: Volo.Abp.LuckyPenny.AutoMapper and Volo.Abp.AutoMapper are mutually exclusive — pick one.

About Text Template Management (and other ABP modules migrated to Mapperly): you can use them just fine alongside AutoMapper in your own code. The Mapperly usage is internal to those modules — you don't need to write any Mapperly mappers yourself. The only thing you can't do is fully remove Mapperly from the solution, since the module assemblies are compiled against it. But it's just a transitive dependency, not something you have to interact with.

So the supported setup is:

  • Your application code → AutoMapper (via Volo.Abp.LuckyPenny.AutoMapper if you hold a commercial license)
  • ABP modules → Mapperly internally
  • Both coexist in the same solution without conflict

Docs: https://abp.io/docs/latest/framework/infrastructure/luckypenny-automapper

Thanks

Hi,

A couple of things to clear up first:

  • The "copy host admin and change TenantId" trick won't work — the tenant admin lives in the tenant DB (Isolated mode) and just changing TenantId on a host user breaks the multi-tenant integrity.
  • SwitchTenant grant only works when AbpMultiTenancyOptions.UserSharingStrategy == Shared. In Isolated mode the grant handler explicitly returns unsupported_grant_type, so adding it on the Angular OpenIddict client has no effect — please remove SwitchTenant from the Angular app's ExtensionGrantTypes.

For your "no SMTP allowed" scenario, the cleanest workaround is to replace the OOTB IIdentityEmailSender so the existing "Invite admin" button never calls SMTP. Instead, the accept-link is written to the log (or your own table / webhook / Redis — whatever your ops team can read). The host admin grabs the link from there and forwards it to the invitee through any internal channel (Slack/Teams/internal portal). The invitee opens the link, the OOTB Account/InviteUser page loads, they pick a username/password and accept.

Zero UI change, zero new endpoints, no client-proxy regeneration in layered solutions. The OOTB "Invite admin" button keeps working unchanged in MVC, Blazor, and Angular.

[Dependency(ReplaceServices = true)]
[ExposeServices(typeof(IIdentityEmailSender))]
public class NoSmtpIdentityEmailSender : IdentityEmailSender
{
    public NoSmtpIdentityEmailSender(
        IEmailSender emailSender,
        ITemplateRenderer templateRenderer,
        IStringLocalizer&lt;IdentityResource&gt; stringLocalizer,
        IAppUrlProvider appUrlProvider,
        UserSharingManager userSharingManager,
        UserInvitationManager userInvitationManager)
        : base(emailSender, templateRenderer, stringLocalizer, appUrlProvider,
               userSharingManager, userInvitationManager)
    {
    }

    public override async Task SendInviteEmailAsync(UserInvitation invitation, bool requireRegister, string appName)
    {
        var url = await AppUrlProvider.GetInviteUserUrlAsync(appName);
        var token = await UserInvitationManager.GenerateTokenFromInvitationIdAsync(invitation.Id);
        var link = $"{url}?token={UrlEncoder.Default.Encode(token)}";

        Logger.LogWarning(
            "[Invite] Tenant={tenantId} Email={email} RequireRegister={requireRegister} Link={link}",
            invitation.InviterTenantId, invitation.InviteeEmail, requireRegister, link);

        // Or persist to your own table / push to a webhook / write to Redis -
        // whatever your ops team can read from to retrieve the link.
    }
}

Drop this class into your Application module. Each invite produces a log line like:

[Invite] Tenant=... Email=admin@mycorp.com RequireRegister=True Link=https://your-app/Account/InviteUser?token=CfDJ8...

Just copy the Link=... value and forward it to the invitee. Done — no SMTP, no SwitchTenant, no host-user copying.

Thanks

Hi,

The "Switch Tenant" feature relies on a custom OpenIddict extension grant (SwitchTenant) plus a few /api/account/user-sharing endpoints. Here is everything you need to wire it up in Next.js.

1. Detect if Shared mode is enabled

Read multiTenancy.userSharingStrategy from the application configuration endpoint. Only show the "Switch tenant" UI when it equals 1 (Shared).

GET {HttpApiHost}/api/abp/application-configuration
{
  "multiTenancy": {
    "isEnabled": true,
    "userSharingStrategy": 1   // 0 = Isolated, 1 = Shared
  }
}

2. Get the list of tenants the current user is shared with

GET {HttpApiHost}/api/account/user-sharing
Authorization: Bearer <current_access_token>

Response:

{
  "items": [
    { "tenantId": "guid-or-null", "tenantName": "TenantA", "email": "user@x.com" },
    { "tenantId": "guid-or-null", "tenantName": "TenantB", "email": "user@x.com" }
  ]
}

3. Switch tenant via the custom SwitchTenant grant type

This is the key part. Post to the AuthServer token endpoint with grant_type=SwitchTenant. The current access_token is sent as a parameter (the grant handler introspects it to identify the user) and a new token set scoped to the target tenant is returned.

POST {AuthServerUrl}/connect/token
Content-Type: application/x-www-form-urlencoded

grant_type=SwitchTenant
&client_id=<your_client_id>
&client_secret=<your_client_secret>      // only for confidential clients
&access_token=<current_valid_access_token>
&TenantId=<target_tenant_guid>           // omit / leave empty to switch to Host

Response is a standard OAuth token response:

{
  "access_token": "...",
  "refresh_token": "...",
  "id_token": "...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "scope": "..."
}

The new access_token already has the target tenant's tenantid claim, so every subsequent API call will run in the new tenant context.

After receiving the response in Next.js you should:

  • Replace the stored access_token / refresh_token / id_token
  • Reload the app (or re-initialize state) so all subsequent requests use the new tokens

This is exactly what Angular does — see SwitchTenantService in commercial-ui/config/src/services/switch-tenant.service.ts, which calls loginUsingGrant('SwitchTenant', { TenantId }) and then location.href = baseUrl to refresh.

4. Leave a shared tenant (optional)

DELETE {HttpApiHost}/api/account/user-sharing/leave?tenantId=<guid>
Authorization: Bearer <current_access_token>

5. Invitation endpoints (if you need invite/accept/reject in your UI)

GET  /api/account/user-sharing/invitation?token=<inviteToken>
POST /api/account/user-sharing/invitation/accept   // body: { token, ... }
POST /api/account/user-sharing/invitation/reject   // body: { token, ... }

Backend prerequisites (just to confirm)

Configure<AbpMultiTenancyOptions>(options =>
{
    options.IsEnabled = true;
    options.UserSharingStrategy = TenantUserSharingStrategy.Shared;
});

If UserSharingStrategy is not Shared, the SwitchTenant grant returns unsupported_grant_type.

TL;DR for the Next.js side

  1. GET /api/abp/application-configuration → check multiTenancy.userSharingStrategy === 1
  2. GET /api/account/user-sharing → render the tenant list
  3. On selection: POST /connect/token with grant_type=SwitchTenant&access_token=<current>&TenantId=<guid>
  4. Save the new tokens and reload the app

Thanks

Hi @rcalv002,

Thanks for letting us know. We will continue working on the Elsa module, and any updates will be included in the release notes for upcoming versions.

Thanks

Hi,

Can you replace the contents of the inline workaround script block in your _Host.cshtml with this (the surrounding script tag and the rest of the page stay as-is):

// LeptonX 5.3.0 side-menu rebind workaround.
// initSideMenu runs once from SideMenuLayout.OnAfterRenderAsync and may
// execute against an empty #lpx-sidebar (when MainMenuProvider yields) or
// be skipped on impersonation rebuild. We watch the sidebar and, whenever
// an outer menu item ends up without click listeners, remove every
// previously attached listener (so we never double-bind a preserved
// element) and call initSideMenu once more to re-attach a single clean
// set. window.initLeptonX is wrapped so the natural call from
// OnAfterRenderAsync goes through the same cleanup path.
(function () {
    try {
        const allBinds = new WeakMap();
        let timer = null;

        const origAdd = EventTarget.prototype.addEventListener;
        EventTarget.prototype.addEventListener = function (type, fn, opts) {
            if (this instanceof Element
                && this.classList && this.classList.contains('lpx-menu-item')
                && this.closest && this.closest('#lpx-sidebar')) {
                let arr = allBinds.get(this);
                if (!arr) { arr = []; allBinds.set(this, arr); }
                arr.push({ type: type, fn: fn, opts: opts });
            }
            return origAdd.call(this, type, fn, opts);
        };

        function cleanupBinds() {
            document.querySelectorAll('#lpx-sidebar .lpx-menu-item').forEach(function (el) {
                const arr = allBinds.get(el);
                if (arr) {
                    arr.forEach(function (b) { el.removeEventListener(b.type, b.fn, b.opts); });
                    allBinds.delete(el);
                }
            });
        }

        const origInitLeptonX = window.initLeptonX;
        window.initLeptonX = function () {
            cleanupBinds();
            return origInitLeptonX.apply(this, arguments);
        };

        function reinit() {
            cleanupBinds();
            const fn = window.leptonx
                && window.leptonx.init
                && window.leptonx.init.initializers
                && window.leptonx.init.initializers.get('initSideMenu');
            if (fn) fn();
        }

        function check() {
            const items = document.querySelectorAll('#lpx-sidebar .outer-menu-item > .lpx-menu-item');
            if (items.length === 0) return;
            let needs = false;
            for (let i = 0; i < items.length; i++) {
                if (!allBinds.has(items[i])) { needs = true; break; }
            }
            if (needs) reinit();
        }

        function debounced() { clearTimeout(timer); timer = setTimeout(check, 300); }

        new MutationObserver(debounced).observe(document.body, { childList: true, subtree: true });
        debounced();
    } catch (err) {
        console.error('[lpx-fix] failed:', err);
    }
})();

A proper upstream fix is also queued for the next LeptonX patch release, so once you upgrade you can drop this whole inline script.

Thanks

Hi,

Thanks for the update, glad to hear you have an internal procedure in place for now. The fix is already merged and will ship in the next release, so you can drop the workaround once you upgrade.

Thanks

Showing 571 to 580 of 12926 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.8.0-preview. Updated on November 02, 2026, 06:56
1
ABP Assistant
🔐 You need to be logged in to use the chatbot. Please log in first.