【问题标题】:Quartz .NET The instance of entity type 'TABLENAME' cannot be tracked becauseQuartz .NET 无法跟踪实体类型“TABLENAME”的实例,因为
【发布时间】: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


【解决方案1】:

与此同时,我找到了解决方案。正如评论中描述的@marko-lahma,使用内置的托管服务,不要实现自己的 JobFactory。

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

【讨论】:

    猜你喜欢
    • 2019-10-28
    • 2023-01-03
    • 1970-01-01
    • 1970-01-01
    • 2017-06-30
    • 2018-06-20
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多