Fysicus
Fysicus

Reputation: 381

Getting Hangfire to work in a Windows Service using .NET 4.6.1

For a project I'm converting an existing console to a Windows Service. The current console application runs a series of jobs consecutively.

The idea of the service is to run each of these jobs on a schedule. To do this we've created a windows service which does this using Hangfire.

The constructor

public BatchService(ILogger<BatchService> logger)
{
    InitializeComponent();
    GlobalConfiguration.Configuration
                       .SetDataCompatibilityLevel(CompatibilityLevel.Version_170)
                       .UseColouredConsoleLogProvider()
                       .UseSimpleAssemblyNameTypeSerializer()
                       .UseRecommendedSerializerSettings()
                       .UseSqlServerStorage(Constants.ConnectionString.Peach, 
                                            new SqlServerStorageOptions
                                            {
                                                CommandBatchMaxTimeout = TimeSpan.FromMinutes(5),
                                                SlidingInvisibilityTimeout = TimeSpan.FromMinutes(5),
                                                QueuePollInterval = TimeSpan.Zero,
                                                UseRecommendedIsolationLevel = true
                                            })
                       .UseNLogLogProvider();

    _logger = logger;
}

The methods used to load everything into Hangfire:

private void LoadConfigurationIntoHangfire()
{
    //  Loading the job configurationNo 
    var jobListing = Configuration.GetJobConfiguration().Where(job => job.JobType.Equals("CRON")).ToList();

    //  Debug
    _logger.LogDebug($"LoadConfigurationIntoHangfire() - jobListing =     {JsonConvert.SerializeObject(jobListing)}");

    foreach (var job in jobListing)
    {
        //  Information
        _logger.LogInformation($"Added/updated job '{job.JobName}' with cron expression '{job.CronExpression}'.");

        RecurringJob.AddOrUpdate<IDataProtectionJob>(x => x.ExecuteJob(job),     job.CronExpression);
    }
}

private void StopHangfire()
{
    //  Information
    _logger.LogInformation($"Stopping Hangfire.");
    
    _hangfireServer.SendStop();
    _hangfireServer.WaitForShutdown(new TimeSpan(0, 0, 5));
    _hangfireServer.Dispose();

    //  Information
    _logger.LogInformation($"Hangfire stopped.");
}

private void StartHangfire()
{
    //  Information
    _logger.LogInformation($"Starting Hangfire.");

    _hangfireServer = new BackgroundJobServer();
    LoadConfigurationIntoHangfire();

    //  Information
    _logger.LogInformation($"Hangfire started.");
}

The OnStart event of the service:

protected override void OnStart(string[] args)
{
    try
    {
        // Update the service state to Start Pending.
        ServiceStatus serviceStatus = new ServiceStatus
        {
            dwCurrentState = ServiceState.SERVICE_START_PENDING,
            dwWaitHint = 100000
        };
        SetServiceStatus(this.ServiceHandle, ref serviceStatus);

        //  Information
        _logger.LogInformation("Starting Windows Service 'BatchService'");

        SetPolling();
        StartHangfire();

        _logger.LogInformation("Windows Service 'BatchService' started");

        // Update the service state to Running.
        serviceStatus.dwCurrentState = ServiceState.SERVICE_RUNNING;
        SetServiceStatus(this.ServiceHandle, ref serviceStatus);
    }
    catch (Exception e)
    {
        _logger.LogError($"Windows Service 'BatchService' failed to start ({e})");

        throw;
    }
}

The jobs are based on a interface IDataProtectionJob, which has an implementation,the implementation itself is beyond the scope (I think).

Now, the implementation builds and launches without problems, but... no tasks are executed. In the log files I found the following error messages:

