【问题标题】:Quartz.NET 3.0 seems to launch all jobs in the same scopeQuartz.NET 3.0 似乎在同一范围内启动所有作业
【发布时间】:2019-03-18 20:27:00
【问题描述】:

在我定义了两个依赖范围服务 (ScopedDataAccess) 的作业后,我很难将 Quartz 3.0.7 与 ASP.NET Core 2.2 一起使用,该服务是我的数据库上下文的包装器:

services.AddScoped<IScopedDataAccess, ScopedDataAccess>();

services.AddDbContext<AggregatorContext>(opt => opt.UseSqlServer(configuration.GetConnectionString("Default")));

问题在于,两个作业都接收到同一个作用域服务实例(因此也是同一个数据库上下文),因此由于并行使用而导致上下文崩溃。

我的代码如下:

Startup.cs

作业被定义为“范围”,我的期望是每个实例都在自己的“范围”中运行

private void ConfigureQuartz(IServiceCollection services, params Type[] jobs)
{
    services.AddSingleton<IJobFactory, QuartzJobFactory>();
    services.Add(jobs.Select(jobType => new ServiceDescriptor(jobType, jobType, ServiceLifetime.Scoped)));

    services.AddSingleton(provider =>
    {
        var schedulerFactory = new StdSchedulerFactory();
        var scheduler = schedulerFactory.GetScheduler().Result;

        scheduler.JobFactory = provider.GetService<IJobFactory>();
        scheduler.Start();
        return scheduler;
    });
}

protected void StartJobs(IApplicationBuilder app, IApplicationLifetime lifetime)
{
    var scheduler = app.ApplicationServices.GetService<IScheduler>();
    var configService = app.ApplicationServices.GetService<IConfigurationService>();

    QuartzServicesUtilities.StartJob<ArticleXUserDataRefresherJob>(scheduler, 
        TimeSpan.FromSeconds(configService.ArticleXUserDataRefresherJobPeriod));
    QuartzServicesUtilities.StartJob<LinkDataFetchJob>(scheduler,
        TimeSpan.FromSeconds(configService.LinkDataJobPeriod));

    lifetime.ApplicationStarted.Register(() => scheduler.Start());
    lifetime.ApplicationStopping.Register(() => scheduler.Shutdown());
}

QuartzServicesUtilities

public class QuartzServicesUtilities
{
    public static void StartJob<TJob>(IScheduler scheduler, TimeSpan runInterval)
        where TJob : IJob
    {
        var jobName = typeof(TJob).FullName;

        var job = JobBuilder.Create<TJob>()
            .WithIdentity(jobName)
            .Build();

        var trigger = TriggerBuilder.Create()
            .WithIdentity($"{jobName}.trigger")
            .StartNow()
            .WithSimpleSchedule(scheduleBuilder =>
                scheduleBuilder
                    .WithInterval(runInterval)
                    .RepeatForever())
            .Build();

        scheduler.ScheduleJob(job, trigger);
    }
}

QuartzJobFactory

public class QuartzJobFactory : IJobFactory
{
    private readonly IServiceProvider _serviceProvider;

    public QuartzJobFactory(IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider;
    }

    public IJob NewJob(TriggerFiredBundle bundle, IScheduler scheduler)
    {
        var jobDetail = bundle.JobDetail;

        var job = (IJob)_serviceProvider.GetService(jobDetail.JobType);
        return job;
    }

    public void ReturnJob(IJob job) { }
}

有没有办法使用 Quartz.NET 为不同的工作获取不同的作用域?

【问题讨论】:

    标签: c# quartz.net asp.net-core-2.2


    【解决方案1】:

    据我所知,这对于 Quartz 是不可能的,我遇到了同样的问题,我找到的唯一解决方案是使用 ServiceLocator 并在 Job 中显式创建范围。

    我以这样的方式结束:

    // Pseudo-Code
    public class MyJob : IJob
    {
        private readonly IServiceLocator _serviceLocator;
    
        public MyJob(IServiceLocator serviceLocator)
        {
            _serviceLocator = serviceLocator;
        }
    
        public async Task Execute(JobExecutionContext context)
        {
            using(_serviceLocator.BeginScope())
            {
                var worker = _serviceLocator.GetService<MyWorker>();
                await worker.DoWorkAsync();
            }
        }
    }
    

    在这种情况下,您的工作人员仍在作用域内,但工作不再存在。因此,您仍然可以在解决方案的其他地方使用您的 Worker,并且范围仍然有效。 ServiceLocator 需要你自己实现,具体取决于你使用的 DI,IServiceLocator 也必须由你自己定义。

    编辑

    在我们的一个项目中,我们使用了这个:

    /// <summary>
    /// A simple service locator to hide the real IOC Container.
    /// Lowers the anti-pattern of service locators a bit.
    /// </summary>
    public interface IServiceLocator
    {
        /// <summary>
        /// Begins an new async scope.
        /// The scope should be disposed explicitly.
        /// </summary>
        /// <returns></returns>
    
        IDisposable BeginAsyncScope();
        /// <summary>
        /// Gets an instance of the given <typeparamref name="TService" />.
        /// </summary>
        /// <typeparam name="TService">Type of the requested service.</typeparam>
        /// <returns>The requested service instance.</returns>
        TService GetInstance<TService>() where TService : class;
    }
    

    我们在这个实现中主要使用 SimpleInjector:

    /// <summary>
    /// SimpleInjector implementation of the service locator.
    /// </summary>
    public class ServiceLocator : IServiceLocator
    {
        #region member vars
    
        /// <summary>
        /// The SimpleInjector container.
        /// </summary>
        private readonly Container _container;
    
        #endregion
    
        #region constructors and destructors
    
        public ServiceLocator(Container container)
        {
            _container = container;
        }
    
        #endregion
    
        #region explicit interfaces
    
        /// <inheritdoc />
        public IDisposable BeginAsyncScope()
        {
            return AsyncScopedLifestyle.BeginScope(_container);
        }
    
        /// <inheritdoc />
        public TService GetInstance<TService>()
            where TService : class
        {
            return _container.GetInstance<TService>();
        }
    }
    

    如您所见,这只是一个简单的包装器,但有助于向消费者隐藏真正的 DI 框架。 我希望这有助于了解您需要的实现。

    【讨论】:

    • 如果我理解正确,我将作业保留为范围 (services.Add(jobs.Select(jobType =&gt; new ServiceDescriptor(jobType, jobType, ServiceLifetime.Scoped)));) 并使用服务定位器创建单独的范围。我使用内置 DI,但不清楚如何实现 ServiceLocator 服务。我已经搜索并找到了基于 HttpContext.ApplicationServices 的定位服务。但 HttpContext 在此上下文中不可用(它与任何请求无关)。
    • 一旦我了解了服务定位器,我将测试提出的解决方案。谢谢。
    • @Alexei 作业不再限定范围,因为如果您打开范围并手动解决服务,则不需要它。
    • 是的,这是有道理的。我将重试提供的实现并让您知道。谢谢。
    • 因为我没有使用 SimpleInjector,所以我在 Startup.cs 中创建了一个静态引用,我使用它为每个作业创建一个新范围:QuartzScopedProvider = services.BuildServiceProvider() 并且服务定位器将使用它来创建新范围:_serviceScope = Startup.QuartzScopedProvider.CreateScope();。是否有意义?不管怎样,你的回答真的让我明白了事情是如何运作的。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-10-24
    • 2013-09-19
    相关资源
    最近更新 更多