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

1

1

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 :-)

Lollop answered 13/3, 2021 at 20:12 Comment(3)
You want to use a provider or factory pattern. Take a look at this answer. https://mcmap.net/q/73164/-how-to-register-multiple-implementations-of-the-same-interface-in-asp-net-coreSkvorak
I would say that you should avoid using the web-context-oriented IHttpContextAccessor out of the direct request processing (out of the controllers). Your controller should build a Poco to pass to the business handling class, which would be the same parameter passed when called through hangfire. Anyway, if changing the business code is not a viable option, I would try decorating the IHttpContextAccessor, providing my own implementation when the decorated accessor has no httpcontext to provide. andrewlock.net/…Venturous
Depending on what you want to pass as context information from IHttpContextAccessor you may also use filters https://mcmap.net/q/735592/-hangfire-multi-tenant-asp-net-core-resolving-the-correct-tenantVenturous
L
1

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

    }
}
Lollop answered 28/6, 2021 at 19:54 Comment(2)
could you please share some sample implementation?Berndt
My actual code is full of other business logic but I tried to extract the important bits @BerndtLollop

© 2022 - 2024 — McMap. All rights reserved.