I ended up not using dependency injection to achieve this different behaviour. Instead I changed the classes that use IHttpContentAccessor
to alternatively derive the 'tenant' from state set within my Hangfire job methods.
- In my job methods I first set the tenant in a 'Scoped' object, based on a parameter to the job method
- In the class that uses
IHttpContentAccessor
to get info from the current request I first look if there is a current request to get tenant info, and if not I check for that scoped object that's only set during hangfire jobs.
- In my job methods I don't use constructor dependency injection. Instead I use the service locator (anti)-pattern within the job method. This means I can set the tenant state first before asking for objects that are depended upon it.
Some example code:
// A service for getting current Tenant info
public class TenantAccessor : ITenantAccessor
{
private readonly IHttpContextAccessor _httpContextAccessor;
public TenantAccessor(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}
// Here's a method I call from everywhere in my system
// when I want the current domain name, which for me
// identifies the tenants since they access it at
// https://customername.myapplication.com.
// Nowhere else uses _httpContextAccessor since that
// won't work if called from within a Hangfire job.
public string GetTenantDomain()
{
// If there's an http context then use it:
if (_httpContextAccessor.HttpContext != null)
return _httpContextAccessor.HttpContext.Request.Host.Host;
// Otherwise return this string value, if set
return _hangfireTenantInfo.TenantDomain;
}
}
public class MyHangfireJobs
{
// All the methods I call from Hangfire look similar to this:
public async Task DoStuffInBackground(
string tenantDomain, //
string someOtherParameter)
{
// First set this string value so other services
// can get the tenant's domain.
var hangfireTenantInfo = _serviceProvider.GetRequiredService<HangfireTenantInfo>();
hangfireTenantInfo.TenantDomain = tenantDomain;
// Now all the normal code in the method
// Some of this code will call services that use
// TenantAccessor.GetTenantDomain()
...
// In these job methods I use GetRequiredService() instead
// of constructor injection, so creation of those
// services happens after setting TenantDomain, e.g.:
var someService = _serviceProvider.GetRequiredService<SomeImportantService>();
someService.DoTheStuff();
}
}
// Just a class that wraps a string variable
public class HangfireTenantInfo
{
public string TenantDomain { get; set; }
}
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
...
// Somewhere in my startup service registration code:
// Register these as scoped so there's one per request.
services.AddScoped<HangfireTenantInfo>();
services.AddScoped<ITenantAccessor, TenantAccessor>();
...
}
...
}
[ApiController]
[Route("api/blah")]
public class BlahController : ControllerBase
{
readonly ITenantAccessor _tenantAccessor;
readonly IBackgroundJobClient _backgroundJobClient;
// Nothing special here: constructor injection for services.
public BlahController(ITenantAccessor tenantAccessor,
IBackgroundJobClient backgroundJobClient)
{
_tenantAccessor = tenantAccessor;
_backgroundJobClient = backgroundJobClient;
}
// This is what a controller method might look like that
// runs background hangfire jobs
[HttpPost("do/stuff/{aParam}")]
public Task DoStuff(string aParam)
{
// Get the current request's Host (which will come
// from httpContext since we're within a request).
var currentDomain = _tenantAccessor.GetTenantDomain();
// Run a background job, passing in tenant's domain
_backgroundJobClient.Enqueue<MyHangfireJobs>(x =>
x.DoStuffInBackground(currentDomain, aParam));
}
}