Create job scheduling Quartz.NET hosting services easily based on the ASP.NET Core

In this article, I'll show you how to use ASP.NET Core hosting service run Quartz.NET jobs. The advantage is that we can easily control the operating status of our Job when the application starts and stops. Next, I will demonstrate how to create a simple IJob, a custom IJobFactoryand began to run in a runtime application QuartzHostedService. I will also introduce a number of issues requiring attention, namely the use of services in the scope singleton class.

Author: according to Le Wish

Starting address: https://www.cnblogs.com/yilezhu/p/12644208.html

Reference English Address: https://andrewlock.net/creating-a-quartz-net-hosted-service-with-asp-net-core/

Introduction - What is Quartz.NET?

At the beginning of what is introduced before Quartz.NET look at the following diagram that outlines all the basic core of content Quartz.NET.

Note: This picture shows get on Baidu aims to learn to share, if infringement, delete the contact.

Quartz.NET

The following from their website description:

Quartz.NET is a full-featured open source job scheduling system, from the most suitable for small applications to large enterprise systems.

For many ASP.NET developers, it is preferred methods used in the timer running background tasks in a reliable, cluster approach. Quartz.NET be used together with the ASP.NET Core also very similar - because Quartz.NET support .NET Standard 2.0, so you can easily use it in your application.

Quartz.NET There are two main concepts:

  • The Job . This is a background task that you Yaoan a specific timetable running.
  • Scheduler . It is responsible for trigger-based, time-based plan to run the job.

ASP.NET Core by hosting services has good support for running "a background task." Managed service when ASP.NET Core application starts to start and run the application life cycle inherent background. By creating Quartz.NET hosting service, you can use standard ASP.NET Core applications running tasks in the background.

Although you can create a "regular" back-office services (for example, every 10 minutes to run a task), but Quartz.NET provides a more robust solution. By using Cron triggers , you can ensure that the task only at certain times of the day (eg, 2:30 am) run, or only a specific operation in a few days, or any combination of the run. It also allows you to cluster run multiple instances of an application to run one instance (HA) only at any time.

In this article, I'll cover the basics of creating jobs and Quartz.NET scheduled to be on a timer running in hosted service.

Installation Quartz.NET

Quartz.NET is .NET Standard 2.0 NuGet package, it is very easy to install in your application. For this test, I created a ASP.NET Core project and select the Empty template. You can dotnet add package Quartzinstall Quartz.NET package. This time view of the project .csproj , shall be as follows:

<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>netcoreapp3.1</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Quartz" Version="3.0.7" />
  </ItemGroup>

</Project>

Create a IJob

For practical background work we are scheduled, we will pass to the injection ILogger<>to achieve the results and then output to the console) write "hello world" in. You must realize that contains a single asynchronous Execute()Quartz interface methods IJob. Note here we use the logger dependency injection is injected into the constructor.

using Microsoft.Extensions.Logging;
using Quartz;
using System;
using System.Threading.Tasks;

namespace QuartzHostedService
{
    [DisallowConcurrentExecution]
    public class HelloWorldJob : IJob
    {
        private readonly ILogger<HelloWorldJob> _logger;

        public HelloWorldJob(ILogger<HelloWorldJob> logger)
        {
            _logger = logger ?? throw new ArgumentNullException(nameof(logger));
        }

        public Task Execute(IJobExecutionContext context)
        {
            _logger.LogInformation("Hello world by yilezhu at {0}!",DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));
            return Task.CompletedTask;
        }
    }
}

I also used [DisallowConcurrentExecution]the properties decorated with the job. This property prevents Quartz.NET try to run the same job at the same time .

Create a IJobFactory

Next, we need to tell how to create Quartz IJobinstance. By default, Quartz will be used Activator.CreateInstanceto create job instance, so as to effectively call new HelloWorldJob(). Unfortunately, since we use constructor injection, and therefore does not work. Instead, we can provide a custom IJobFactoryhook to ASP.NET Core dependency injection container ( IServiceProvider) in:

using Microsoft.Extensions.DependencyInjection;
using Quartz;
using Quartz.Spi;
using System;

namespace QuartzHostedService
{
    public class SingletonJobFactory : IJobFactory
    {
        private readonly IServiceProvider _serviceProvider;

        public SingletonJobFactory(IServiceProvider serviceProvider)
        {
            _serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider));
        }

        public IJob NewJob(TriggerFiredBundle bundle, IScheduler scheduler)
        {
            return _serviceProvider.GetRequiredService(bundle.JobDetail.JobType) as IJob;
        }

        public void ReturnJob(IJob job)
        {
            
        }
    }
}

