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
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>()));
}
);
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).
@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);
}
}
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();
}
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.