Open Closed

IBackgroundJobStore.GetWaitingJobsAsync does not respect tenant connection string in DB-per-tenant architecture #10783


User avatar
0
clodagh.oneill@allsop.software created

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&lt;IConnectionStringResolver&gt;();
var connStr = await resolver.ResolveAsync(); // → "...Initial Catalog=db-tenant..."

// ✅ Our AppDbContext connects to tenant DB
var dbContext = scope.ServiceProvider.GetRequiredService&lt;AppDbContext&gt;();
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 &lt;= now)
    .ToListAsync(); // → Count: 1

// ❌ IBackgroundJobStore still returns 0
var jobStore = scope.ServiceProvider.GetRequiredService&lt;IBackgroundJobStore&gt;();
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):

  1. ReplaceDbContext — Added  [ReplaceDbContext(typeof(IBackgroundJobsDbContext))]  on our  AppDbContext  and registered  options.ReplaceDbContext<IBackgroundJobsDbContext>()  in the module. ❌
  2. 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.
Markdown supported.
Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)

3 Answer(s)
  • User Avatar
    0
    maliming created
    Support Team Fullstack Developer

    Hi,

    This is by design, not something you can toggle. The background jobs store is host-only.

    IBackgroundJobsDbContext is marked with [IgnoreMultiTenancy], and when the connection string is resolved for such a context, the framework forces CurrentTenant.Change(null) before resolving:

    // UnitOfWorkDbContextProvider.ResolveConnectionStringAsync
    if (typeof(TDbContext).IsDefined(typeof(IgnoreMultiTenancyAttribute), false))
    {
        using (CurrentTenant.Change(null)) // always host
        {
            return await ConnectionStringResolver.ResolveAsync(connectionStringName);
        }
    }
    

    EfCoreBackgroundJobRepository is bound to IBackgroundJobsDbContext, so TDbContext here is that interface and it always carries [IgnoreMultiTenancy]. That's why GetWaitingJobsAsync always hits the host DB even though your own AppDbContext resolves 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:

    • ReplaceDbContext does swap in your concrete DbContext, but the [IgnoreMultiTenancy] check runs against the repository's generic type IBackgroundJobsDbContext, not against your replacement, so the connection string is still resolved as host.
    • The AbpDbConnectionOptions remapping and the named AbpBackgroundJobs connection string on the tenant are never consulted, because the current tenant is already null at resolve time.
    • BackgroundJobRecord isn't IMultiTenant (there's no TenantId column), so the IDataFilter suppression 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 / EfCoreBackgroundJobStore resolve 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: query BackgroundJobRecord directly through your own tenant-aware DbContext (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.GetWaitingListAsync so the semantics match — depending on the version it may also filter on ApplicationName and CompletionTime, 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 / IBackgroundJobStore write to the host DB too. If your jobs need to live in the tenant DB, insert and update them through this tenant-aware DbContext as well, not through the standard manager. And the built-in BackgroundJobWorker only 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)
  • User Avatar
    0
    clodagh.oneill@allsop.software created

    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)
  • User Avatar
    0
    maliming created
    Support Team Fullstack Developer

    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)
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 September 28, 2026, 11:44
1
ABP Assistant
🔐 You need to be logged in to use the chatbot. Please log in first.