#c# #quartz.net #asp.net-core-2.2
#c# #quartz.net #asp.net-core-2.2
Вопрос:
Мне трудно использовать Quartz 3.0.7 с ASP.NET Ядро 2.2 после того, как я определил два задания, которые полагаются на службу с ограниченной областью (ScopedDataAccess), которая является оболочкой для контекста моей базы данных:
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().Resu<
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 чтобы получить разные области для разных заданий?
Ответ №1:
Как я знаю, это невозможно с Quartz, я боролся с теми же проблемами, и единственным решением, которое я нашел, было использовать ServiceLocator и явно создать область в задании.
Я закончил чем-то вроде этого:
// 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 от потребителей.
Я надеюсь, что это немного поможет понять вашу необходимую реализацию.
Комментарии:
1. Если я правильно понял, я сохраняю задания как ограниченные (
services.Add(jobs.Select(jobType => new ServiceDescriptor(jobType, jobType, ServiceLifetime.Scoped)));
) и использую service locator для создания отдельной области. Я использую встроенный DI, но неясно, как реализовать ServiceLocator service. Я искал и нашел службы поиска на основе HttpContext. ApplicationServices. но HttpContext недоступен в этом контексте (он не связан ни с каким запросом).2. Я протестирую предлагаемое решение, как только разберусь с локатором служб. Спасибо.
3. @Alexey Задания больше не ограничены, потому что вам это не нужно, если вы открываете область и разрешаете службы вручную.
4. Да, это имеет смысл. Я повторю предоставленную реализацию и дам вам знать. Спасибо.
5. Поскольку я не использую SimpleInjector, я создал статическую ссылку в Startup.cs, которую я использую для создания новой области для каждого задания:
QuartzScopedProvider = services.BuildServiceProvider()
и service locator будет использовать это для создания новых областей:_serviceScope = Startup.QuartzScopedProvider.CreateScope();
. Имеет ли это смысл? В любом случае, ваш ответ действительно помог мне понять, как все работает.