We are using ABP 8.x (or 9/10.x if applicable) with the SaaS module enabled and database-per-tenant architecture. The tenant is correctly created, and the table SaasTenantConnectionStrings contains a valid "Default" connection string for that TenantId.
However, during runtime ABP is not switching the connection string. All data is always saved into the Host database regardless of the tenant connection string.
What we already verified
The tenant exists in SaasTenants.
The connection string exists in SaasTenantConnectionStrings with Name = "Default".
The connection string points to a different database and that database exists.
Permissions and authentication are working correctly.
Requests are successfully authenticated and the application resolves the user identity, but data is still persisted in the Host DB.
Expected behavior
ABP should switch the connection string when a tenant is resolved, and EF Core should write data into the tenant's isolated database.
Actual behavior
ABP always uses the Host database even though a tenant-specific connection string is present. No exceptions are thrown. EF Core always connects to the Host DB.
Additional notes
The issue occurs even when calling the application service methods that use CurrentTenant.
We confirmed that the connection string for the tenant is correct by testing it manually.
We also checked the DbContext configuration and module configuration, but ABP still ignores the tenant connection string.
No domain resolution or pipeline configuration seems to affect the problem; the behavior is consistent.
Request
We need assistance validating:
Whether ABP is correctly resolving the tenant in the current HTTP request.
Whether EF Core is correctly switching the connection string for the DbContext.
Whether there is any misconfiguration in our DbContext, module setup, or multitenancy configuration that prevents ABP from using the tenant connection string.
Please advise how to debug or what additional logs to produce so we can identify why ABP always uses the Host database.
4 Answer(s)
-
0
- Solution Below is a definitive checklist to find and fix why ABP is not switching to the Tenant’s “Default” connection string (DB-per-tenant) and always persists to Host DB.
A. Verify tenant resolution happens before EF Core runs
- Ensure multi-tenancy middleware order is correct:
- For modular/tiered apps and microservices, UseMultiTenancy must be after UseRouting and after UseAuthentication, before UseAuthorization.
- Example:
app.UseRouting(); app.UseCors(); app.UseAuthentication(); app.UseMultiTenancy(); app.UseAuthorization();- Add a temporary middleware to log the current tenant:
app.Use(async (ctx, next) => { var currentTenant = ctx.RequestServices.GetRequiredService<ICurrentTenant>(); var logger = ctx.RequestServices.GetRequiredService<ILogger<YourModule>>(); logger.LogInformation("TenantId={TenantId}, Name={TenantName}", currentTenant.Id, currentTenant.Name); await next(); });- If TenantId is null for authenticated tenant requests, tenant is not resolved:
- If you use header-based resolver, ensure the __tenant header reaches the service (reverse proxies like NGINX may drop headers with underscores unless enabled).
- If you use domain-based resolver, confirm domain mapping matches the current host.
- If you rely on ABP’s login “Switch Tenant” UI, confirm the token has tenantid claim and the middleware is placed correctly (see above).
B. Confirm entity and repository are multi-tenant aware
- Your aggregate/entities that should be stored per-tenant must implement IMultiTenant (have Guid? TenantId). If they don’t, ABP will not set TenantId nor filter/apply tenant-specific behavior, and may end up writing to host DB.
public class Product : AggregateRoot<Guid>, IMultiTenant { public Guid? TenantId { get; set; } public string Name { get; set; } }- In split schema/separate tenant DbContext scenarios, ensure mapping resides in the correct DbContext and multi-tenancy side is set:
- Main context: builder.SetMultiTenancySide(MultiTenancySides.Both)
- Tenant-only context: builder.SetMultiTenancySide(MultiTenancySides.Tenant)
- When seeding or running background jobs, always run inside a tenant scope:
using (CurrentTenant.Change(tenantId)) { // Use repositories/services here so they connect to the tenant DB }C. Validate the connection string switching mechanism
- Ensure the tenant has a connection string entry with Name = "Default" in SaasTenantConnectionStrings.
- For ABP v9+ SaaS module, the framework checks connection strings before saving/using them. If you store encrypted connection strings, replace the default IConnectionStringChecker to decrypt before checking:
[Dependency(ReplaceServices = true)] public class EncryptedConnectionStringChecker : SqlServerConnectionStringChecker, IConnectionStringChecker, ITransientDependency { private readonly IStringEncryptionService _enc; public EncryptedConnectionStringChecker(IStringEncryptionService enc) { _enc = enc; } public override async Task<AbpConnectionStringCheckResult> CheckAsync(string connectionString) { var plain = _enc.Decrypt(connectionString); return await base.CheckAsync(plain); } }- If you do not encrypt, ensure the plaintext “Default” connection string is valid and points to a different physical database. Test via the SaaS UI “Test connection” button (Pro) or by manually opening a DbConnection using the same string.
D. Ensure DbContext is not statically bound to Host connection
- Do not hardcode UseSqlServer/GetConnectionString in OnConfiguring that bypasses ABP’s IConnectionStringResolver.
- Use the standard ABP patterns:
- Your DbContext should inherit AbpDbContext<TDbContext>.
- Register EF Core with AddAbpDbContext and use options.Configure to map entities; do not set a fixed connection string there.
- If you manually override IConnectionStringResolver, ensure your resolver properly:
- Uses the current tenant context (ICurrentTenant.Id)
- Resolves “Default” by tenant first, then falls back to host
- Does not cache per-host result across tenants
E. Unit of Work and scope boundaries
- Confirm your application services/repositories execute inside a request scope with an active Unit of Work (default for ABP HTTP pipeline). External threads or early-resolved singletons may leak host-scoped DbContexts.
- For background workers, use IUnitOfWorkManager and CurrentTenant.Change(tenantId) around any repository usage.
F. Produce diagnostic logs to pinpoint the problem
- Enable EF Core connection logging to see which connection string is actually used:
Configure<AbpEfCoreOptions>(options => { options.Configure(context => { context.DbContextOptions.UseLoggerFactory(MyLoggerFactory) // with Microsoft.Extensions.Logging console/debug provider .EnableSensitiveDataLogging(); }); });- Add a small test endpoint to print what ABP resolves:
public class DebugService : ApplicationService { private readonly IConnectionStringResolver _resolver; public DebugService(IConnectionStringResolver resolver) { _resolver = resolver; } public async Task<string> GetConnAsync() { return await _resolver.ResolveAsync(ConnectionStrings.DefaultConnectionStringName); } }Call it as tenant; it must return the tenant DB connection string.
G. Common misconfigurations that cause “always Host DB”
- UseMultiTenancy middleware missing or placed before authentication.
- Entity not implementing IMultiTenant.
- Hardcoded connection string in DbContext configuration.
- Tenant not resolved due to proxy/header settings.
- Storing/encrypting connection strings without adapting IConnectionStringChecker or check flow (ABP v9+).
- Background process not wrapped in CurrentTenant.Change.
H. If you use “Separate Tenant Schema” template
- Keep tenant entities’ mapping in the base DbContext when you want hybrid behavior, or strictly in the TenantDbContext for tenant-only tables.
- Always add migrations for both main and tenant DbContexts and run DbMigrator so tenant DBs are up to date at runtime.
- Sources
- https://abp.io/docs/latest/framework/architecture/multi-tenancy
- https://abp.io/docs/latest/modules/saas
- https://github.com/abpframework/abp/blob/dev/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpDbContext.cs
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.
-
0
Hello, I need assistance with a multi-tenant issue that I can no longer resolve.
I reverted all recent changes and restored the project to the exact state it had when tenant-specific databases were working correctly. However, even after fully rolling back everything, the system still does not use the tenant databases.
The behavior is the following:
The system successfully creates the database for each tenant when the connection string is defined.
The connection strings appear correctly in the tables SaasTenants and SaasTenantConnectionStrings, including a valid “Default” entry for each tenant.
All entities implement IMultiTenant, and no DbContext contains hard-coded connection strings.
Despite this, ABP always writes and reads data from the Host database, completely ignoring the tenant’s “Default” connection string.
Tenant databases remain empty because they are never used.
This issue persists even after restoring the project to the original version where everything previously worked.
No matter which tenant I log in as, EF Core operations (insert/update/select) still target the Host DB.
In short:
ABP creates the tenant databases, but the application never switches to them — all data continues going to the Host DB even after reverting every change.
I would appreciate guidance on what could cause ABP to stop switching the connection string per tenant, even after a full rollback.
-
0
- Solution Here’s a precise, end-to-end checklist to identify why ABP no longer switches to the tenant database and always uses the Host DB, even after rollback. Follow and verify each step; one of these is almost always the cause.
A) Confirm tenant resolution and middleware order
- Middleware order must be:
- UseRouting
- UseCors (if any)
- UseAuthentication
- UseMultiTenancy
- UseAuthorization
- If UseMultiTenancy runs before UseAuthentication, ABP can’t read the tenant from the user’s token and ICurrentTenant stays null, so the Host connection is used. Example:
app.UseRouting(); app.UseCors(); app.UseAuthentication(); app.UseMultiTenancy(); app.UseAuthorization();- Log the resolved tenant at request time:
app.Use(async (ctx, next) => { var currentTenant = ctx.RequestServices.GetRequiredService<ICurrentTenant>(); var logger = ctx.RequestServices.GetRequiredService<ILogger<YourModule>>(); logger.LogInformation("CurrentTenant: Id={Id}, Name={Name}", currentTenant.Id, currentTenant.Name); await next(); });If you consistently see Id = null for authenticated tenant requests, tenant resolution is failing.
B) Validate tenant resolvers (domain/header/claim)
- If you rely on the __tenant header, ensure proxies do not strip underscores. For NGINX, set: underscores_in_headers on;
- If you use domain-based resolution, ensure the current host matches configured mappings.
- Tokens must carry the tenantid claim and authentication must run before UseMultiTenancy.
C) Prove the connection string resolver actually returns the tenant DB
- Add a quick diagnostic application service:
public class ConnDebugAppService : ApplicationService { private readonly IConnectionStringResolver _resolver; public ConnDebugAppService(IConnectionStringResolver resolver) => _resolver = resolver; public async Task<string> GetDefaultConnAsync() { return await _resolver.ResolveAsync(ConnectionStrings.DefaultConnectionStringName); } }- Call this as a tenant. If you still get the Host connection string, ABP’s connection resolution isn’t seeing the tenant context (CurrentTenant.Id is null), or resolution is overridden.
D) Ensure DbContexts are not pinning the Host connection
- Do NOT hardcode UseSqlServer/UseNpgsql/etc. globally in a way that applies to all DbContexts. Use the standard ABP pattern:
- DbContext inherits AbpDbContext<T>
- Register via AddAbpDbContext
- Configure provider through AbpDbContextOptions, never with a literal connection string
- If you have multiple DbContexts/providers, target configuration per context:
Configure<AbpDbContextOptions>(options => { options.Configure<MyMainDbContext>(opt => opt.UseSqlServer()); options.Configure<MyTenantDbContext>(opt => opt.UseSqlServer()); // or UseMySQL/UseNpgsql per context if you have multiple DBMS });- Remove any custom IConnectionStringResolver/DbContextFactory that bypasses ABP’s tenant-aware resolver.
E) Entities, repositories, and background processes
- All tenant-scoped entities must implement IMultiTenant (Guid? TenantId). You confirmed this—good.
- Background workers/seeding must run inside a tenant scope; otherwise, they use Host connection:
using (CurrentTenant.Change(tenantId)) { // repositories/services here }- Long-lived singletons caching DbContexts or repositories can “leak” the Host scope. Ensure DbContexts/repositories are used in request/unit-of-work scope only.
F) SaaS connection strings and ABP 9+/10+ validation
- Tenant must have a “Default” connection string in SaasTenantConnectionStrings.
- If you encrypt tenant connection strings, ABP’s connection check will fail unless you replace IConnectionStringChecker to decrypt before validate:
[Dependency(ReplaceServices = true)] public class EncryptedConnectionStringChecker : SqlServerConnectionStringChecker, IConnectionStringChecker, ITransientDependency { private readonly IStringEncryptionService _enc; public EncryptedConnectionStringChecker(IStringEncryptionService enc) => _enc = enc; public override async Task<AbpConnectionStringCheckResult> CheckAsync(string connectionString) { var plain = _enc.Decrypt(connectionString); return await base.CheckAsync(plain); } }If the framework thinks the string is invalid, it can silently fall back to Host DB behavior afterward.
G) Turn on DB/EF logging to see which connection is used
- Enable logging for EF Core:
Configure<AbpEfCoreOptions>(options => { options.Configure(ctx => { ctx.DbContextOptions .EnableDetailedErrors() .EnableSensitiveDataLogging(); }); });- Also add a DbConnection interceptor or log the connection string in DbContext.OnConfiguring to verify which connection is opened at runtime per request.
H) Cross-cutting checks that commonly cause “always Host DB”
- UseMultiTenancy order is wrong (most frequent root cause).
- Tenant not resolved due to proxy stripping __tenant header or misconfigured domain resolver.
- A custom IConnectionStringResolver or factory that ignores CurrentTenant.
- A global configuration or OnConfiguring that fixes a connection string.
- Encrypted tenant connection strings without replacing IConnectionStringChecker (ABP 9+).
- Work executed in background or outside a UnitOfWork without CurrentTenant.Change.
I) Quick “prove it” steps
- Call your test endpoint twice:
- As Host (no tenant) → expect Host connection string from GetDefaultConnAsync.
- As a Tenant (login/switch-tenant/domain/header) → must return the tenant DB connection string. If not, tenant resolution is broken; re-check middleware order and resolvers.
- Create a trivial controller action that logs ICurrentTenant.Id and the result of IConnectionStringResolver.ResolveAsync("Default") before doing any EF work. If Id is null or resolver returns Host string, fix resolution first; EF will follow automatically once CurrentTenant is correct.
- Sources
- https://abp.io/docs/latest/framework/architecture/multi-tenancy
- https://abp.io/docs/latest/modules/saas
- https://github.com/abpframework/abp/blob/dev/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpDbContext.cs
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.
-
0
hi
Data always saved in Host database
- You can check
ICurrentTenant.Idto see if the current tenant is correctly resolved. - Has your entity implemented the
IMultipleTenantinterface? - You can inject
IConnectionStringResolverand try toResolveAsyncand see the result after changing the current tenant.
Thanks.
- You can check