Search code examples
asp.net-corehangfireasp.net-core-5.0

Hangfire dependency injection with ASP.NET core: inject different objects when processing a hangfire Job


I've got an asp.net 5 web app using standard .net Dependency Injection. As I understand it, out of the box Hangfire will use the same dependencies for instantiating jobs as MVC will for controllers etc. I'd like to inject a different dependency when instantiating jobs. How can I do this?

e.g. some classes have dependencies on IHttpContextAccessor so I want to provide an alternative for use within hangfire jobs that will get its state from serialized job parameters instead.

I see some discussion here of complex things that sounds like what I need ... but I'd love a simple example :-)


Solution

  • 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));
    
        }
    }