2

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

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

2 kinds of activators:

public class ContainerJobActivator : JobActivator
{
    private IServiceProvider _container;

    public ContainerJobActivator(IServiceProvider serviceProvider) =>
        _container = serviceProvider;

    public override object ActivateJob(Type type) => _container.GetService(type);
}

public class ScopedContainerJobActivator : JobActivator
{
    readonly IServiceScopeFactory _serviceScopeFactory;
    public ScopedContainerJobActivator(IServiceProvider serviceProvider)
    {
        _serviceScopeFactory = serviceProvider.GetService<IServiceScopeFactory>();
    }

    public override JobActivatorScope BeginScope(JobActivatorContext context) =>
        new ServiceJobActivatorScope(_serviceScopeFactory.CreateScope());

    private class ServiceJobActivatorScope : JobActivatorScope
    {
        readonly IServiceScope _serviceScope;
        public ServiceJobActivatorScope(IServiceScope serviceScope) =>
            _serviceScope = serviceScope;

        public override object Resolve(Type type) =>
            _serviceScope.ServiceProvider.GetService(type);
    }
}

Startup:

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddHangfire(configuration => configuration
            .SetDataCompatibilityLevel(CompatibilityLevel.Version_170)
            .UseSimpleAssemblyNameTypeSerializer()
            .UseRecommendedSerializerSettings()
            .UseSqlServerStorage("connection string", 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"));
        // doesn't work either
        // services.AddSingleton<IMyContract>(i => new MyContractImplementation("blabla"));
        // doesn't work either
        // services.AddTransient<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}");
            });
        });
    }
}
Steven
  • 166,672
  • 24
  • 332
  • 435
Stephane
  • 1,359
  • 1
  • 15
  • 27

1 Answers1

1

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}");
                });
            });
        }
    }
}
Stephane
  • 1,359
  • 1
  • 15
  • 27
  • Right, The issue was that you need to declare IMyContract as transient, not scoped. Or if you want to use it within the scope, then you have to create new scope before IMyContract can be resolved. – Alexander Pavlenko Sep 04 '20 at 17:27
  • Thanks, this helped solve the issue I was having! – mack May 03 '21 at 20:04