استفاده از Quartz.Net با Asp.Net و worker services

مقدمه(از net 6. استفاده شده)فرض کنین یک سایت فروشگاهی دارین و می خواین سبد های خریدی که 7 روز از ثبت اونا گذشته و نهایی نشدن رو از دیتابیس حذف کنین. برای این کار هر شب ساعت 24 لیست سبد های خریدی که باز هستن و از ثبت اونها 7 روز گذشته رو از دیتابیس واکشی و اونا رو حذف می کنین.یا به طور کلی فرض کنین می خواین برنامه تون جدای از کار هایی که با دستور کاربر انجام می شن، خودش بیاد و یک سری کار هایی که شما براش تعریف کردین رو اون زیر براتون اجرا کنه. به اون کار هایی که اون زیر انجام می شن Background Tasks می گن.در ASP.NET Core اون کار ها می تونن به عنوان hosted services پیاده سازی بشن. هر کلاسی که اینترفیس Microsoft.Extensions.Hosting.IHostedService رو پیاده سازی بکنه یک hosted service محسوب میشه.public class ExampleHostedService : IHostedService { public async Task StartAsync(CancellationToken stoppingToken) { while(!stoppingToken.IsCancellationRequested) { Console.WriteLine(&quotExample Hosted Service is working.&quot); await Task.Delay(2000, stoppingToken); } await Task.CompletedTask; } public Task StopAsync(CancellationToken stoppingToken) { return Task.CompletedTask; } } کلاس بالا یک hosted service محسوب میشه و بعد از اینکه این سرویس رو با استفاده از اکستنشن متد AddHostedService به سرویس های برنامه مون اضافه کردیم می تونیم ازش استفاده کنیم.Program.cs: اگر پروژه وب دارینvar builder = WebApplication.CreateBuilder(args); builder.Services.AddHostedService<ExampleHostedService>();Program.cs: اگر پروژه غیر وب دارینIHost host = Host.CreateDefaultBuilder(args) .ConfigureServices(services => { services.AddHostedService<ExampleHostedService>(); }).Build();دو متد StartAsync و StopAsync متعلق به IHostedService هستند. بعد از اجرای برنامه متد StartAsync به طور خودکار فراخوانی میشه. چیزی که ما در این متد تعریف کردیم میاد هر 2 ثانیه یک بار یک متنی تو کنسول چاپ می کنه. اگر هاستی در حال اجرا باشه و یک graceful shutdown داشته باشه متد StopAsync اجرا میشه. graceful shutdown هم یعنی مثلا با CancellationToken کاری که در حال اجراست کنسل بشه یا هاست رو Stop یا Dispose کنیم. ولی اگر فراید اجرای برنامه مختل بشه و به صورت ناخواسته برنامه متوقف بشه دیگه graceful نیست و ممکنه StopAsync اجرا نشه.حالا اگر بخوایم Background Task هامون رو با یک برنامه ریزی که خودمون تعریف می کنیم اجرا بشن، یا اینکه چند کار همزمان اجرا بشه باید از job scheduler استفاده کنیم. اگر چند Background Task داشته باشیم به ترتیبی که به سرویس های پروژه اضافه شدن اجرا می شن.تو job scheduler ها شما می تونی تعیین کنی یک کار در یک روز خاص و در یک ساعت خاص انجام بشه. مثلا یک کاری ساعت 11:30و 12:30و 13:30 هر 4شنبه و جمعه اجرا بشه. جلوتر در مورد نهوه برنامه ریزی صحبت میشه و اونجاست که انعطاف پذیزی این نوع برنامه ریزی رو درک می کنید.برای انجام هم زمان کار ها: اگر دو Background Task داشته باشیم:public class ExampleHostedService1 : IHostedService { public async Task StartAsync(CancellationToken stoppingToken) { int i = 0; while(!stoppingToken.IsCancellationRequested && i != 5) { Console.WriteLine(&quotExample Hosted Service1 is working.&quot); i++; await Task.Delay(2000, stoppingToken); } await Task.CompletedTask; } public Task StopAsync(CancellationToken stoppingToken) { return Task.CompletedTask; } }public class ExampleHostedService2 : IHostedService { public async Task StartAsync(CancellationToken stoppingToken) { int i = 0; while(!stoppingToken.IsCancellationRequested && i != 5) { Console.WriteLine(&quotExample Hosted Service2 is working.&quot); i++; await Task.Delay(2000, stoppingToken); } await Task.CompletedTask; } public Task StopAsync(CancellationToken stoppingToken) { return Task.CompletedTask; } }و به صورت زیر به سرویس های برنامه اضافه شده باشن:builder.Services.AddHostedService<ExampleHostedService1>(); builder.Services.AddHostedService<ExampleHostedService2>();خروجی به این صورت میشه:و اگر به صورت زیر به سرویس های برنامه اضافه شده باشن:builder.Services.AddHostedService<ExampleHostedService2>(); builder.Services.AddHostedService<ExampleHostedService1>(); خروجی به صورت زیر میشه:احرولی در quartz کار ها بر اساس زمان بندی اجرا می شن. اگر زمان بندی دو کار یکسان باشه، اون دو کار با هم اجرا می شن.Quartz.Net چیست ؟Quartz یک سیستم زمان بندی کار ها است که به صورت open source برای .net ارائه شده. quartz سه مفهوم اصلی داره :job : این همون Background Task هست می خوایم اجرا بشه.trigger : یک trigger کنترل می کنه کی یک job اجرا بشه. trigger یک زمان بندی می گیره، سر زمان ماشه رو می کشه(trigger یعنی ماشه). یک کار می تونه چند trigger داشته باشه. مثلا یک کار می تونه هم اول هر ماه اجرا بشه و هم روز 4ام هر هفته!.scheduler : همه چیز زیر سر اینه. هماهنگی کار ها و trigger ها بر عهده scheduler هست. به قولی طبق نیاز trigger کار رو اجرا می کنه. trigger سر زمان بندی شلیک میکنه، scheduler کار مربوط به اون trigger رو اجرا می کنه.ایجاد پروژهدر ویژوال استدیو یا هر چیزی که صلاح می دونید یک پروژه با قالب Worker Service بسازین. شاید شما در حال حاظر یک پروژه دارین و به job scheduler نیاز پیدا کردین؛ در سلوشن همون پروژه یک پروژه با قالب Worker Service بسازین. در نهایت ما برای کار هامون با پروژه Worker Service سر و کله می زنیم.کلاس Worker رو حذف می کنیم. در Program.cs هم ارور ها رو پاک می کنیم.در visual studio اگر Alt+t+n+n رو بزنیم NuGet باز میشه. از اونجا پکیج Quartz.Extensions.Hosting رو روی پروژه Worker Service نصب می کنیم.اضافه کردن Quartz.net Hosted Service در Program.cs کد زیر رو اضافه می کنیم:using Quartz; IHost host = Host.CreateDefaultBuilder(args) .ConfigureServices((hostContext, services) => { services.AddQuartz(q => { q.UseMicrosoftDependencyInjectionJobFactory(); }); services.AddQuartzHostedService( q => q.WaitForJobsToComplete = true); }) .Build(); await host.RunAsync();AddQuartz برای اینه که ما Quartz رو به سرویس های پروژه مون اضافه کنیم و یک سری تنظیمات رو به اون اعمال کنیم. مثل همین UseMicrosoftDependencyInjectionJobFactory که به ما اجازه میده از سرویس هایی که به پروژه مون اضافه کردیم با هر لایف تایمی بتونیم در Job هامون استفاده کنیم. AddQuartzHostedService هم برای اینه که به برنامه بگیم ما می خوایم Quartz به عنوان HostedService اجرا بشه. جور دیگه هم می شه اجرا بشه ؟ بله؛ Quartz به HostedService گره نخورده و ما می تونیم کار ها مون رو دستی هم اجرا کنیم. برای اطلاعات بیشتر در این مورد به این مقاله یک نگاهی بندازین. اگر به کد های موجود یک نگاه اجمالی داشته باشیم یک جایی میبینیم گفته await scheduler.Start(); که نشون میده این فرآیند می تونه دستی هم صورت بگیره. WaitForJobsToComplete هم برای اینه که اگر یک وقت درخواست دادیم که Scheduler مون خاموش بشه(await scheduler.Shutdown();)، اگر کاری در همون حین در حال اجرا بود، Quartz صبر می کنه تا اون تموم بشه بعد Scheduler رو خاموش می کنه.ساختن IJobیک کلاس ایجاد می کنیم و از اینترفیس IJob ارث بریش میدیم( :-) ) و اونو پیاده سازی می کنیم. using Quartz; namespace WorkerService; public class HelloWorldJob : IJob { public Task Execute(IJobExecutionContext context) { throw new NotImplementedException(); } }public Task Execute(IJobExecutionContext context) { Console.WriteLine(&quotHello world!&quot); return Task.CompletedTask; }متد Execute متعلق به IJob هست. بقیه چیزا هم که واضحه.Configuring Jobکد های زیر رو به AddQuartz اضافه می کنیم.var jobKey = new JobKey(&quotHelloWorldJob&quot); q.AddJob<HelloWorldJob>(j => j.WithIdentity(jobKey)); q.AddTrigger(options => options .ForJob(jobKey) .WithIdentity(&quotHelloWorldJob-trigger&quot) .WithCronSchedule(&quot0/5 * * * * ?&quot));شکل نهایی Program.cs:using Quartz; IHost host = Host.CreateDefaultBuilder(args) .ConfigureServices((hostContext, services) => { services.AddQuartz(q => { q.UseMicrosoftDependencyInjectionJobFactory(); var jobKey = new JobKey(&quotHelloWorldJob&quot); q.AddJob<HelloWorldJob>(j => j.WithIdentity(jobKey)); q.AddTrigger(options => options .ForJob(jobKey) .WithCronSchedule(&quot0/5 * * * * ?&quot)); }); services.AddQuartzHostedService( q => q.WaitForJobsToComplete = true); }) .Build(); await host.RunAsync();jobKey حکم کد ملی رو برای Job هامون داره و برای هر Job باید یک jobKey یکتا درست کنیم. وظیفش هم وصل کردن job به trigger مربوطش هست. بعد به Quartz می گیم HelloWorldJob رو با jobKey که مشخص کردیم به عنوان یک Job به خودت اضافش کن. بعد برای اون jobKey که درست کردیم یک trigger درست میکنیم. گفتیم trigger یک زمان بندی میگیره و سر اون زمان بندی شلیک می کنه. ما این زمان بندی رو به وسیله cron expressions به trigger میدیم. cron expressions یک رشته از یک سری کاراکتر مجاز هستن که در این مقاله و این مقاله شما می تونید به طور نسبتن کاملی با این عبارات آشنا بشید. در اینجا هر 5 ثانیه یک بار شلیک می کنه.حالا روی Solution راست کلیک کنین و Properties رو بزنین.Multiple startup project رو بزنین پروژه وب و Worker تون رو در حالت Startبزارین. اینطوری وقتی برنامه تون اجرا میشه هردو تا پروژه با هم اجرا می شن.حالا اگر برنامه رو اجرا کنین می بینین همه چیز درست کار می کنه.کنسول چپ متعلق به WebApplication هست و راستی مال Worker Service. تمیز کاریبه appsettings.json پروژه Worker Service خطوط زیر رو اضافه کنین:&quotQuartz&quot: { &quotHelloWorldJob&quot: &quot0/5 * * * * ?&quot }یک کلاس به اسم ServiceCollectionQuartzConfiguratorExtensions بسازین:public static class ServiceCollectionQuartzConfiguratorExtensions { public static void AddJobAndTrigger<T>( this IServiceCollectionQuartzConfigurator quartz, IConfiguration config) where T : IJob { string jobName = typeof(T).Name; var configKey = $&quotQuartz:{jobName}" var cronSchedule = config[configKey]; if (string.IsNullOrEmpty(cronSchedule)) { throw new Exception($&quotNo Quartz.NET Cron schedule found for job in configuration at {configKey}&quot); } var jobKey = new JobKey(jobName); quartz.AddJob<T>(opts => opts.WithIdentity(jobKey)); quartz.AddTrigger(opts => opts .ForJob(jobKey) .WithCronSchedule(cronSchedule)); } }Program.cs شما به این صورت در میاد:using Quartz; using WorkerService; IHost host = Host.CreateDefaultBuilder(args) .ConfigureServices((hostContext, services) => { services.AddQuartz(q => { q.UseMicrosoftDependencyInjectionJobFactory(); q.AddJobAndTrigger<HelloWorldJob>(hostContext.Configuration); }); services.AddQuartzHostedService( q => q.WaitForJobsToComplete = true); }) .Build(); await host.RunAsync();حالا اگر یک Job دیگه داشته باشیم تنها کاری که باید بکنیم اینه که اون job رو با Extension method AddJobAndTrigger به Quartz اضافه کنیم و cron expression اون job رو در appsettings.json به اسم همون job اضافه کنیم. مثلا اگر job زیر رو داشته باشیم:public class DeleteExpiredCart : IJob { private readonly DataContext _dataContext; public DeleteExpiredCart(DataContext dataContext) { _dataContext = dataContext; } public Task Execute(IJobExecutionContext context) { var carts = _dataContext.Carts; foreach(var cart in carts) { if(DateTime.Now.Day - cart.DateOfRegistration.Day > 7) { _dataContext.Carts.Remove(cart); } } _dataContext.SaveChanges(); return Task.CompletedTask; } }خط زیر رو به Program.cs اضافه می کنیم:q.AddJobAndTrigger<DeleteExpiredCart>(hostBuilderContext.Configuration);services.AddQuartz(q => { q.UseMicrosoftDependencyInjectionJobFactory(); q.AddJobAndTrigger<HelloWorldJob>(hostBuilderContext.Configuration); q.AddJobAndTrigger<DeleteExpiredCart>(hostBuilderContext.Configuration); });خط زیر رو هم به appsettings.json اضافه می کنیم:&quotDeleteExpiredCart&quot: &quot0 0 0 * * ?&quot&quotQuartz&quot: { &quotHelloWorldJob&quot: &quot0/5 * * * * ?&quot, &quotDeleteExpiredCart&quot: &quot0 0 0 * * ?&quot }بعد از اجرای برنامه هر 5 ثانیه یک متن در کنسول چاپ میشه و هر شب ساعت 24 سبد های خرید منقضی شده از دیتابیس حذف می شن.کد نهایی پروژه رو می تونید از این ریپازیتوری دریافت کنین.