【发布时间】:2022-01-26 07:47:47
【问题描述】:
我们使用 .NET Core 3.1 构建了一个 API,它从 Excel 中提取数据并通过 将 EF Core 导入 MS SQL 数据库。我们使用石英。 NET 以便在后台线程中处理它。对于 DI,我们使用 Autofac。
我们使用 Scoped Services 能够通过 DI 使用 DBContext(如此处所述 https://andrewlock.net/creating-a-quartz-net-hosted-service-with-asp-net-core/)。
很遗憾,当多个用户同时使用该应用程序时,保存数据仍然不起作用。我们收到以下错误消息:
The instance of entity type 'TABLENAME' cannot be tracked because another instance with the same key value for {'TABLEKEY'} is already being tracked. When attaching existing entities, ensure that only one entity instance with a given key value is attached. Consider using 'DbContextOptionsBuilder.EnableSensitiveDataLogging' to see the conflicting key values.
这里是我们的相关代码:
Startup.cs
// Add DbContext
services.AddDbContext<ApplicationDbContext>(options => options.UseSqlServer(Configuration.GetConnectionString("Default"), b => b.MigrationsAssembly("XY.Infrastructure")));
// Add Quartz
services.AddQuartz(q =>
{
// as of 3.3.2 this also injects scoped services (like EF DbContext) without problems
q.UseMicrosoftDependencyInjectionJobFactory();
// these are the defaults
q.UseSimpleTypeLoader();
q.UseDefaultThreadPool(tp =>
{
tp.MaxConcurrency = 24;
});
});
services.AddQuartzServer(options =>
{
// when shutting down we want jobs to complete gracefully
options.WaitForJobsToComplete = true;
});
// Add Services
services.AddHostedService<QuartzHostedService>();
services.AddSingleton<IJobFactory, SingletonJobFactory>();
services.AddSingleton<ISchedulerFactory, StdSchedulerFactory>();
services.AddTransient<ImportJob>();
services.AddScoped<IApplicationDbContext, ApplicationDbContext>();
services.AddScoped<IMyRepository, MyRepository>();
逻辑.cs
// Grab the Scheduler instance from the Factory
var factory = new StdSchedulerFactory();
var scheduler = await factory.GetScheduler();
var parameters = new JobDataMap()
{
new KeyValuePair<string, object>("request", message),
new KeyValuePair<string, object>("sales", sales),
};
var jobId = $"processJob{Guid.NewGuid()}";
var groupId = $"group{Guid.NewGuid()}";
// defines the job
IJobDetail job = JobBuilder.Create<ImportJob>()
.WithIdentity(jobId, groupId)
.UsingJobData(parameters)
.Build();
// defines the trigger
ITrigger trigger = TriggerBuilder.Create()
.WithIdentity($"Trigger{Guid.NewGuid()}", groupId)
.ForJob(job)
.StartNow()
.Build();
// schedule Job
await scheduler.ScheduleJob(job, trigger);
// and start it off
await scheduler.Start();
QuartzHostedService.cs
public class QuartzHostedService : IHostedService
{
private readonly ISchedulerFactory _schedulerFactory;
private readonly IJobFactory _jobFactory;
private readonly ILogger<QuartzHostedService> _logger;
private readonly IEnumerable<JobSchedule> _jobSchedules;
public QuartzHostedService(
ISchedulerFactory schedulerFactory,
IJobFactory jobFactory,
IEnumerable<JobSchedule> jobSchedules,
ILogger<QuartzHostedService> logger)
{
_schedulerFactory = schedulerFactory;
_jobSchedules = jobSchedules;
_jobFactory = jobFactory;
_logger = logger;
}
public IScheduler Scheduler { get; set; }
public async Task StartAsync(CancellationToken cancellationToken)
{
try
{
Scheduler = await _schedulerFactory.GetScheduler(cancellationToken);
Scheduler.JobFactory = _jobFactory;
foreach (var jobSchedule in _jobSchedules)
{
var job = CreateJob(jobSchedule);
var trigger = CreateTrigger(jobSchedule);
await Scheduler.ScheduleJob(job, trigger, cancellationToken);
}
await Scheduler.Start(cancellationToken);
}
catch (Exception ex)
{
_logger.LogError(ex.Message);
}
}
public async Task StopAsync(CancellationToken cancellationToken)
{
await Scheduler?.Shutdown(cancellationToken);
}
private static IJobDetail CreateJob(JobSchedule schedule)
{
var jobType = schedule.JobType;
return JobBuilder
.Create(jobType)
.WithIdentity(jobType.FullName)
.WithDescription(jobType.Name)
.Build();
}
private static ITrigger CreateTrigger(JobSchedule schedule)
{
return TriggerBuilder
.Create()
.WithIdentity($"{schedule.JobType.FullName}.trigger")
.StartNow()
.Build();
}
}
SingletonJobFactory.cs
public class SingletonJobFactory : IJobFactory
{
private readonly IServiceProvider _serviceProvider;
public SingletonJobFactory(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public IJob NewJob(TriggerFiredBundle bundle, IScheduler scheduler)
{
try
{
return _serviceProvider.GetRequiredService(bundle.JobDetail.JobType) as IJob;
}
catch (Exception ex)
{
throw;
}
}
public void ReturnJob(IJob job) { }
}
Importjob.cs
[DisallowConcurrentExecution]
public class ImportJob : IJob
{
private readonly IServiceProvider _provider;
private readonly ILogger<ImportJob> _logger;
public ImportJob(IServiceProvider provider, ILogger<ImportJob> logger)
{
_provider = provider;
_logger = logger;
}
public async Task Execute(IJobExecutionContext context)
{
try
{
using (var scope = _provider.CreateScope())
{
var jobType = context.JobDetail.JobType;
var job = scope.ServiceProvider.GetRequiredService(jobType) as IJob;
var repo = _provider.GetRequiredService<MYRepository>();
var importFactSales = _provider.GetRequiredService<IImportData>();
var savedRows = 0;
var request = (MyRequest)context.JobDetail.JobDataMap.Get("request");
var sales = (IEnumerable<MyData>)context.JobDetail.JobDataMap.Get("sales");
await importFactSales.saveValidateItems(repo, request, sales, savedRows);
}
}
catch (Exception ex)
{
_logger.LogError(ex.Message);
}
}
}
【问题讨论】:
-
您似乎有很多似乎不必要的自定义组件,您检查过github.com/quartznet/quartznet/blob/main/src/… 吗? Quartz 有内置的托管服务(services.AddQuartzServer()),你也不需要创建自己的工作工厂。为什么不通过作业构造函数注入像 MYRepository 这样的依赖项 - 不需要 IServiceProvider 或自定义范围?
-
@MarkoLahma 感谢您的评论。没有自己的作业工厂,我无法创建 IJob 实例(错误:无法实例化没有空构造函数的类型)。任何想法来解决这个问题?我想使用石英的内置托管服务。
-
如果您检查示例应用程序并遵循其结构,它应该可以工作。如您所见,该示例使用通过构造函数注入依赖项的作业。确保您也使用最新版本的 Quartz。可能是您手动添加作业吗,Quartz 回调具有
AddJob方法。如前所述,您在// Add Services下注册的与调度程序相关的所有内容似乎都是多余的。 -
@MarkoLahma 是的,作业是手动添加的(参见上面的 Logic.cs),我们使用 Autofac 作为 DI 库。我已经在使用最新版本 (3.3.3)。
-
@MarkoLahma 我还没有做对。我用最新的 Quartz 版本和 Autofac 创建了一个示例项目。该解决方案与原始解决方案具有相同的项目结构。我的代码在github.com/zugcorm/Quartz 下可用
标签: .net-core asp.net-core-3.1 quartz.net