Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Hangfire - Multi tenant, ASP.NET Core - Resolving the correct tenant

I got a SaaS project that needs the use Hangfire. We already implemented the requirements to identify a tenant.

Architecture

  • Persistence Layer
    • Each tenant has it's own database
  • .NET Core
    • We already have a service TenantCurrentService which returns the ID of the tenant, from a list of source [hostname, query string, etc]
    • We already have a DbContextFactory for Entity Framework which return a DB context with the correct connection string for the client
    • We are currently using ASP.NET Core DI (willing to change if that helps)
  • Hangfire
    • Using single storage (eg: Postgresql), no matter the tenant count
    • Execute the job in an appropriate Container/ServiceCollection, so we retrieve the right database, right settings, etc.

The problem

I'm trying to stamp a TenantId to a job, retrieved from TenantCurrentService (which is a Scoped service).

When the job then gets executed, we need to retrieve the TenantId from the Job and store it in HangfireContext, so then the TenantCurrentService knows the TenantId retrieved from Hangfire. And from there, our application layer will be able to connect to the right database from our DbContextFactory

Current state

  • Currently, we have been able to store tenantId retrieved from our Service using a IClientFilter.
  • How can I retrieve my current ASP.NET Core DI ServiceScope from IServerFilter (which is responsible to retrieve the saved Job Parameters), so I can call .GetRequiredService().IdentifyTenant(tenantId)

Is there any good article regarding this matter / or any tips that you guys can provide?

like image 677
Dekim Avatar asked Aug 07 '19 12:08

Dekim


1 Answers

First, you need to be able to set the TenantId in your TenantCurrentService. Then, you can rely on filters :

client side (where you enqueue jobs)

public class ClientTenantFilter : IClientFilter
{
        public void OnCreating(CreatingContext filterContext)
        {
           if (filterContext == null) throw new ArgumentNullException(nameof(filterContext));

            filterContext.SetJobParameter("TenantId", TenantCurrentService.TenantId);
        }
}

and server side (where the job is dequeued).

public class ServerTenantFilter : IServerFilter
{
    public void OnPerforming(PerformingContext filterContext)
    {
      if (filterContext == null) throw new ArgumentNullException(nameof(filterContext));

      var tenantId = filterContext.GetJobParameter<string>("TenantId");
      TenantCurrentService.TenantId = tenantId;
    }
}

The server filter can be declared when you configure your server through an IJobFilterProvider:

        var options = new BackgroundJobServerOptions
        {
            Queues = ...,
            FilterProvider = new ServerFilterProvider()
        };
        app.UseHangfireServer(storage, options, ...);

where ServerFilterProvider is :

public class ServerFilterProvider : IJobFilterProvider
{
    public IEnumerable<JobFilter> GetFilters(Job job)
    {
        return new JobFilter[]
                   {
                       new JobFilter(new CaptureCultureAttribute(), JobFilterScope.Global, null),
                       new JobFilter(new ServerTenantFilter (), JobFilterScope.Global,  null),
                   };
    }
}

The client filter can be declared when you instantiate a BackgroundJobClient

var client = new BackgroundJobClient(storage, new BackgroundJobFactory(new ClientFilterProvider());

where ClientFilterProvider behaves as ServerFilterProvider, delivering client filter

A difficulty may be to have the TenantCurrentService available in the filters. I guess this should be achievable by injecting factories in the FilterProviders and chain it to the filters.

I hope this will help.

like image 116
jbl Avatar answered Oct 06 '22 01:10

jbl