2022-11-23 19:05:53.0881|DEBUG|Hangfire.Server.DelayedJobScheduler|2 scheduled job(s) processed by scheduler. 2022-11-23 19:05:53.1353|WARN|Hangfire.AutomaticRetryAttribute|Failed to process the job '17': an exception occurred. Retry attempt 5 of 10 will be performed in 00:05:36.|System.MissingMethodException: No parameterless constructor defined for this object. at System.RuntimeTypeHandle.CreateInstance(RuntimeType type, Boolean publicOnly, Boolean noCheck, Boolean& canBeCached, RuntimeMethodHandleInternal& ctor, Boolean& bNeedSecurityCheck) at System.RuntimeType.CreateInstanceSlow(Boolean publicOnly, Boolean skipCheckThis, Boolean fillCache, StackCrawlMark& stackMark) at System.RuntimeType.CreateInstanceDefaultCtor(Boolean publicOnly, Boolean skipCheckThis, Boolean fillCache, StackCrawlMark& stackMark)
at System.Activator.CreateInstance(Type type, Boolean nonPublic) at System.Activator.CreateInstance(Type type) at Hangfire.JobActivator.ActivateJob(Type jobType) at Hangfire.JobActivator.SimpleJobActivatorScope.Resolve(Type type) at Hangfire.Server.CoreBackgroundJobPerformer.Perform(PerformContext context) at Hangfire.Server.BackgroundJobPerformer.<>c__DisplayClass9_0.b__0() at Hangfire.Server.BackgroundJobPerformer.InvokePerformFilter(IServerFilter filter, PerformingContext preContext, Func1 continuation) at Hangfire.Server.BackgroundJobPerformer.<>c__DisplayClass9_1.<PerformJobWithFilters>b__2() at Hangfire.Server.BackgroundJobPerformer.PerformJobWithFilters(PerformContext context, IEnumerable1 filters) at Hangfire.Server.BackgroundJobPerformer.Perform(PerformContext context) at Hangfire.Server.Worker.PerformJob(BackgroundProcessContext context, IStorageConnection connection, String jobId) 2022-11-23 19:05:53.1383|WARN|Hangfire.AutomaticRetryAttribute|Failed to process the job '20': an exception occurred. Retry attempt 2 of 10 will be performed in 00:01:04.|System.MissingMethodException: Cannot create an instance of an interface. at System.RuntimeTypeHandle.CreateInstance(RuntimeType type, Boolean publicOnly, Boolean noCheck, Boolean& canBeCached, RuntimeMethodHandleInternal& ctor, Boolean& bNeedSecurityCheck) at System.RuntimeType.CreateInstanceSlow(Boolean publicOnly, Boolean skipCheckThis, Boolean fillCache, StackCrawlMark& stackMark) at System.RuntimeType.CreateInstanceDefaultCtor(Boolean publicOnly, Boolean skipCheckThis, Boolean fillCache, StackCrawlMark& stackMark)
at System.Activator.CreateInstance(Type type, Boolean nonPublic) at System.Activator.CreateInstance(Type type) at Hangfire.JobActivator.ActivateJob(Type jobType) at Hangfire.JobActivator.SimpleJobActivatorScope.Resolve(Type type) at Hangfire.Server.CoreBackgroundJobPerformer.Perform(PerformContext context) at Hangfire.Server.BackgroundJobPerformer.<>c__DisplayClass9_0.b__0() at Hangfire.Server.BackgroundJobPerformer.InvokePerformFilter(IServerFilter filter, PerformingContext preContext, Func1 continuation) at Hangfire.Server.BackgroundJobPerformer.<>c__DisplayClass9_1.<PerformJobWithFilters>b__2() at Hangfire.Server.BackgroundJobPerformer.PerformJobWithFilters(PerformContext context, IEnumerable1 filters) at Hangfire.Server.BackgroundJobPerformer.Perform(PerformContext context) at Hangfire.Server.Worker.PerformJob(BackgroundProcessContext context, IStorageConnection connection, String jobId)

I googled the error message, found some explanation based on the way Hangfire resolves the dependencies of the methods you assign to it for execution.

I've played around a bit with, but to no avail. What can I try next?

OS: Windows Server 2016
.NET version: 4.6.1

Upvotes: 3

Views: 1157

Answers (1)

mason
mason

Reputation: 32694

By default, Hangfire does not know what type to use to execute a job when the job is enqueued with an interface. There's a specific component called the JobActivator, which simply uses Activator.CreateInstance to create the type to execute the method on for your background job. And you can't just use Activator.CreateInstance to create an instance of a type. You'll get an error:

'Cannot dynamically create an instance of type ''. Reason: Cannot create an instance of an interface.'

Nor can it create an instance of a type that doesn't have a parameterless constructor. Consider this code:

var result = Activator.CreateInstance(typeof(TestType));

public class TestType
{
    public TestType(string someParameter)
    {

    }
}

You'll get this error:

System.MissingMethodException: 'Cannot dynamically create an instance of type ''. Reason: No parameterless constructor defined.'

That happens to be the same error you got.

However, just because the default job activator doesn't know how to deal with interfaces or parameterless types doesn't mean you're stuck. Hangfire's creators have provided the flexibility to swap in a different job activator, as described in their documentation.

In .NET, we often use IoC containers to resolve dependencies. That's exactly what Hangfire needs. In order to wire it up, you pick an IoC container (if you don't already have one) and then find a suitable library that provides a job activator to serve as the glue between your IoC container and Hangfire. If one doesn't exist, you could create one yourself, as the contract isn't that complex. You'd just inherit from JobActivator and override its methods.

In my company, we have an app that uses Autofac as the IoC container. So we installed the Hangfire.Autofac library that provides us with an Autofac job activator for Hangfire.

Then we tell Hangfire to use it on startup:

IContainer container = GetContainer(); //implement this however you like

// Tell Hangfire to use our container
GlobalConfiguration.Configuration.UseAutofacActivator(container);


// Now later on, when we have jobs to enqueue, we can do so via an interface.
RecurringJob.AddOrUpdate<IDataProtectionJob>(x => x.ExecuteJob(job), job.CronExpression);

In the Hangfire's job storage, it will serialize the expression with the interface name. So then any background job servers (which may or may not be the same as the app that schedules the jobs) can use their IoC containers to resolve a concrete type that implements your interface (IDataProjectionJob in this case), then execute the background job method (ExecuteJob in this case).

Upvotes: 4

Related Questions