Open Closed

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


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 v10.3.0 (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)

2 Answer(s)
  • User Avatar
    0
    AI-Bot created
    Support Team ABP Agent
    • IBackgroundJobStore (EF Core implementation) uses the Background Jobs module’s own DbContext / connection name (by default AbpBackgroundJobs) and it is not your AppDbContext. So the fact that AppDbContext resolves the tenant DB correctly does not guarantee that EfCoreBackgroundJobStore will, unless the Background Jobs module is also configured for tenant-specific connection resolution and migrations in the same way.
    • In a DB-per-tenant setup, the supported/expected way is:
      1. ensure the Background Jobs module is installed/migrated into each tenant database (so each tenant really has its own AbpBackgroundJobs table), and
      2. run job polling/execution per tenant by switching tenant with ICurrentTenant.Change(...) (your approach is conceptually correct), but only if the Background Jobs module’s DbContext is actually tenant-aware and mapped to the tenant connection string.

    What to do (supported approach)

    1. Make sure the Background Jobs module uses the tenant DB connection

      • Ensure the tenant has a connection string for the connection name that the Background Jobs module uses (AbpBackgroundJobs by default), or ensure AbpBackgroundJobs is mapped to Default for tenants (your attempt #2 is the correct direction conceptually).
      • Then ensure your tenant DB migration process actually creates the Background Jobs tables in the tenant DB for that connection name.
    2. Poll inside a Unit of Work after switching tenant Even though you tried it, the safe pattern is:

    using (_currentTenant.Change(tenantId))
       {
           using var scope = _serviceProvider.CreateScope();
    
           var uowManager = scope.ServiceProvider.GetRequiredService<IUnitOfWorkManager>();
           using var uow = uowManager.Begin(requiresNew: true, isTransactional: false);
    
           var jobStore = scope.ServiceProvider.GetRequiredService<IBackgroundJobStore>();
           var jobs = await jobStore.GetWaitingJobsAsync(null, 100);
    
           await uow.CompleteAsync();
       }
    

    If this still reads from host DB, it strongly indicates the Background Jobs module is still resolving its own connection as host (i.e., its DbContext is not using the tenant-mapped connection in your setup), even though Default resolves fine for your AppDbContext.

    1. If you need strict DB-per-tenant job queues, query via your tenant DbContext is acceptable Given your proof that direct querying through your tenant-resolving AppDbContext returns the expected rows, and IBackgroundJobStore continues to hit host DB, the practical approach in DB-per-tenant systems is to bypass IBackgroundJobStore and query/process BackgroundJobRecord from the tenant DbContext that you know is correctly wired to tenant connection strings.

      This aligns with the general ABP guidance that ICurrentTenant.Change(...) is the mechanism to operate per-tenant in background workers, and that “disable tenant filter” only helps in shared-db scenarios (not DB-per-tenant).

    Notes on what won’t help here

    • Disable<IMultiTenant>() will not make DB-per-tenant work; it only changes filtering, not which database you connect to.
    • Replacing IBackgroundJobsDbContext with your AppDbContext typically won’t help unless the Background Jobs EF Core module’s DbContext/connection-name mapping is fully aligned with your tenant DB strategy.

    Sources

    • https://abp.io/docs/latest/framework/fundamentals/connection-strings (validated)
    • https://abp.io/support/questions/2007 (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.

    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,

    This is by design, and it's actually two things stacking up, which is why none of your five attempts moved the needle:

    1. The background jobs DbContext is marked [IgnoreMultiTenancy], and BackgroundJobRecord isn't a multi-tenant entity (no TenantId). The module is meant to live in the host database.
    2. On top of that, ABP's EF Core repository treats any non-multi-tenant entity as host data. When it resolves the DbContext, it temporarily switches ICurrentTenant to null for entities that aren't IMultiTenant. BackgroundJobRecord isn't, so the connection string is pinned to host regardless of what tenant you switched to.

    Because of point 2, ReplaceDbContext can't help — even if you point the store at a tenant-aware context, the repository still forces the tenant to null for this entity type. And the connection-string mapping / named-connection attempts never get consulted, since the tenant is already null by the time resolution runs.

    To answer your last question: yes, for DB-per-tenant job queues you route this through a tenant-aware DbContext and bypass the host-pinning. The cleanest way is to replace IBackgroundJobRepository with one bound to your AppDbContext (which already resolves the tenant connection and has the table mapped), and override GetDbContextAsync so it stops resetting the tenant:

    [Dependency(ReplaceServices = true)]
    [ExposeServices(typeof(IBackgroundJobRepository))]
    public class TenantAwareBackgroundJobRepository
        : EfCoreRepository<AppDbContext, BackgroundJobRecord, Guid>, IBackgroundJobRepository
    {
        private readonly IDbContextProvider<AppDbContext> _dbContextProvider;
    
        protected IClock Clock { get; }
    
        public TenantAwareBackgroundJobRepository(
            IDbContextProvider<AppDbContext> dbContextProvider,
            IClock clock)
            : base(dbContextProvider)
        {
            _dbContextProvider = dbContextProvider;
            Clock = clock;
        }
    
        // BackgroundJobRecord is not IMultiTenant, so the base repository would switch the
        // current tenant to null and read from the host DB. Keep the current tenant instead
        // so the connection string follows it.
        protected override Task<AppDbContext> GetDbContextAsync()
        {
            return _dbContextProvider.GetDbContextAsync();
        }
    
        public virtual async Task<List<BackgroundJobRecord>> GetWaitingListAsync(
            string applicationName,
            int maxResultCount,
            CancellationToken cancellationToken = default)
        {
            var now = Clock.Now;
            return await (await GetDbSetAsync())
                .Where(t => t.ApplicationName == applicationName)
                .Where(t => !t.IsAbandoned && t.NextTryTime <= now)
                .OrderByDescending(t => t.Priority)
                .ThenBy(t => t.TryCount)
                .ThenBy(t => t.NextTryTime)
                .Take(maxResultCount)
                .ToListAsync(GetCancellationToken(cancellationToken));
        }
    }
    

    The built-in BackgroundJobStore just delegates to IBackgroundJobRepository, so once this replacement is in place, IBackgroundJobStore.GetWaitingJobsAsync reads from the tenant DB when you switch tenant in your worker. No new DbContext and no extra migration — you're reusing the per-tenant table you already have.

    One thing to keep in mind: this makes the whole store tenant-aware, so enqueue/update/delete follow the current tenant too. Jobs added while a tenant is current get written to that tenant's DB, which is what you want for per-tenant queues. The built-in BackgroundJobWorker still runs as host and only picks up host jobs, so polling each tenant DB stays the job of your own worker that switches ICurrentTenant — which is exactly what you're already doing.

    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.