JobActivator with ASP.NET Core DI

Hello,

I’m trying to implement a custom JobActivator with the default ASP.NET Core 5.0 DI, because when a specific function is running with Hangfire, I want it to use another implementation of an interface. So I created my custom implementation, which looks like this:

 public class HangfireJobActivator : JobActivator
{
    private readonly IServiceProvider serviceProvider;

    public HangfireJobActivator(IServiceProvider serviceProvider)
    {
        this.serviceProvider = serviceProvider;
    }

    public override object ActivateJob(Type jobType)
    {
        if (jobType == typeof(IUserProvider<Guid>))
            return new TestUserProvider();
        else
            return serviceProvider.GetService(jobType);
    }
}

But when the ActivateJob-method get’s called by Hangfire, it’ll throw an exception because the serviceProvider is already disposed. How can I fix this?

Have you been able to confirm that Hangfire honors .UseActivator(new HangfireJobActivator()) ? I’ve tried to introduce a new activator implementation and I’m not able to get it to work.

According to this post the AspNetCore implementation doesn’t allow a custom activator in any circumstance.

@nickalbrecht or @pieceofsummer Can either of you confirm this is still the case?

I fixed the issue by creating a seperate service provider for the job activator:

public class HangfireJobActivator : JobActivator
{
    private readonly IServiceProvider serviceProvider;

    /// <summary>
    /// Initializes a new instance of the <see cref="HangfireJobActivator"/> class.
    /// </summary>
    /// <param name="serviceCollection">The service collection.</param>
    public HangfireJobActivator(IServiceCollection serviceCollection)
    {
        serviceCollection.AddScoped<IUserProvider<Guid>, BackgroundUserProvider>();
        serviceProvider = serviceCollection.BuildServiceProvider();
    }

    /// <summary>
    /// Activates the job.
    /// </summary>
    /// <param name="jobType">Type of the job.</param>
    /// <returns>object</returns>
    public override object ActivateJob(Type jobType)
    {
        return serviceProvider.GetService(jobType);
    }
}

That makes sense, but how are you adding types to your IServiceCollection.

I’m using the Core builds v1.7.18 and I cannot get the custom JobActivator used, it seems to be hard coded to the AspNetCoreJobActivator. Can you confirm your version?

I’m using the same service collection as the one ASP.NET Core uses. You have to specify the custom JobActivator that Hangfire should use. Both these things can be done in the Startup class of your application. I’ve included an example Startup class, where I removed all the code that’s not Hangfire related.

Please let me know if you have any more questions!

/// <summary>
/// Startup class
/// </summary>
public class Startup
{
    private IServiceCollection serviceCollection;

    /// <summary>
    /// Gets the configuration.
    /// </summary>
    /// <value>
    /// The configuration.
    /// </value>
    public IConfiguration Configuration { get; }

    /// <summary>
    /// Initializes a new instance of the <see cref="Startup"/> class.
    /// </summary>
    /// <param name="configuration">The configuration.</param>
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    /// <summary>
    /// Configures the services.
    /// This method gets called by the runtime. Use this method to add services to the container.
    /// </summary>
    /// <param name="services">The services.</param>
    public void ConfigureServices(IServiceCollection services)
    {
        string databaseConnectionString = Configuration.GetConnectionString("LunaDatabase");

        services.AddHangfire(options => options.UsePostgreSqlStorage(databaseConnectionString));

        serviceCollection = services;
    }

    /// <summary>
    /// Configures the specified application.
    /// </summary>
    /// <param name="app">The application.</param>
    /// <param name="env">The env.</param>
    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        // hangfire implementation
        GlobalConfiguration.Configuration.UseActivator(new HangfireJobActivator(serviceCollection));

        app.UseHangfireServer();
    }
}
1 Like

I added a scoped provider in ActiveJob, with my naive understanding that it should dispose of the job once it’s out of scope (e.g. finished). Admittedly I’m not sure if this optimization is correct, perhaps Hangfire does this work behind the scenes.

public override object ActivateJob(Type jobType)
{
    using var scope = serviceProvider.CreateScope();
    return scope.ServiceProvider.GetRequiredService(jobType);
    //return serviceProvider.GetRequiredService(jobType);
}

If you check out the source code of the AspNetCoreJobActivator, it seems that Hangfire does the same thing behind the scenes as you. To confirm this just remove or disable your custom JobActivator (Hangfire will default to the AspNetCoreJobActivator), then run your Hangfire jobs. If they all succeed without exceptions, you won’t need your own implementation.

Thank you very much. Your response led me to investigate further. Because I was using a custom JobActivator, I was bypassing the AspNetCoreJobActivator altogether.

public class ContainerActivator : JobActivator

What I did now was to inherit from AspNetCoreJobActivator

public class ContainerActivator : AspNetCoreJobActivator

here in the constructor, I am dynamically loading types from folders. So far it seems to be working.

And now it seems silly to use a custom JobActivator at all, since it was only to register types in the DI container. Probably can just do this in Startup.cs.

public ContainerActivator(IServiceScopeFactory serviceScopeFactory, IConfiguration config, IServiceCollection services)
    : base(serviceScopeFactory)
{
    var section = config.GetSection("Hangfire:JobPaths");
    var folders = section.Get<string[]>();

    foreach (var s in folders)
    {
        var dir = Path.GetDirectoryName(s);
        var src = Path.GetFileName(s) ?? "*.dll";

        var files = Directory.GetFiles(dir, src);

        foreach (var f in files)
        {
            var asm = Assembly.LoadFrom(f);
            var types = asm.DefinedTypes;

            foreach (var t in types)
            { 
                services.AddTransient(t);
            }
        }
    }
}