Ответ 1
TL; DR (полный ответ можно найти ниже)
Предполагаемое оснащение: RTM для Visual Studio 2017,.NET Core 1.1,.NET Core SDK 1.0, SQL Server Express 2016 LocalDB.
В веб-приложении .csproj:
<Project Sdk="Microsoft.NET.Sdk.Web">
<!-- .... existing contents .... -->
<!-- add the following ItemGroup element, it adds required packages -->
<ItemGroup>
<PackageReference Include="Quartz" Version="3.0.0-alpha2" />
<PackageReference Include="Quartz.Serialization.Json" Version="3.0.0-alpha2" />
</ItemGroup>
</Project>
В классе Program
(по умолчанию для Visual Studio по умолчанию):
public class Program
{
private static IScheduler _scheduler; // add this field
public static void Main(string[] args)
{
var host = new WebHostBuilder()
.UseKestrel()
.UseContentRoot(Directory.GetCurrentDirectory())
.UseIISIntegration()
.UseStartup<Startup>()
.UseApplicationInsights()
.Build();
StartScheduler(); // add this line
host.Run();
}
// add this method
private static void StartScheduler()
{
var properties = new NameValueCollection {
// json serialization is the one supported under .NET Core (binary isn't)
["quartz.serializer.type"] = "json",
// the following setup of job store is just for example and it didn't change from v2
// according to your usage scenario though, you definitely need
// the ADO.NET job store and not the RAMJobStore.
["quartz.jobStore.type"] = "Quartz.Impl.AdoJobStore.JobStoreTX, Quartz",
["quartz.jobStore.useProperties"] = "false",
["quartz.jobStore.dataSource"] = "default",
["quartz.jobStore.tablePrefix"] = "QRTZ_",
["quartz.jobStore.driverDelegateType"] = "Quartz.Impl.AdoJobStore.SqlServerDelegate, Quartz",
["quartz.dataSource.default.provider"] = "SqlServer-41", // SqlServer-41 is the new provider for .NET Core
["quartz.dataSource.default.connectionString"] = @"Server=(localdb)\MSSQLLocalDB;Database=Quartz;Integrated Security=true"
};
var schedulerFactory = new StdSchedulerFactory(properties);
_scheduler = schedulerFactory.GetScheduler().Result;
_scheduler.Start().Wait();
var userEmailsJob = JobBuilder.Create<SendUserEmailsJob>()
.WithIdentity("SendUserEmails")
.Build();
var userEmailsTrigger = TriggerBuilder.Create()
.WithIdentity("UserEmailsCron")
.StartNow()
.WithCronSchedule("0 0 17 ? * MON,TUE,WED")
.Build();
_scheduler.ScheduleJob(userEmailsJob, userEmailsTrigger).Wait();
var adminEmailsJob = JobBuilder.Create<SendAdminEmailsJob>()
.WithIdentity("SendAdminEmails")
.Build();
var adminEmailsTrigger = TriggerBuilder.Create()
.WithIdentity("AdminEmailsCron")
.StartNow()
.WithCronSchedule("0 0 9 ? * THU,FRI")
.Build();
_scheduler.ScheduleJob(adminEmailsJob, adminEmailsTrigger).Wait();
}
}
Пример класса задания:
public class SendUserEmailsJob : IJob
{
public Task Execute(IJobExecutionContext context)
{
// an instance of email service can be obtained in different ways,
// e.g. service locator, constructor injection (requires custom job factory)
IMyEmailService emailService = new MyEmailService();
// delegate the actual work to email service
return emailService.SendUserEmails();
}
}
Полный ответ
Кварц для .NET Core
Во-первых, вам нужно использовать v3 Quartz, поскольку он нацелен на .NET Core, согласно это объявление.
В настоящее время только альфа-версии пакетов v3 доступны в NuGet. Похоже, команда приложила немало усилий для выпуска 2.5.0, который не нацелен на .NET Core. Тем не менее, в своем репо GitHub ветвь master
уже посвящена v3, и в основном открытые проблемы для версии v3, похоже, не быть критическим, главным образом старыми предметами списка желаний, ИМХО. Поскольку недавняя фиксация активности довольно низкая, я ожидал бы, что выпуск v3 через несколько месяцев или, может быть, полгода - но никто не знает.
Работа и переработка IIS
Если веб-приложение будет размещено в IIS, вы должны принять во внимание поведение в процессе переработки/разгрузки рабочих процессов. Веб-приложение ASP.NET Core работает как обычный процесс .NET Core, отдельно от w3wp.exe - IIS работает только как обратный прокси. Тем не менее, когда экземпляр w3wp.exe перерабатывается или выгружается, связанный с ним процесс приложения .NET Core также сигнализируется о выходе (согласно this),
Веб-приложение также может быть самообслуживанием за обратным прокси-сервером, отличным от IIS (например, NGINX), но я предполагаю, что вы используете IIS и соответствующим образом сужаете свой ответ.
Проблемы, связанные с внедрением/разгрузкой, хорошо объясняются в должности, на которую ссылается @darin-dimitrov:
- Если, например, в пятницу 9:00 процесс не работает, поскольку за несколько часов до этого он был выгружен IIS из-за бездействия - электронные письма администратора не будут отправлены до тех пор, пока процесс не будет снова запущен. Чтобы этого избежать, настройте IIS, чтобы минимизировать выгрузку/рециркуляцию (см. Этот ответ).
- По моему опыту, приведенная выше конфигурация по-прежнему не дает 100% гарантии того, что IIS никогда не выгрузит приложение. Для 100% гарантии, что ваш процесс завершен, вы можете настроить команду, которая периодически отправляет запросы вашему приложению и, таким образом, сохраняет его в живых.
- Когда хост-процесс перезагружается/выгружается, задания должны быть изящно остановлены, чтобы избежать повреждения данных.
Зачем вам назначать запланированные задания в веб-приложении
Я могу думать об одном обосновании того, что эти задания электронной почты размещены в веб-приложении, несмотря на проблемы, перечисленные выше. Это решение иметь только одну модель приложения (ASP.NET). Такой подход упрощает кривую обучения, процедуру развертывания, мониторинг производства и т.д.
Если вы не хотите вводить бэкэнд-микросервисы (что было бы хорошим местом для перемещения заданий по электронной почте), тогда имеет смысл преодолеть поведение IIS по переработке/разгрузке и запустить Quartz внутри веб-приложения.
Или, может быть, у вас есть другие причины.
Постоянное хранилище заданий
В вашем сценарии статус выполнения задания должен быть сохранен вне процесса. Поэтому по умолчанию RAMJobStore не подходит, и вам нужно использовать хранилище заданий ADO.NET.
Поскольку вы упомянули SQL Server в вопросе, я приведу пример настройки базы данных SQL Server.
Как запустить (и изящно остановить) планировщик
Предполагаю, что вы используете Visual Studio 2017 и последнюю/последнюю версию инструментария .NET Core. Mine - это .NET Core Runtime 1.1 и .NET Core SDK 1.0.
В примере установки DB я использую базу данных с именем Quartz
в SQL Server 2016 Express LocalDB. Сценарии установки DB могут быть найдены здесь.
Сначала добавьте требуемые ссылки на пакеты для веб-приложения .csproj(или сделайте это с помощью графического интерфейса менеджера пакетов NuGet в Visual Studio):
<Project Sdk="Microsoft.NET.Sdk.Web">
<!-- .... existing contents .... -->
<!-- the following ItemGroup adds required packages -->
<ItemGroup>
<PackageReference Include="Quartz" Version="3.0.0-alpha2" />
<PackageReference Include="Quartz.Serialization.Json" Version="3.0.0-alpha2" />
</ItemGroup>
</Project>
С помощью Руководство по миграции и Учебное пособие по V3, мы можем выяснить, как запустить и остановить планировщик. Я предпочитаю инкапсулировать это в отдельный класс, назовите его QuartzStartup
.
using System;
using System.Collections.Specialized;
using System.Threading.Tasks;
using Quartz;
using Quartz.Impl;
namespace WebApplication1
{
// Responsible for starting and gracefully stopping the scheduler.
public class QuartzStartup
{
private IScheduler _scheduler; // after Start, and until shutdown completes, references the scheduler object
// starts the scheduler, defines the jobs and the triggers
public void Start()
{
if (_scheduler != null)
{
throw new InvalidOperationException("Already started.");
}
var properties = new NameValueCollection {
// json serialization is the one supported under .NET Core (binary isn't)
["quartz.serializer.type"] = "json",
// the following setup of job store is just for example and it didn't change from v2
["quartz.jobStore.type"] = "Quartz.Impl.AdoJobStore.JobStoreTX, Quartz",
["quartz.jobStore.useProperties"] = "false",
["quartz.jobStore.dataSource"] = "default",
["quartz.jobStore.tablePrefix"] = "QRTZ_",
["quartz.jobStore.driverDelegateType"] = "Quartz.Impl.AdoJobStore.SqlServerDelegate, Quartz",
["quartz.dataSource.default.provider"] = "SqlServer-41", // SqlServer-41 is the new provider for .NET Core
["quartz.dataSource.default.connectionString"] = @"Server=(localdb)\MSSQLLocalDB;Database=Quartz;Integrated Security=true"
};
var schedulerFactory = new StdSchedulerFactory(properties);
_scheduler = schedulerFactory.GetScheduler().Result;
_scheduler.Start().Wait();
var userEmailsJob = JobBuilder.Create<SendUserEmailsJob>()
.WithIdentity("SendUserEmails")
.Build();
var userEmailsTrigger = TriggerBuilder.Create()
.WithIdentity("UserEmailsCron")
.StartNow()
.WithCronSchedule("0 0 17 ? * MON,TUE,WED")
.Build();
_scheduler.ScheduleJob(userEmailsJob, userEmailsTrigger).Wait();
var adminEmailsJob = JobBuilder.Create<SendAdminEmailsJob>()
.WithIdentity("SendAdminEmails")
.Build();
var adminEmailsTrigger = TriggerBuilder.Create()
.WithIdentity("AdminEmailsCron")
.StartNow()
.WithCronSchedule("0 0 9 ? * THU,FRI")
.Build();
_scheduler.ScheduleJob(adminEmailsJob, adminEmailsTrigger).Wait();
}
// initiates shutdown of the scheduler, and waits until jobs exit gracefully (within allotted timeout)
public void Stop()
{
if (_scheduler == null)
{
return;
}
// give running jobs 30 sec (for example) to stop gracefully
if (_scheduler.Shutdown(waitForJobsToComplete: true).Wait(30000))
{
_scheduler = null;
}
else
{
// jobs didn't exit in timely fashion - log a warning...
}
}
}
}
Примечание 1. В приведенном выше примере SendUserEmailsJob
и SendAdminEmailsJob
являются классами, реализующими IJob
. Интерфейс IJob
немного отличается от IMyEmailService
, потому что он возвращает void Task
, а не Task<bool>
. Оба класса заданий должны получать IMyEmailService
как зависимость (возможно, инъекцию конструктора).
Примечание 2. Для долговременного задания, которое может быть выполнено своевременно, в методе IJob.Execute
он должен следить за состоянием IJobExecutionContext.CancellationToken
. Это может потребовать изменения в интерфейсе IMyEmailService
, чтобы его методы получили параметр CancellationToken
:
public interface IMyEmailService
{
Task<bool> SendAdminEmails(CancellationToken cancellation);
Task<bool> SendUserEmails(CancellationToken cancellation);
}
Когда и где запускать и останавливать планировщик
В ASP.NET Core код начальной загрузки приложения находится в классе Program
, что очень похоже на консольное приложение. Метод Main
вызывается для создания веб-хоста, запуска его и ожидания до его завершения:
public class Program
{
public static void Main(string[] args)
{
var host = new WebHostBuilder()
.UseKestrel()
.UseContentRoot(Directory.GetCurrentDirectory())
.UseIISIntegration()
.UseStartup<Startup>()
.UseApplicationInsights()
.Build();
host.Run();
}
}
Самое простое - просто поместить вызов QuartzStartup.Start
прямо в метод Main
, как и в TL; DR. Но так как мы также должны должным образом обрабатывать отключение процесса, я предпочитаю более точно использовать код запуска и завершения работы.
Эта строка:
.UseStartup<Startup>()
относится к классу с именем Startup
, который подкрашивается при создании нового проекта ASP.NET Core Web Application в Visual Studio. Класс Startup
выглядит следующим образом:
public class Startup
{
public Startup(IHostingEnvironment env)
{
// scaffolded code...
}
public IConfigurationRoot Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
// scaffolded code...
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
// scaffolded code...
}
}
Ясно, что вызов QuartzStartup.Start
должен быть вставлен в один из методов класса Startup
. Вопрос в том, где QuartzStartup.Stop
должен быть подключен.
В старой .NET Framework ASP.NET предоставил интерфейс IRegisteredObject
. В соответствии с этот пост и документация в ASP.NET Core был заменен на IApplicationLifetime
. Бинго. Экземпляр IApplicationLifetime
можно ввести в метод Startup.Configure
через параметр.
Для согласованности я привяжу оба QuartzStartup.Start
и QuartzStartup.Stop
к IApplicationLifetime
:
public class Startup
{
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(
IApplicationBuilder app,
IHostingEnvironment env,
ILoggerFactory loggerFactory,
IApplicationLifetime lifetime) // added this parameter
{
// the following 3 lines hook QuartzStartup into web host lifecycle
var quartz = new QuartzStartup();
lifetime.ApplicationStarted.Register(quartz.Start);
lifetime.ApplicationStopping.Register(quartz.Stop);
// .... original scaffolded code here ....
}
// ....the rest of the scaffolded members ....
}
Обратите внимание, что я расширил сигнатуру метода Configure
с помощью дополнительного параметра IApplicationLifetime
. Согласно документации, ApplicationStopping
будет блокироваться до тех пор, пока не будут завершены зарегистрированные обратные вызовы.
Изящное завершение работы в IIS Express и ядро ASP.NET Core
Мне удалось наблюдать ожидаемое поведение IApplicationLifetime.ApplicationStopping
только для IIS, с установленным последним модулем ASP.NET Core. И IIS Express (установленный с RTM для Visual Studio 2017), и IIS с устаревшей версией модуля ASP.NET Core не вызывали последовательно IApplicationLifetime.ApplicationStopping
. Я считаю, что из-за эта ошибка была исправлена.
Вы можете установить последнюю версию модуля ASP.NET Core здесь. Следуйте инструкциям в разделе "Установка последнего основного модуля ASP.NET".
Кварц против FluentScheduler
Я также взглянул на FluentScheduler, поскольку он был предложен в качестве альтернативной библиотеки @Brice Molesti. К моему первому впечатлению, FluentScheduler - довольно упрощенное и незрелое решение по сравнению с Quartz. Например, FluentScheduler не предоставляет таких фундаментальных функций, как сохранение статуса работы и кластерное выполнение.