【问题标题】:Impossible to use dependency injection in an Hangfire job无法在 Hangfire 作业中使用依赖注入
【发布时间】:2020-04-19 09:12:48
【问题描述】:

上下文

我使用 Hangfire(版本 1.7.11)作为调度程序。但是我不能在我的工作中使用正确的 DI。

目前有效

没问题安排这样的事情,因为SomeConcreteService 有一个无参数的构造函数:

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

什么不起作用

但是当我尝试使用此处推荐的方法将服务注入 Hangfire 作业时出现异常:https://docs.hangfire.io/en/latest/background-methods/using-ioc-containers.html

当我尝试使用 DI 添加新的计划作业时,出现以下异常:

抛出异常:System.Linq.Expressions.dll 中的“System.InvalidOperationException”:从范围“”引用的“TestHangfire.IMyContract”类型的“变量“mc”,但未定义”

异常发生在这一行:

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

这个问题是如此微不足道,以至于我确信我遗漏了一些明显的东西。

感谢您的帮助。

(几乎)完整的代码

服务:

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 种激活剂:

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

启动:

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

【问题讨论】:

    标签: asp.net-core dependency-injection hangfire


    【解决方案1】:

    我发现了问题。

    因为实际上似乎是表达式导致了问题,并且考虑到添加重复作业的另一种方法是传输类型和方法信息,在我看来问题是由一种过于进化的表达方式。所以我改变了方法,让我的服务方法通过给定一个参数来完成整个工作

    这是有效的新代码:

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

    【讨论】:

    • 对,问题是您需要将 IMyContract 声明为瞬态,而不是作用域。或者如果你想在范围内使用它,那么你必须在 IMyContract 被解析之前创建新的范围。
    • 谢谢,这有助于解决我遇到的问题!
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-06-09
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多