I have installed Hangfire in an ASP.Net Core 5 web application. I followed the Getting Started guide on the Hangfire website and the initial installation and configuration were quick and it works.
As it is a multitenant application (DB per tenant) I need to be able to connect to the right database when the job is processed on the server.
I came across this well explained post which is exactly what I need as far as I can tell but I can't figure out the last step which is how to get the value from the job parameter (the tenant identification) into the method that Hangfire is executing.
I have created the Client filter
#region Hangfire Job Filters
#region Client filters
public class HfClientTenantFilter : IClientFilter
{
private readonly IMultiTenant _multiTenant;
public HfClientTenantFilter(IMultiTenant multiTenant)
{
_multiTenant = multiTenant;
}
public async void OnCreating(CreatingContext filterContext)
{
if (filterContext == null) throw new ArgumentNullException(nameof(filterContext));
MdxTenantConfig tenantConfig = await _multiTenant.GetTenantConfig();
filterContext.SetJobParameter("TenantId", tenantConfig.Id);
}
public void OnCreated(CreatedContext filterContext)
{
if (filterContext == null) throw new ArgumentNullException(nameof(filterContext));
}
}
public class HfClientFilterProvider : IJobFilterProvider
{
private readonly IMultiTenant _multiTenant;
public HfClientFilterProvider(IMultiTenant multiTenant) {
_multiTenant = multiTenant;
}
public IEnumerable<JobFilter> GetFilters(Job job)
{
return new JobFilter[]
{
new JobFilter(new CaptureCultureAttribute(), JobFilterScope.Global, null),
new JobFilter(new HfClientTenantFilter(_multiTenant),JobFilterScope.Global, null)
};
}
}
#endregion Client filters
and Server filter:
#region Server filters
public class HfServerTenantFilter : IServerFilter
{
private readonly IHfTenantProvider _hfTenantProvider;
public HfServerTenantFilter(IHfTenantProvider hfTenantProvider)
{
_hfTenantProvider = hfTenantProvider;
}
public void OnPerforming(PerformingContext filterContext)
{
if (filterContext == null) throw new ArgumentNullException(nameof(filterContext));
var tenantId = filterContext.GetJobParameter<string>("TenantId");
// need to get the tenantId passed to the method that calls the creation of the DbContext
_hfTenantProvider.HfSetTenant(tenantId);
}
public void OnPerformed(PerformedContext filterContext)
{
if (filterContext == null) throw new ArgumentNullException(nameof(filterContext));
}
}
public class HfServerFilterProvider : IJobFilterProvider
{
private readonly IHfTenantProvider _hfTenantProvider;
public HfServerFilterProvider(IHfTenantProvider hfTenantProvider)
{
_hfTenantProvider = hfTenantProvider;
}
public IEnumerable<JobFilter> GetFilters(Job job)
{
return new JobFilter[]
{
new JobFilter(new CaptureCultureAttribute(), JobFilterScope.Global, null),
new JobFilter(new HfServerTenantFilter(_hfTenantProvider), JobFilterScope.Global, null),
};
}
}
#endregion Server filters
#endregion Hangfire Job Filters
Attaching the Server filter in Startup
services.AddHangfireServer(opt => {
opt.FilterProvider = new HfServerFilterProvider(new HfTenantProvider());
});
When executing in debug I see that the parameter TenantID
is correctly set in the client filter and also correctly retrieved by the server filter.
I am now trying to make that value available to the method that is being executed but hasn't succeeded yet.
I tried using a scoped service:
#region Scoped Tenant provider service
public interface IHfTenantProvider {
void HfSetTenant(string TenantCode);
string HfGetTenant();
}
public class HfTenantProvider: IHfTenantProvider
{
public string HfTenantCode;
public void HfSetTenant(string TenantCode)
{
HfTenantCode = TenantCode;
}
public string HfGetTenant()
{
return HfTenantCode;
}
}
#endregion Scoped Tenant provider service
and in Startup:
services.AddScoped<IHfTenantProvider, HfTenantProvider>();
In the OnPerforming
method of the Server filter, I set the value in the Scoped service as retrieved from the Job Parameter (see above for full code) which is working as I can see in debugging.
_hfTenantProvider.HfSetTenant(tenantId);
This is the test method being scheduled:
public bool HfTest()
{
try
{
BackgroundJobClient jobClient = GetJobClient();
jobClient.Enqueue<MdxMetaCRUD>(crud => crud.HfTest());
}
catch (Exception)
{
return false;
}
return true;
}
and this is the method itself where I retrieve the value from the Scoped service which is, however null
:
public async Task HfTest()
{
string tenant =_hfTenantProvider.HfGetTenant();
using (var _dbContext = _contextHelper.CreateContext(true, tenant))
{
Entity entity = new()
{
Name = "HFtest",
Description = tenant,
DefaultAuditTrackRetention = 1
};
await _dbContext.Entities.AddAsync(entity);
}
}
The ContextHelper returns a new DbContext:
public ApplicationDbContext CreateContext(bool backgroundProcess, string backgroundTenant)
{
return new ApplicationDbContext(_multiTenant, backgroundProcess, backgroundTenant);
}
The DbContext has an override to retrieve the connection string for the tenant (which is working fine in the user context). In case of a Job being executed by Hangfire, I am trying to pass the TenantId
into _backgroundTenant
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
try
{
MdxTenantConfig tenantConfig = Task.Run(() => _multiTenant.GetTenantConfig(_backgroundProcess, _backgroundTenant)).Result;
optionsBuilder.UseSqlServer(tenantConfig.ConnectionString);
base.OnConfiguring(optionsBuilder);
}
catch //(Exception)
{
throw;
}
}
I was hoping that the Job being launched (Server filters being retrieved) and the Job being executed were in the same scope (request) but it seems not.
Am I doing something wrong with the scoped service or is my approach wrong? I saw some articles referring to a "factory" but that is beyond my knowledge (for the moment).