Using Dependency Injection with Hangfire .NET Core Console app

Hello everyone,

I am trying to implement Hangfire using a console .NET core app for both Client and Server.
The Client app is implemented using the Microsoft Default DI Container.

I have made some research and implemented the both server and client. After running the client, the jobs and scheduled. When I run the server, I get an error as follows:
JobActivator returned NULL instance of the 'HangfireClient.Core' type.

The client and server are seperate console apps. The client is added as a reference to the server

Here is how I set up the Client.
Some code omitted for brevity

 private static async Task Main(string[] args)
        {
            string environment = Environment.GetEnvironmentVariable("NETCORE_ENVIRONMENT");
            try
            {
                string appSetting = string.IsNullOrEmpty(environment) ? $"appsettings.json" : $"appsettings.{environment}.json";

                IConfiguration configuration = new ConfigurationBuilder()
                  .SetBasePath(Directory.GetCurrentDirectory())
                  .AddJsonFile(appSetting, false, true)
                  .Build();

                var servicesProvider = BuildDI(configuration);
                using (servicesProvider as IDisposable)
                {

                    RecurringJob.AddOrUpdate<ICore>("HangFireClient", job => job.StartCore(), Cron.Minutely);

                    Console.WriteLine("Press ANY key to exit");
                }
            }
            catch (Exception ex)
            {
                logger.Error(ex, "Stopped program because of exception");
            }
            finally
            {
                LogManager.Shutdown();
            }
        }

 private static IServiceProvider BuildDI(IConfiguration configuration)
        {
            //create a serviceProvider
            IServiceCollection services = new ServiceCollection();

            services.AddSingleton(configuration);

            //hangfire
            string hangfireConnectionString = configuration.GetConnectionString("HangfireDb");
            GlobalConfiguration.Configuration.UseSqlServerStorage(hangfireConnectionString);

            //add the main program to run
            services.AddSingleton<ICore, Core>();

            //generate a provider
            return services.BuildServiceProvider();
        }

My server implementation is written below.
The server is a background worker task

Worker.cs
 protected override async Task ExecuteAsync(CancellationToken stoppingToken)
        {
            try
            {
                //while (!stoppingToken.IsCancellationRequested)
                //{
                //    _logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);
                //    await Task.Delay(1000, stoppingToken);
                //}

                using (_server = new BackgroundJobServer())
                {
                    Console.WriteLine("Hangfire Server started. Press Ctrl + C to stop...");
                    while (!stoppingToken.IsCancellationRequested)
                    {
                        _logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);
                        await Task.Delay(1000, stoppingToken);
                    }
                }
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "{Message}", ex.Message);

                Console.WriteLine(stoppingToken.IsCancellationRequested);

                Environment.Exit(1);
            }
        }

The ContainerJobActivator:

public class ContainerJobActivator : JobActivator
    {
        private IServiceProvider _container;

        public ContainerJobActivator(IServiceProvider container)
        {
            _container = container;
        }

        public override object ActivateJob(Type type)
        {
            return _container.GetService(type);
        }
    }


Program.cs:
 public static void Main(string[] args)
        {
            IHost host = Host.CreateDefaultBuilder(args)
                //.UseWindowsService(option =>
                //{
                //    option.ServiceName = "Hangfire Server";
                //})

                .ConfigureServices(services =>
                {
                    services.AddHostedService<Worker>();

                    var provider = services.BuildServiceProvider();

                    GlobalConfiguration.Configuration.UseActivator(new ContainerJobActivator(provider));
                })
                .Build();
            host.Run();
        }

When debugging, I noticed that that ActivateJob is returning null

Any help here would be deeply appreciated.

Thanks

@aschenta you helped on a previous question, I was hoping you can help with some guidance here. Thanks

You aren’t injecting Hangfire properly into your service collection.

Here is a snipet from our own code. As you can see, we’re using IServiceCollection.AddHangfire and the calling IGlobalConfiguration.UseActivator to set it up. The later is necessary to inject the activator globally and we’re giving it an instance of the IServiceScopeFactory from the defaut .net core service factory (I’ve removed most unrelated code from the snipet)

                    services.AddHangfireServer(p =>
                    {
                        p.CancellationCheckInterval = TimeSpan.FromSeconds(5);
                        p.Queues = new[] { "cleanup", "default" };
                    });
                    services.AddHangfire((provider, globalConfig) =>
                        {
                            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>()));
                        }
                    );
1 Like

Thank you very much for the reply @Fulgan

The server is a console application and according to their documentation, it states that

Hangfire.Core package is enough
Please don’t install the Hangfire package for console applications as it is a quick-start package only and contain dependencies you may not need (for example, Microsoft.Owin.Host.SystemWeb).

No matter what, you need to inject the service factory into the activator somehow: hangfire will not do that for you.

UseActivator is part of the Hangfire.Core assembly so you should still be able to use it.

@Fulgan Thank you very much for you assistance so far.
I made the changes as you suggested.

the Activator looks like this now:

public class ContainerJobActivator : JobActivator
    {
        private IServiceScopeFactory _container;
       
        public ContainerJobActivator(IServiceScopeFactory container)
        {
            _container = container;
        }

        public override object ActivateJob(Type type)
        {
            using var scope = _container.CreateScope();

            var b = scope.ServiceProvider.GetRequiredService(type);

            return scope.ServiceProvider.GetRequiredService(type);

        }
    }

