Hangfire - Multi Tenant, ASP.NET Core - Resolving the Correct Tenant

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

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.

Multi-tenant ASP.NET Core Web Application, how to restrict multiple tabs to the same tenant?

I ended up using SignalR to achieve my goal. Overkill but found it interesting and some added benefits.

When the tenant is changed, I set the cookie value, then call a server method using signalr to broadcast to a group. This triggers a method on the clients within the group to redirect to the home page.

The groups are based on the username which is unique. So if someone duplicates a new tab or window the connection is stored against the group.

It's not perfect but seems to work well.

Hangfire per-job correlationId/state

Thanks to jbl's comment I looked at what I was doing again and managed to get it working through a kludge.

I've got the transient state holder
(basically it's the HttpContextAccessor class renamed):

public class StateHolder
{
private static AsyncLocal<ContextHolder> _contextCurrent = new AsyncLocal<ContextHolder>();

public string State {
get {
return _contextCurrent.Value?.Context;
}
set {
var holder = _contextCurrent.Value;
if (holder != null)
{
holder.Context = null;
}

if (value != null)
{
_contextCurrent.Value = new ContextHolder { Context = value };
}
}
}

private class ContextHolder
{
public string Context;
}
}

and then in Hangfire I hook it up to the activation with

public class LoggingActivator : JobActivator
{
private readonly IServiceScopeFactory _serviceScopeFactory;
private readonly ContextAccessor _contextAccessor;

public LoggingActivator([NotNull] IServiceScopeFactory serviceScopeFactory, ContextAccessor contextAccessor)
{
_serviceScopeFactory = serviceScopeFactory ?? throw new ArgumentNullException(nameof(serviceScopeFactory));
_contextAccessor = contextAccessor;
}

public override JobActivatorScope BeginScope(JobActivatorContext context)
{
return new LoggingActivatorScope(_serviceScopeFactory.CreateScope(), _contextAccessor);
}
}

and

public class LoggingActivatorScope : JobActivatorScope
{
private readonly IServiceScope _serviceScope;
private readonly ContextAccessor _contextAccessor;

public LoggingActivatorScope(
[NotNull] IServiceScope serviceScope,
ContextAccessor contextAccessor)
{
_serviceScope = serviceScope ?? throw new ArgumentNullException(nameof(serviceScope));
_contextAccessor = contextAccessor;
}

public override object Resolve(Type type)
{
_contextAccessor.Context = Guid.NewGuid().ToString();

return ActivatorUtilities.GetServiceOrCreateInstance(_serviceScope.ServiceProvider, type);
}

public override void DisposeScope()
{
_serviceScope.Dispose();
}
}

That seems to work fine.

How to get a Hangfire Job Parameter into the method that is executed by that job

You may try implementing your HfTenantProvider like this :

public class HfTenantProvider : IHfTenantProvider
{
private static System.Threading.AsyncLocal<string> HfTenantCode { get; } = new System.Threading.AsyncLocal<string>();

public void HfSetTenant(string TenantCode)
{
HfTenantCode.Value = TenantCode;
}

public string HfGetTenant()
{
return HfTenantCode.Value;
}
}

I agree that this makes the scoping of the HfTenantProvider somewhat useless.
Also, you may call HfSetTenant(null) in the OnPreformed method of your server filter. I think this is cleaner, even though this should not make big difference.



Related Topics



Leave a reply



Submit