The plant will be a IServiceProviderpass constructor, and implement IJobFactorythe interface. Here is the most important method is the NewJob()method. In this method, the plant must return the requested Quartz scheduler IJob. In this implementation, entrusted to us directly IServiceProviderand let DI container to find the desired instance. Due to GetRequiredServicethe non-generic version returns an object, so we must be cast to the end IJob.

This ReturnJobmethod is the scheduler attempts to return (ie destroy) local jobs created by this factory. Unfortunately, the use of the built-in IServiceProvidermechanism has not done so. We can not create a suitable new API required for the Quartz IScopeService, we can only create a single case of a job.

This is very important. Using the above implementation, only create a single embodiment (or transient) the IJobimplementation is safe.

Configuration jobs

I am IJobhere to show only a realization, but we hope to achieve universal Quartz hosting service is applicable to any number of jobs. To solve this problem, we created a simple DTO JobSchedule, is used to define a given job type of timer program:

using System;
using System.ComponentModel;

namespace QuartzHostedService
{
    /// <summary>
    /// Job调度中间对象
    /// </summary>
    public class JobSchedule
    {
        public JobSchedule(Type jobType, string cronExpression)
        {
            this.JobType = jobType ?? throw new ArgumentNullException(nameof(jobType));
            CronExpression = cronExpression ?? throw new ArgumentNullException(nameof(cronExpression));
        }
        /// <summary>
        /// Job类型
        /// </summary>
        public Type JobType { get; private set; }
        /// <summary>
        /// Cron表达式
        /// </summary>
        public string CronExpression { get; private set; }
        /// <summary>
        /// Job状态
        /// </summary>
        public JobStatus JobStatu { get; set; } = JobStatus.Init;
    }

    /// <summary>
    /// Job运行状态
    /// </summary>
    public enum JobStatus:byte
    {
        [Description("初始化")]
        Init=0,
        [Description("运行中")]
        Running=1,
        [Description("调度中")]
        Scheduling = 2,
        [Description("已停止")]
        Stopped = 3,

    }
}

Here JobTypeis the job of the .NET type (in our example is HelloWorldJob), and CronExpressionis a Quartz.NET the Cron expression . Cron expressions allow complex scheduling a timer so you can set up complex rules below, for example, "Monthly No. 5 and No. 20 is triggered once every half hour between 8:00 to 10:00." Just be sure to check the document can, because not all operating systems Cron expressions are used interchangeably.

We will work to add DI and Startup.ConfigureServices()configure their schedule:

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Quartz;
using Quartz.Impl;
using Quartz.Spi;

namespace QuartzHostedService
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            //添加Quartz服务
            services.AddSingleton<IJobFactory, SingletonJobFactory>();
            services.AddSingleton<ISchedulerFactory, StdSchedulerFactory>();
            //添加我们的Job
            services.AddSingleton<HelloWorldJob>();
            services.AddSingleton(
                 new JobSchedule(jobType: typeof(HelloWorldJob), cronExpression: "0/5 * * * * ?")
           );
        }
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
           ......
        }
    }
}

This code is added to the contents as a single four to Example DI container:

  • SingletonJobFactory Is described earlier, it is used to create the job instance.
  • One ISchedulerFactoryimplementation, using the built-in StdSchedulerFactory, it can handle the job scheduling and management
  • The HelloWorldJobjob itself
  • A type HelloWorldJob, and comprises a five-second run once Cron expression JobScheduleinstantiating objects.

Now that we have completed most of the basic work, just missing a combined them together, QuartzHostedServicethe.

Creating QuartzHostedService

This QuartzHostedServiceis IHostedServicean implementation of a set Quartz scheduler, and enable it to run in the background. Because Quartz design, we can IHostedServicedirectly implement it, rather than from the base BackgroundServicederive more common methods class . The complete code for the services listed below, I will later be described in detail.

using Microsoft.Extensions.Hosting;
using Quartz;
using Quartz.Spi;
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;

namespace QuartzHostedService
{
    public class QuartzHostedService : IHostedService
    {
        private readonly ISchedulerFactory _schedulerFactory;
        private readonly IJobFactory _jobFactory;
        private readonly IEnumerable<JobSchedule> _jobSchedules;

        public QuartzHostedService(ISchedulerFactory schedulerFactory, IJobFactory jobFactory, IEnumerable<JobSchedule> jobSchedules)
        {
            _schedulerFactory = schedulerFactory ?? throw new ArgumentNullException(nameof(schedulerFactory));
            _jobFactory = jobFactory ?? throw new ArgumentNullException(nameof(jobFactory));
            _jobSchedules = jobSchedules ?? throw new ArgumentNullException(nameof(jobSchedules));
        }
        public IScheduler Scheduler { get; set; }

