Hi,
We're implementing a database-per-tenant architecture where each tenant has its own SQL Server database with its own AbpBackgroundJobs table. We need our custom background job worker to poll each tenant's job queue independently.
Setup:
• ABP Commercial 5.x (EF Core, SQL Server) • Each tenant has a "Default" connection string configured via SaasTenantConnectionStrings • The tenant database contains an AbpBackgroundJobs table with waiting jobs (confirmed via SSMS)
────────────────────
Original Approach:
Our custom AsyncPeriodicBackgroundWorkerBase switches tenant context before polling:
using (_currentTenant.Change(tenantId)) { using var scope = serviceProvider.CreateScope(); var jobStore = scope.ServiceProvider.GetRequiredService<IBackgroundJobStore>();
var jobs = await jobStore.GetWaitingJobsAsync(null, 100);
// Returns 0 — despite waiting jobs existing in the tenant DB
}
We expected ICurrentTenant.Change(tenantId) to cause the EfCoreBackgroundJobStore to resolve the tenant's connection string (as it does for all our other DbContext queries), but it always queries the host database.
────────────────────
Diagnostic proof that the scope IS switching correctly:
using (_currentTenant.Change(tenantId)) { using var scope = serviceProvider.CreateScope(); var uowManager = scope.ServiceProvider.GetRequiredService<IUnitOfWorkManager>(); using var uow = uowManager.Begin(requiresNew: true, isTransactional: false);
// ✅ Connection string resolver returns tenant DB
var resolver = scope.ServiceProvider.GetRequiredService<IConnectionStringResolver>();
var connStr = await resolver.ResolveAsync(); // → "...Initial Catalog=db-tenant..."
// ✅ Our AppDbContext connects to tenant DB
var dbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
var actualDb = dbContext.Database.GetDbConnection().Database; // → "db-tenant"
// ✅ Direct EF query finds the job
var directJobs = await dbContext.BackgroundJobs
.Where(j => !j.IsAbandoned && j.NextTryTime <= now)
.ToListAsync(); // → Count: 1
// ❌ IBackgroundJobStore still returns 0
var jobStore = scope.ServiceProvider.GetRequiredService<IBackgroundJobStore>();
var storeJobs = await jobStore.GetWaitingJobsAsync(null, 100); // → Count: 0
}
This proves the tenant context switch works for our AppDbContext but IBackgroundJobStore ignores it entirely.
────────────────────
What we've tried (all unsuccessful):
- ReplaceDbContext — Added [ReplaceDbContext(typeof(IBackgroundJobsDbContext))] on our AppDbContext and registered options.ReplaceDbContext<IBackgroundJobsDbContext>() in the module. ❌
- AbpDbConnectionOptions remapping — Configured the "AbpBackgroundJobs" database to map to "Default" with IsUsedByTenants = true : Configure<AbpDbConnectionOptions>(options => { options.Databases.Configure("AbpBackgroundJobs", database => { database.MappedConnections.Add("Default"); database.IsUsedByTenants = true; }); });
3. IDataFilter suppression — Wrapped the call in using (_dataFilter.Disable<IMultiTenant>()) to bypass tenant filtering on the query. ❌ (This returned jobs from the host DB instead)
4. New UnitOfWork — Created a fresh scope + IUnitOfWorkManager.Begin(requiresNew: true) to force EF Core to re-evaluate the connection string. ❌
5. Named connection string on tenant — Added "AbpBackgroundJobs" as a named connection string on the tenant record (in addition to "Default"). ❌
────────────────────
Question:
Is there a supported way to make IBackgroundJobStore (specifically EfCoreBackgroundJobStore ) resolve to the tenant's database when ICurrentTenant is switched programmatically in a background worker context?
Or is the expected approach for DB-per-tenant job processing to bypass IBackgroundJobStore entirely and query the BackgroundJobRecord DbSet directly via our own DbContext (which does resolve correctly)?
Thank you.
3 Answer(s)
-
0
Hi,
This is by design, not something you can toggle. The background jobs store is host-only.
IBackgroundJobsDbContextis marked with[IgnoreMultiTenancy], and when the connection string is resolved for such a context, the framework forcesCurrentTenant.Change(null)before resolving:// UnitOfWorkDbContextProvider.ResolveConnectionStringAsync if (typeof(TDbContext).IsDefined(typeof(IgnoreMultiTenancyAttribute), false)) { using (CurrentTenant.Change(null)) // always host { return await ConnectionStringResolver.ResolveAsync(connectionStringName); } }EfCoreBackgroundJobRepositoryis bound toIBackgroundJobsDbContext, soTDbContexthere is that interface and it always carries[IgnoreMultiTenancy]. That's whyGetWaitingJobsAsyncalways hits the host DB even though your ownAppDbContextresolves to the tenant DB in the same scope.That's also why none of your five attempts changed anything — the connection string is resolved under the host tenant, before anything tenant-specific is looked at:
ReplaceDbContextdoes swap in your concrete DbContext, but the[IgnoreMultiTenancy]check runs against the repository's generic typeIBackgroundJobsDbContext, not against your replacement, so the connection string is still resolved as host.- The
AbpDbConnectionOptionsremapping and the namedAbpBackgroundJobsconnection string on the tenant are never consulted, because the current tenant is alreadynullat resolve time. BackgroundJobRecordisn'tIMultiTenant(there's noTenantIdcolumn), so theIDataFiltersuppression has nothing to disable — you just got host rows back.- A fresh unit of work doesn't help either; the host-forcing happens inside the DbContext provider regardless of the UoW.
So there's no supported way to make
IBackgroundJobStore/EfCoreBackgroundJobStoreresolve to the tenant database. For a DB-per-tenant setup where the jobs physically live in each tenant DB, your second option is the right one: queryBackgroundJobRecorddirectly through your own tenant-awareDbContext(the one that already resolves correctly):using (_currentTenant.Change(tenantId)) { using var scope = serviceProvider.CreateScope(); using var uow = scope.ServiceProvider .GetRequiredService<IUnitOfWorkManager>() .Begin(requiresNew: true, isTransactional: false); var dbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>(); var now = Clock.Now; var waitingJobs = await dbContext.BackgroundJobs .Where(j => !j.IsAbandoned && j.NextTryTime <= now) .OrderByDescending(j => j.Priority) .ThenBy(j => j.TryCount) .ThenBy(j => j.NextTryTime) .Take(100) .ToListAsync(); // process, then update TryCount / NextTryTime / IsAbandoned // through the same dbContext await uow.CompleteAsync(); }Keep the filter in sync with your ABP version's
EfCoreBackgroundJobRepository.GetWaitingListAsyncso the semantics match — depending on the version it may also filter onApplicationNameandCompletionTime, and those columns may not exist on older ones, so mirror exactly what your version has.One thing to keep in mind for the whole pipeline: enqueue and update go through the same host-only store, so
IBackgroundJobManager/IBackgroundJobStorewrite to the host DB too. If your jobs need to live in the tenant DB, insert and update them through this tenant-awareDbContextas well, not through the standard manager. And the built-inBackgroundJobWorkeronly polls the host store, so your own per-tenant worker is exactly the pattern to use here.Thanks
Markdown supported.Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post) -
0
Thanks for the detailed explanation. That makes sense now and it also explains why none of the approaches I tried had any effect.
We'll go ahead with the custom per-tenant worker and use our tenant-aware DbContext to manage the background jobs instead of the built-in store.
Just out of curiosity, is there any plan to support tenant-specific background job stores (for database-per-tenant scenarios) in a future ABP release?
Markdown supported.Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post) -
0
Hi,
Glad that cleared it up.
The built-in background job store is host-only by design, and there's no committed plan or release target for a tenant-specific / database-per-tenant store right now. If that changes it'll show up in the release notes.
For now, the custom per-tenant worker over your tenant-aware DbContext is the way to go.
Thanks
Markdown supported.Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)