And in my Program.cs,

Host host = Host.CreateDefaultBuilder(args)
                //.UseWindowsService(option =>
                //{
                //    option.ServiceName = "WellaHealth Hangfire Server";
                //})

                .ConfigureServices(services =>
                {
                    services.AddHostedService<Worker>();
                    var provider = services.BuildServiceProvider();

                    GlobalConfiguration.Configuration.UseActivator(new ContainerJobActivator(provider.GetRequiredService<IServiceScopeFactory>()));
                })
                .Build();

I am getting a different error now.
No service for type 'HangfireClient.ICore' has been registered
I am confused because on my Hangfire.Client, I am registering a type for ICore as seen here:

private static async Task Main(string[] args)
        {
            string environment = Environment.GetEnvironmentVariable("NETCORE_ENVIRONMENT");
            try
            {
                string appSetting = string.IsNullOrEmpty(environment) ? $"appsettings.json" : $"appsettings.{environment}.json";

                IConfiguration configuration = new ConfigurationBuilder()
                  .SetBasePath(Directory.GetCurrentDirectory())
                  .AddJsonFile(appSetting, false, true)
                  .Build();

                var servicesProvider = BuildDI(configuration);
                using (servicesProvider as IDisposable)
                {

                    RecurringJob.AddOrUpdate<ICore>("HangFireClient", job => job.StartCore(), Cron.Minutely);

                    Console.WriteLine("Press ANY key to exit");
                }
            }
            catch (Exception ex)
            {
                logger.Error(ex, "Stopped program because of exception");
            }
            finally
            {
                LogManager.Shutdown();
            }
        }

 private static IServiceProvider BuildDI(IConfiguration configuration)
        {
            //create a serviceProvider
            IServiceCollection services = new ServiceCollection();

            services.AddSingleton(configuration);

            //hangfire
            string hangfireConnectionString = configuration.GetConnectionString("HangfireDb");
            GlobalConfiguration.Configuration.UseSqlServerStorage(hangfireConnectionString);

            //add the main program to run. Type registered here
            services.AddScoped<ICore, Core>();

            //generate a provider
            return services.BuildServiceProvider();
        }

Try adding your service as transent instead of scoped. Without the hosting, it’s unlikely Hangfire will create a scope of its own.

Thanks for your response @Fulgan
I think I may have found a solution. I am still unsure if this is anti-pattern or may lead to issues.
I believe the issue is that since both hangfire server and clients are different apps, the services in the client app are not being injected.
To fix this, I declared a method in the client app that will add all services and return an IServiceProvider that is the used in the JobActivator

in the Client app, I changed the BuildDI method to public:

 public static IServiceProvider BuildDI(IServiceCollection services, string appSetting = "")
        {
            if (string.IsNullOrEmpty(appSetting))
            {
                appSetting = "HangfireClient.appsettings.json";
            }

            IConfiguration configuration = new ConfigurationBuilder()
              .SetBasePath(Directory.GetCurrentDirectory())
              .AddJsonFile(appSetting, false, true)
              .Build();

            services.AddSingleton(configuration);

            string connectionString = configuration.GetConnectionString("ClientDb");
            services.AddDbContext<HangfireDbContext>(options =>
            {
                options.UseSqlServer(connectionString);
            });

            //hangfire
            string hangfireConnectionString = configuration.GetConnectionString("HangfireDb");
            GlobalConfiguration.Configuration.UseSqlServerStorage(hangfireConnectionString);

            //add the main program to run
            services.AddScoped<ICore, Core>();

            services.AddScoped<IRecurringJobManager, RecurringJobManager>();

            //generate a provider
            return services.BuildServiceProvider();
        }

In the Program.cs of the HangfireServer, I now have

var clientProvider = HangfireClient.Program.BuildDI(services);

GlobalConfiguration.Configuration.UseActivator(new ScopedContainerJobActivator(clientProvider));

The ScopedContainerJobActivator is declared thus:

public class ScopedContainerJobActivator : JobActivator
    {
        private readonly IServiceScopeFactory _serviceScopeFactory;

        public ScopedContainerJobActivator(IServiceProvider serviceProvider)
        {
            if (serviceProvider == null) throw new ArgumentNullException(nameof(serviceProvider));
            _serviceScopeFactory = serviceProvider.GetService<IServiceScopeFactory>();
        }

        public override JobActivatorScope BeginScope(JobActivatorContext context)
        {
            return new ServiceJobActivatorScope(_serviceScopeFactory.CreateScope());
        }

        private class ServiceJobActivatorScope : JobActivatorScope
        {
            private readonly IServiceScope _serviceScope;

            public ServiceJobActivatorScope(IServiceScope serviceScope)
            {
                if (serviceScope == null)
                {
                    throw new ArgumentNullException(nameof(serviceScope));
                };
                _serviceScope = serviceScope;
            }

            public override object Resolve(Type type)
            {
                var c = _serviceScope.ServiceProvider.GetService(type);
                return _serviceScope.ServiceProvider.GetService(type);
            }
        }
    }

the above setup works, but as I said, I wonder if it’s breaking any pattern or would cause any issue.