        public async Task StartAsync(CancellationToken cancellationToken)
        {
            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);
                jobSchedule.JobStatu = JobStatus.Scheduling;
            }
            await Scheduler.Start(cancellationToken);
            foreach (var jobSchedule in _jobSchedules)
            {
                jobSchedule.JobStatu = JobStatus.Running;
            }
        }

        public async Task StopAsync(CancellationToken cancellationToken)
        {
            await Scheduler?.Shutdown(cancellationToken);
            foreach (var jobSchedule in _jobSchedules)
            {
             
                jobSchedule.JobStatu = JobStatus.Stopped;
            }
        }

        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")
                .WithCronSchedule(schedule.CronExpression)
                .WithDescription(schedule.CronExpression)
                .Build();
        }
    }
}

The QuartzHostedServicethree interdependent dependency: we are Startupconfigured in ISchedulerFactoryand IJobFactorythere is a IEnumerable<JobSchedule>. We only add to a DI container JobScheduleobjects (that is HelloWorldJob), but if you sign up for more work planned in DI container, they will all inject here (of course, you can also be obtained through the database, coupled with UI control , is not to achieve a visual background scheduling of it? own imagination it ~).

StartAsyncThe method is called when the application starts, so here is our configuration Quartz place. Our a first ISchedulerinstance, to assign attributes for later use, and then injected JobFactoryExamples provided to the dispatcher:

 public async Task StartAsync(CancellationToken cancellationToken)
        {
            Scheduler = await _schedulerFactory.GetScheduler(cancellationToken);
            Scheduler.JobFactory = _jobFactory;
            ...
        }

Next, we loop injection operations plan and use defined at the end of class for each job CreateJoband CreateTriggerassist in the creation of a method of Quartz IJobDetailand ITrigger. If you do not like this part of the work, or need to configure more control, you can expand as needed JobScheduleto easily customize it DTO.

public async Task StartAsync(CancellationToken cancellationToken)
{
    // ...
   foreach (var jobSchedule in _jobSchedules)
            {
                var job = CreateJob(jobSchedule);
                var trigger = CreateTrigger(jobSchedule);
                await Scheduler.ScheduleJob(job, trigger, cancellationToken);
                jobSchedule.JobStatu = JobStatus.Scheduling;
            }
    // ...
}

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")
        .WithCronSchedule(schedule.CronExpression)
        .WithDescription(schedule.CronExpression)
        .Build();
}

Finally, once all jobs have been scheduled, you can call it Scheduler.Start()to deal with the actual start Quartz.NET planning process in the background. When the application is closed, the framework will call StopAsync(), and you can call Scheduler.Stop()in order to safely shut down the scheduler process.

public async Task StopAsync(CancellationToken cancellationToken)
{
    await Scheduler?.Shutdown(cancellationToken);
}

You can use AddHostedService()an extension method in managed services Startup.ConfigureServicesinto our back office services:

public void ConfigureServices(IServiceCollection services)
{
    // ...
    services.AddHostedService<QuartzHostedService>();
}

If you run the application, you should see once every 5 seconds to run background tasks and write the console (or configure logging anywhere)

image-20200406151153107

Use the service scope in the job

There is described in this article to achieve a big problem: You can only create Singleton or Transient job. This means you can not use registered for the scope of any service dependencies. For example, you will not be able EF Core of DatabaseContextinjecting your IJobimplementation, because you will encounter Captive Dependency problem.

It is not hard to solve this problem: You can inject IServiceProviderand create your own scope. For example, if you need to HelloWorldJobuse the service scope, you can use the following:

public class HelloWorldJob : IJob
{
    // 注入DI provider
    private readonly IServiceProvider _provider;
    public HelloWorldJob( IServiceProvider provider)
    {
        _provider = provider;
    }

    public Task Execute(IJobExecutionContext context)
    {
        // 创建一个新的作用域
        using(var scope = _provider.CreateScope())
        {
            // 解析你的作用域服务
            var service = scope.ServiceProvider.GetService<IScopedService>();
            _logger.LogInformation("Hello world by yilezhu at {0}!",DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));
        }

        return Task.CompletedTask;
    }
}

This ensures that both create a new scope each time you run the job, so you can IJobretrieve (and deal with) the scope of service. Unfortunately, the wording is indeed some confusion. In the next article, I'll show you another way to achieve a more elegant, it is more concise, interested can follow the first time in "DotNetCore real" public number for updates.

to sum up

In this article, I introduced Quartz.NET, and shows how to use it in ASP.NET Core of IHostedServicethe scheduling background jobs. The example shown in this article the most suitable singleton or transient operation, which is not ideal, because the use of the service scope is very clumsy. In the next article, I'll show you another way to achieve a more elegant, it is more concise, and makes it easier to use the service scope, interested can follow the first time in "DotNetCore real" public number for updates.

Guess you like

Origin www.cnblogs.com/yilezhu/p/12644208.html