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
Does Garbage Collector Call Dispose()
Possible Pitfalls of Using This (Extension Method Based) Shorthand
JSON.Net Serialize Directly from Oledbconnection
Dependency Injection Type-Selection
Startup.Cs in a Self-Hosted .Net Core Console Application
Deciding on When to Use Xmldocument VS Xmlreader
How to Set Largeaddressaware from Within Visual Studio
Recommendations on Parsing .Eml Files in C#
C# Wait for User to Finish Typing in a Text Box
Why Can't I Declare C# Methods Virtual and Static
C# Regular Expressions, String Between Single Quotes
Creating Custom Picturebox with Draggable and Resizable Selection Window
Datagrid Shows Path of Image Instead of Image Itself