Yes: with TenantUserSharingStrategy.Shared, the intended way to associate a user with a tenant is the invitation flow (invite → accept). If you “create the same user again” inside each tenant like in isolated mode, you end up with separate identities and you will “break” the shared-account model (no global identity to synchronize).
It’s not that a user must be created under Host manually. In Shared mode, the system is designed so that users are global, and tenants are workspaces a user joins. The normal onboarding is:
When a user is invited from Tenant A for the first time and they don’t already have an account, after they accept/register ABP will (conceptually) create:
What role is assigned in Host?
Validated references:
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.
No, users do not have to be created under Host first.
About My Account updates and syncing:
Name/Surname/Phone inside a tenant UI, it should still end up updating the global identity (host) and then replicate/sync.About phone number and 2FA code destination:
What you should do to get the correct behavior end-to-end:
TenantUserSharingStrategy.Shared enabled (you already did).Links (validated):
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.
TenantUserSharingStrategy.Shared, it is expected to see multiple rows in AbpUsers for the same person (Host + each tenant). ABP keeps these rows synchronized automatically (it’s not a “single physical row shared by all tenants”). This design is required especially for scenarios like database-per-tenant, where a single shared row is not possible.TenantId = NULL). Security settings like 2FA are handled at the Host side, so 2FA works consistently even though there are replicated tenant rows.To enable and use Shared User Accounts correctly (and get the syncing behavior), do all of the following:
*.Domain or *.HttpApi.Host module where you configure multi-tenancy):using Volo.Abp.MultiTenancy;
Configure<AbpMultiTenancyOptions>(options =>
{
options.IsEnabled = true;
options.UserSharingStrategy = TenantUserSharingStrategy.Shared;
});
Use the shared-user workflow to add a user to another tenant:
Update user profile data from the Host user account (TenantId=NULL)
If you migrated an existing database (created users before switching), clean up duplicates created the “isolated way”
More details and the intended behavior/workflow are described here:
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 (with OpenIddict/IdentityServer-style JWT access tokens), you generally can’t “invalidate” an already-issued JWT access token server-side unless your APIs introspect tokens (reference tokens) or you keep a server-side denylist and check it on every request. A JWT is self-contained and remains valid until it expires.
The recommended approach to “log the user out everywhere” is:
If your goal is “call something from the app server and force the browser user to be logged out”:
/connect/revocation from your app server can revoke refresh tokens (depending on your OpenIddict/IdentityServer configuration), but it does not immediately invalidate an already-issued JWT access token that the SPA is currently using.If you truly need immediate access token invalidation, use access token introspection (so the API asks the auth server if the token is still active on each call). ABP support confirms using token introspection to address token/session desync scenarios:
Sources (validated):
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.
Where to configure it (in your Blazor Server host):
// in ConfigureServices
services.AddServerSideBlazor().AddCircuitOptions(options =>
{
// How long to keep a disconnected circuit in memory
options.DisconnectedCircuitRetentionPeriod = TimeSpan.FromMinutes(3); // example
});
Default value
DisconnectedCircuitRetentionPeriod is 3 minutes (Blazor Server default).If you meant a different “idle timeout” (e.g., authentication cookie/session expiration or ABP’s Idle Session Timeout feature in Settings), that is a different mechanism and not the Blazor Server circuit timeout.
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.
BlogPostPublicAppService, ABP’s conventional routing ends up mapping both:
GetAsync(...) andGetListAsync(...)
to the same path: GET /api/app/voxo-blog-post-publicThat can happen if GetAsync is not considered to have an id route parameter by the conventional route builder (e.g., its parameter name/type doesn’t match what the convention expects), so ABP doesn’t add /{id} and both methods collapse to the same route.
Override the methods you want (even if you keep the base logic) and add explicit ASP.NET Core route attributes so they can’t collapse to the same path.
Example pattern:
using Microsoft.AspNetCore.Mvc;
public class VoxoBlogPostPublicAppService : BlogPostPublicAppService
{
public VoxoBlogPostPublicAppService(
IBlogRepository blogRepository,
IBlogPostRepository blogPostRepository,
ITagRepository tagRepository,
BlogPostManager blogPostManager)
: base(blogRepository, blogPostRepository, tagRepository, blogPostManager)
{
}
[HttpGet]
[Route("api/app/voxo-blog-post-public/{id}")]
public override Task<BlogPostPublicDto> GetAsync(Guid id)
{
return base.GetAsync(id);
}
[HttpGet]
[Route("api/app/voxo-blog-post-public")]
public override Task<PagedResultDto<BlogPostPublicDto>> GetListAsync(BlogPostPublicGetListInput input)
{
return base.GetListAsync(input);
}
}
Key points:
GetAsync to be .../{id} and GetListAsync to be the collection route.[ExposeServices(typeof(IBlogPostPublicAppService))] didn’t change itBecause the problem is not “two controllers exist”; it’s “two actions resolve to the same HTTP method + path” for the generated controller of the service type that ends up exposed. Replacing DI alone doesn’t guarantee the auto-controller route templates remain unique after conventions are applied.
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.
BlogPostPublicAppService is exposed as an HTTP API via Auto API Controllers. When you add your derived class in the Application layer, ABP will treat it as a new application service and (by convention) it will also be turned into an API controller.In your specific case, both of these methods end up being mapped to the same conventional route:
GetAsync(...) → GET /api/app/voxo-blog-post-publicGetListAsync(...) → GET /api/app/voxo-blog-post-publicSwashbuckle then throws:
Conflicting method/path combination "GET api/app/voxo-blog-post-public" …
This happens because Auto API Controller conventional routing requires unique GET routes, and ABP’s conventional routing is based on method names/signatures. If the method parameters don’t let ASP.NET Core distinguish the templates (e.g., no {id} segment ends up in the template for one of them), you’ll get exactly this kind of Swagger collision.
Keep the replacement only in DI, and ensure only one controller is generated.
Use only the interface/base service exposure, and do not expose the concrete type itself:
[Dependency(ReplaceServices = true)]
[ExposeServices(typeof(IBlogPostPublicAppService))]
public class VoxoBlogPostPublicAppService : BlogPostPublicAppService
{
public VoxoBlogPostPublicAppService(
IBlogRepository blogRepository,
IBlogPostRepository blogPostRepository,
ITagRepository tagRepository,
BlogPostManager blogPostManager)
: base(blogRepository, blogPostRepository, tagRepository, blogPostManager)
{
}
}
Notes:
typeof(BlogPostPublicAppService) and typeof(VoxoBlogPostPublicAppService) from [ExposeServices].IBlogPostPublicAppService to your class, but the framework won’t treat your class as an additional HTTP endpoint “service type” to generate routes for.If you really want the class to be treated as a separate API controller, you must ensure GetAsync and GetListAsync do not map to the same GET path by explicitly adding [HttpGet] / [Route] attributes (or changing method names). But in an override/replacement scenario, that’s usually not what you want.
Links (verified):
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.
This is a known limitation/bug pattern of LeptonX’s FormSelect enhancement: it builds a non-Blazor-owned wrapper (.custom-select-wrapper) around the native <select> and (as you observed) it doesn’t reliably clean up when the underlying <select> is removed/re-rendered by a SPA renderer (like Blazor). The result is exactly what you measured: orphaned wrappers accumulating and duplicated/invalid combobox UI.
Workaround (client-side cleanup on removal): add your own DOM-removal hook that deletes the LeptonX wrapper when the underlying <select> leaves the DOM.
abp.dom.onNodeRemoved(...) (it’s based on MutationObserver) and remove .custom-select-wrapper nodes that no longer contain a <select> (or whose aria-labelledby target is missing).<select>.Example “orphan wrapper” cleanup logic (plain JS; safe to run globally):
// Run once on app startup
const cleanupOrphanLeptonSelectWrappers = () => {
document.querySelectorAll(".custom-select-wrapper[data-lpx-bound='true']").forEach(w => {
const select = w.querySelector("select");
if (!select) {
w.remove();
return;
}
// Optional: also remove if aria-labelledby points to missing element
const display = w.querySelector(".custom-select-display[aria-labelledby]");
if (display) {
const id = display.getAttribute("aria-labelledby");
if (id && !document.getElementById(id)) {
w.remove();
}
}
});
};
const obs = new MutationObserver(muts => {
// If anything removed, run cleanup
if (muts.some(m => m.removedNodes && m.removedNodes.length)) {
cleanupOrphanLeptonSelectWrappers();
}
});
obs.observe(document.body, { childList: true, subtree: true });
Workaround (avoid enhancement for Blazor-owned selects):
select.form-select). If you can render selects without form-select, LeptonX won’t wrap them.If you need a forced rebind/rebuild (when Blazor reuses DOM in-place): your “tear down wrapper then call leptonx.FormSelect.processSelect(select)” approach is consistent with the other reported LeptonX stale-wrapper issues. In practice:
.custom-select-wrapper containing the selectprocessSelect(select) after the render cycleThat avoids duplication and stale markup.
Product fix: there is no public/verified documentation for a dispose/destroy API on leptonx.FormSelect in your version, so a theme-side fix would need to add a removed-nodes path (or explicit destroy) in LeptonX.
Sources (verified):
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 install-libs work with Yarn Berry. ABP’s guidance for its frontend tooling is to use Yarn Classic (v1.22+) (not v2+), so the CLI behavior you’re seeing is consistent with the supported tooling expectations.abp install-libs is not required (and in your case, it’s actively harmful because it can rewrite yarn.lock to v1 format). You can remove it from your setup script. The install-libs command is for installing/copying web (JS/CSS) libraries into wwwroot/libs for MVC/Razor Pages/Blazor Server style UIs; Angular manages its dependencies via its own package manager workflow (yarn install, yarn start/build) and does not need that wwwroot/libs step.Practical recommendation for your repo:
abp install-libs from your onboarding/build scripts for the Angular project.corepack prepare yarn@4.12.0 --activate
yarn install
yarn.lock is rewritten to a v1 header format, to prevent silent regressions.Source (ABP prerequisite explicitly recommending Yarn Classic, not v2+):
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.