Problem to setup dependency injection for RecurringJob

Tags: #<Tag:0x00007faff7b33900>

Context

I use Hangfire (version 1.7.11) as a scheduler. But I can’t use proper DI in my jobs.

What works so far

I have no problem scheduling something like this, given the fact SomeConcreteService have a parameterless constructor:

RecurringJob.AddOrUpdate<SomeConcreteService>(jobId, mc => Console.WriteLine($"Message from job: {mc.GetValue()}"), "1/2 * * * *");

What does not work

But I get an exception when I try to inject a service into a Hangfire job using what is recommended here: https://docs.hangfire.io/en/latest/background-methods/using-ioc-containers.html

When I try to add a new scheduled job using DI, I get the following exception:

Exception thrown: 'System.InvalidOperationException' in System.Linq.Expressions.dll: 'variable 'mc' of type 'TestHangfire.IMyContract' referenced from scope '', but it is not defined'

The exception occurs a this line:

RecurringJob.AddOrUpdate<IMyContract>(jobId, mc => Console.WriteLine($"Message from job {jobId} => {mc.GetValue()}"), "1/2 * * * *");

The problem is so trivial that I am sure I am missing something obvious.

Thanks for helping.

The (nearly) full code

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Hangfire;
using Hangfire.SqlServer;
using Hangfire.Storage;
using System.Text.Json;

namespace TestHangfire
{
    #region Service
    public interface IMyContract
    {
        string GetValue();
    }
    public class MyContractImplementation : IMyContract
    {
        public string _label;

        public MyContractImplementation(string label)
        {
            _label = label;
        }

        public string GetValue() => $"{_label}:{Guid.NewGuid()}";
    }
    #endregion
    
    #region 2 kinds of activators
    public class ContainerJobActivator : JobActivator
    {
        private IServiceProvider _container;

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

        public override object ActivateJob(Type type)
        {
            return _container.GetService(type);
        }
    }
    public class ScopedContainerJobActivator : JobActivator
    {
        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
        {
            readonly IServiceScope _serviceScope;
            public ServiceJobActivatorScope(IServiceScope serviceScope)
            {
                if (serviceScope == null) throw new ArgumentNullException(nameof(serviceScope));
                _serviceScope = serviceScope;
            }
            public override object Resolve(Type type)
            {
                return _serviceScope.ServiceProvider.GetService(type);
            }
        }
    }
    #endregion
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddHangfire(configuration => configuration
                .SetDataCompatibilityLevel(CompatibilityLevel.Version_170)
                .UseSimpleAssemblyNameTypeSerializer()
                .UseRecommendedSerializerSettings()
                .UseSqlServerStorage("Server=localhost,1433;Database=HangfireTest;user=sa;password=xxxxxxxxx;MultipleActiveResultSets=True", new SqlServerStorageOptions
                {
                    CommandBatchMaxTimeout = TimeSpan.FromMinutes(5),
                    SlidingInvisibilityTimeout = TimeSpan.FromMinutes(5),
                    QueuePollInterval = TimeSpan.Zero,
                    UseRecommendedIsolationLevel = true,
                    UsePageLocksOnDequeue = true,
                    DisableGlobalLocks = true
                }));

