Recommended scoping for dependency-injected recurring job classes

Is there a recommended scoping for classes that will be created by Hangfire?

Consider the following code.

// Configure Hangfire
IHost host = Host.CreateDefaultBuilder(args)
    .ConfigureServices((context, services) =>
    {
        services.AddHangfire(configuration => configuration
            .UseRecommendedSerializerSettings()
            .UseSqlServerStorage(context.Configuration.GetConnectionString("HangfireConnection")));
        services.AddHangfireServer();

        services.AddSingleton<IEdiImporterWorkerService, EdiImporterWorkerService>();
        services.AddSingleton<IEtaBuilderWorkerService, EtaBuilderWorkerService>();
        services.AddSingleton<IReportBuilderWorkerService, ReportBuilderWorkerService>();
    })
    .UseWindowsService()
    .Build();

  // Add recurring jobs
  jobManager.AddOrUpdate<IEdiImporterWorkerService>("Test 1", x => x.RunAsync(), "*/1 * * * *");
  jobManager.AddOrUpdate<IEtaBuilderWorkerService>("Test 2", x => x.RunAsync(), "*/2 * * * *");
  jobManager.AddOrUpdate<IReportBuilderWorkerService>("Test 3", x => x.RunAsync(), "*/3 * * * *");

Here, I’m using AddSingleton(). However, these classes could allocate a lot of memory. And I would prefer to use AddScoped(). But I’m not really clear what AddScoped() does, since there are no requests other than those coming from Hangfire.

Is one recommended over the other?

Singleton means there is only one instance in the entire application. These should generally be thread safe as they could be accessed concurrently. Scoped would be an instance per job.

Singleton can be tricky and is usually reserved for items that can take a while to setup/startup (relatively speaking) like database setup/factory. Scoped would be for most things like an open database connection so jobs aren’t stepping on each other.

In my experience, anything the job references would typically be scoped. Singleton would usually be used within another DI setup, not directly in a job.

Since your job and your injected item are the same, I can’t really speak to what kind of experience you’ll have.

Thanks for responding.

Yes, I do understand what AddSingleton() does. I just wasn’t sure which approach makes the most sense with Hangfire.

Also, the difference between AddScoped() and AddTransient() isn’t clear in the context of Hangfire. AddScoped() means one instance per request. But with Hangfire, there isn’t really a request.

At this point, I’m leaning towards using AddTransient() to prevent unneeded data from being held in memory.

The lifetime is defined by the implementor, in this case Hangfire. I believe the request would be the job. I use autofac and had to specify to disable tagged lifetime scope as I was getting weird results.

The issue with transient is it will create a new instance each time. There could be multiple instances for a single job. If you have A and B in your job constructor and B also makes use of A, they will have different instances.

Hangfire doesn’t perform activation by itself: you need to create and provide a job activator implementation class as described in this document: Using IoC Containers — Hangfire Documentation

That said, implementing the job activator is really simple. Here is what I have in my project:

    internal class AspNetCoreJobActivatorScope : JobActivatorScope
    {
        private readonly IServiceScope _serviceScope;
        private readonly string jobId;

        public AspNetCoreJobActivatorScope([NotNull] IServiceScope serviceScope, string jobId)
        {
            this.jobId = jobId;
            _serviceScope = serviceScope ?? throw new ArgumentNullException(nameof(serviceScope));
        }

        public override object Resolve(Type type)
        {
            var res = ActivatorUtilities.GetServiceOrCreateInstance(_serviceScope.ServiceProvider, type);
            if (res is IBackgroundJob backgroundJob)
                backgroundJob.JobID = jobId;
            return res;
        }

        public override void DisposeScope()
        {
            _serviceScope.Dispose();
        }
    }
    // This class injects the default DI container into hangfire jobs
    public class HangfireActivator : JobActivator
    {
        private readonly IServiceScopeFactory _serviceScopeFactory;

        public HangfireActivator([NotNull] IServiceScopeFactory serviceScopeFactory)
        {
            _serviceScopeFactory = serviceScopeFactory ?? throw new ArgumentNullException(nameof(serviceScopeFactory));
        }

        public override JobActivatorScope BeginScope(JobActivatorContext context)
        {
            return new AspNetCoreJobActivatorScope(_serviceScopeFactory.CreateScope(), context.BackgroundJob.Id);
        }
    }

