Quartz .NET 无法跟踪实体类型 'TABLENAME' 的实例,因为

Quartz .NET The instance of entity type 'TABLENAME' cannot be tracked because

我们使用 .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>();

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

同时我找到了解决办法。正如@marko-lahma 在评论中所述,使用built-in 托管服务,不要实现自己的 JobFactory。

  • 删除 SingletonJobFactory.cs 和 QuartzHostedService.cs
  • 使用 Autofac.Extras.Quartz 和 Quartz.Extensions.Hosting Nuget 包
  • 不要再使用 CreateScope,通过构造函数注入所有需要的依赖关系
  • 在 Startup 中注册 QuartzAutofacFactoryModule 和 QuartzAutofacJobsModule。