            services.AddHangfireServer();
            services.BuildServiceProvider();
            services.AddScoped<IMyContract>(i => new MyContractImplementation("blabla"));
        }

        public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IServiceProvider serviceProvider)
        {
            // Just to ensure the service is correctly injected...
            Console.WriteLine(serviceProvider.GetService<IMyContract>().GetValue());
            
            // I face the problem for both activators: ScopedContainerJobActivator or ContainerJobActivator
            GlobalConfiguration.Configuration.UseActivator(new ContainerJobActivator(serviceProvider));
            // GlobalConfiguration.Configuration.UseActivator(new ScopedContainerJobActivator(serviceProvider));

            app.UseRouting();
            app.UseHangfireDashboard();

            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            app.UseEndpoints(endpoints =>
            {
                endpoints.MapGet("/", async context =>
                {
                    await context.Response.WriteAsync(JsonSerializer.Serialize(Hangfire.JobStorage.Current.GetConnection().GetRecurringJobs()
                        .Select(i => new { i.Id, i.CreatedAt, i.Cron }).ToList()));
                });
                endpoints.MapGet("/add", async context =>
                {
                    var manager = new RecurringJobManager();
                    var jobId = $"{Guid.NewGuid()}";

                    // I GET AN EXCEPTION HERE: 
                    // Exception thrown: 'System.InvalidOperationException' in System.Linq.Expressions.dll: 'variable 'mc' of type 'TestHangfire.IMyContract' referenced from scope '', but it is not defined'
                    manager.AddOrUpdate<IMyContract>(jobId, mc => Console.WriteLine($"Message from job {jobId} => {mc.GetValue()}"), "1/2 * * * *");

                    // doesn't work either: it's normal, it is just a wrapper of what is above
                    // RecurringJob.AddOrUpdate<IMyContract>(jobId, mc => Console.WriteLine($"Message from job {jobId} => {mc.GetValue()}"), "1/2 * * * *");

                    await context.Response.WriteAsync($"Schedule added: {jobId}");
                });
            });
        }
    }
}

I found the issue.

As it was actually the expression that seemed to cause an issue, and given the fact that the other way to add a recurring job is to transmit a type, and a method info, it seemed to me that the problem was caused by an expression that was too evolved. So I changed the approach to have a method of my service that make the whole job by being given a parameter.

Here is the new code that works:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Hangfire;
using Hangfire.SqlServer;
using Hangfire.Storage;
using System.Text.Json;

namespace TestHangfire
{
    #region Service
    public interface IMyContract
    {
        void MakeAction(string someText);
    }
    public class MyContractImplementation : IMyContract
    {
        public string _label;

        public MyContractImplementation(string label)
        {
            _label = label;
        }

        public void MakeAction(string someText) => Console.WriteLine($"{_label}:{someText}");
    }
    #endregion

    #region 2 kinds of activators
    public class ContainerJobActivator : JobActivator
    {
        private IServiceProvider _container;

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

        public override object ActivateJob(Type type)
        {
            return _container.GetService(type);
        }
    }
    #endregion
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddHangfire(configuration => configuration
                .SetDataCompatibilityLevel(CompatibilityLevel.Version_170)
                .UseSimpleAssemblyNameTypeSerializer()
                .UseRecommendedSerializerSettings()
                .UseSqlServerStorage("Server=localhost,1433;Database=HangfireTest;user=sa;password=xxxxxx;MultipleActiveResultSets=True", new SqlServerStorageOptions
                {
                    CommandBatchMaxTimeout = TimeSpan.FromMinutes(5),
                    SlidingInvisibilityTimeout = TimeSpan.FromMinutes(5),
                    QueuePollInterval = TimeSpan.Zero,
                    UseRecommendedIsolationLevel = true,
                    UsePageLocksOnDequeue = true,
                    DisableGlobalLocks = true
                }));

            services.AddHangfireServer();
            services.BuildServiceProvider();
            services.AddTransient<IMyContract>(i => new MyContractImplementation("blabla"));
        }

        public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IServiceProvider serviceProvider)
        {
            GlobalConfiguration.Configuration.UseActivator(new ContainerJobActivator(serviceProvider));

            app.UseRouting();
            app.UseHangfireDashboard();

            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            app.UseEndpoints(endpoints =>
            {
                endpoints.MapGet("/", async context =>
                {
                    await context.Response.WriteAsync(JsonSerializer.Serialize(Hangfire.JobStorage.Current.GetConnection().GetRecurringJobs()
                        .Select(i => new { i.Id, i.CreatedAt, i.Cron }).ToList()));
                });
                endpoints.MapGet("/add", async context =>
                {
                    var manager = new RecurringJobManager();
                    var jobId = $"{Guid.NewGuid()}";
                    manager.AddOrUpdate<IMyContract>(jobId, (IMyContract mc) => mc.MakeAction(jobId), "1/2 * * * *");

                    await context.Response.WriteAsync($"Schedule added: {jobId}");
                });
            });
        }
    }
}