and then, when configuring Hangfire:

                        globalConfig.UseSqlServerStorage(hangfireConfig.ConnectionString)
                            // Create a custom job activator in order to use the ASP.net core default Di container
                            .UseActivator(new HangfireActivator(provider.GetRequiredService<IServiceScopeFactory>()));

That way, the activator I provided to Hangfire will automatically provide a scope per job. It is important to do it this way because you will actually be able to share the same instance of objects in the same scope. Using transient should be limited to the cases where it really is necessary: using scoped instances will limit the number of objects created (and initialized) during your job which could portentially improve memory usage drastically.

If you use transient, every time you request the DI container to obtain or inject an instance of a class, you will get a new instance. This means that if you have devided your application in many classes and using DI to pass them depedancies, you will get a number of (most likely useless) classes instance.

For things like database connections and other external dependancies wrappers, this makes a crutial difference in ressource usage.

Yes, I have now settled on Scoped. However, I’m not creating an activator class. Yet it seems to be working fine. I’m not clear on why you seem to be suggesting I need a custom activator class. Are you saying there’s an issue with the default behavior?

Hangfire will, by default, use the default activator to create object instances passed as parameters for your jobs.

This means that it cannot create instances that do not have a default, parameterless constructors. It also means that all instances are transient (since there is actually no IoC container to handle instance and scopes lifetime management used)

The various “Euqueue” methodes will capture the type of the instance you pass them and Hangfire will re-create new instances of these concrete types when the job runs. That is why it might give you the illusion that it’s “working” as long as you don’t have any injected dependencies.

Should you want to use a DI container for managing your instances lifetime, you must use a custom job activator such as the one I described in my previous answer.

I appreciate you explaining it but I am really struggling to understand this.

Currently, I’ve written no activator. But, in fact, my job class constructor is not parameterless. And it only has one constructor.

public ReportBuilderWorkerService(ILogger<ReportBuilderWorkerService> logger)
{
    Logger = logger;
}

So I’m not sure what you mean when you say the default activator cannot create instances that do not have a default, parameterless constructors.

I’m not saying you’re not right. But I’m obviously still missing pieces of this.

I’m not saying you’re not right. But I’m obviously still missing pieces of this.

I go by the documentation I linked you and by experience: we initially implemented it the same way you did and I ended up having the services inside the job receiving null as parameter until I implemented it the way I showned. Are you sure you don’t actually get a null in there?

I’m afraid I don’t have time right now to test the limits again but I’m sure that the way I described works properly and I’m sure that relying on the default activator caused us issues in the past.

If I find time to test it again, I will update my answer but I still suggest you follow it nevertheless.

Oh and, by the way, I left a bit of application-specific code in my reply: you can remove the following two lines:

        if (res is IBackgroundJob backgroundJob)
            backgroundJob.JobID = jobId;

I am logging to the logger and I can see it’s working. I also tested it by adding a DbContext parameter, and tested I was able to access the database. If DI cannot provide an instance of a type, that should throw an exception.

I’m not sure what else to test as, even after reading the documentation, I don’t really feel I have a good understanding of your concern here.

I had trouble using AddSingleton, speacially when using hangfire logging console.
The logs were all “mixed”, because they were (of course) sharing the same instance.
That said, I always add my hangfire classes as scoped.
Hope this helps you somehow.

Thanks for the confirmation.

Do you have any comments on the discussion above? The suggestion is that I must create a custom activator. However, I’m using the default activator with dependency injection and, so far, it appears to be working okay.

Hello again.

I managed to find some time tom perform a test and it seems that, indeed, Hangfire will use the default .net core service provider for jobs activation (at least, it does so if you’re running asp.net core)

I’m not sure why I had to write a custom activator for our code base 3 years ago but it seems it’s not necessary (anymore?).

Thanks for responding back. Yeah, it seems to be working okay for us, although I don’t really think I had a good understanding of the issues involved. But thanks for the confirmation.