tag route net form asp asp.net-core asp.net-core-mvc quartz.net

asp.net-core - route - select asp net core



¿Cómo iniciar Quartz en ASP.NET Core? (4)

TL; DR (respuesta completa se puede encontrar a continuación)

Herramientas asumidas: Visual Studio 2017 RTM, .NET Core 1.1, .NET Core SDK 1.0, SQL Server Express 2016 LocalDB.

En la aplicación web .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>

En la clase de Program (como se describe por defecto en 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(); } }

Un ejemplo de una clase de trabajo:

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(); } }

Respuesta completa

Cuarzo para .NET Core

Primero, tienes que usar v3 de Quartz, ya que apunta a .NET Core, de acuerdo con quartz-scheduler.net/2016/08/16/… .

Actualmente, solo las versiones alfa de los paquetes v3 están nuget.org/packages/Quartz/3.0.0-alpha2 . Parece que el equipo hizo un gran esfuerzo para lanzar 2.5.0, que no tiene como objetivo .NET Core. Sin embargo, en su repo de GitHub, la rama master ya está dedicada a v3, y básicamente, los problemas abiertos para la versión v3 no parecen ser críticos, en su mayoría son elementos antiguos de la lista de deseos, IMHO. Dado que la actividad de confirmación reciente es bastante baja, esperaría un lanzamiento de v3 en pocos meses, o quizás medio año, pero nadie lo sabe.

Empleos y reciclaje de IIS

Si la aplicación web se va a alojar en IIS, debe tener en cuenta el comportamiento de reciclaje / descarga de los procesos de trabajo. La aplicación web ASP.NET Core se ejecuta como un proceso regular de .NET Core, independiente de w3wp.exe: IIS solo sirve como proxy inverso. Sin embargo, cuando una instancia de w3wp.exe se recicla o descarga, el proceso de la aplicación .NET Core relacionado también se señala para salir (de acuerdo con this ).

La aplicación web también puede ser auto hospedada detrás de un proxy inverso que no es IIS (por ejemplo, NGINX), pero asumiré que usted usa IIS, y limitaré mi respuesta en consecuencia.

Los problemas que introduce el reciclaje / descarga se explican bien en la haacked.com/archive/2011/10/16/… :

  • Si, por ejemplo, el viernes a las 9:00, el proceso está inactivo, debido a que varias horas antes fue descargado por IIS debido a la inactividad, no se enviarán correos electrónicos de administración hasta que el proceso se reanude. Para evitar eso, configure IIS para minimizar las descargas / reciclados ( vea esta respuesta ).
    • Desde mi experiencia, la configuración anterior todavía no ofrece una garantía del 100% de que IIS nunca descargará la aplicación. Para garantizar al 100% que su proceso está activo, puede configurar un comando que periódicamente envía solicitudes a su aplicación, y por lo tanto lo mantiene vivo.
  • Cuando el proceso del host se recicla / descarga, los trabajos deben detenerse con gracia, para evitar la corrupción de datos.

¿Por qué alojar trabajos programados en una aplicación web?

Puedo pensar en una justificación de tener esos trabajos de correo electrónico alojados en una aplicación web, a pesar de los problemas mencionados anteriormente. Es decisión tener solo un tipo de modelo de aplicación (ASP.NET). Este enfoque simplifica la curva de aprendizaje, el procedimiento de despliegue, el monitoreo de la producción, etc.

Si no desea introducir microservicios backend (que sería un buen lugar para mover los trabajos de correo electrónico), entonces tiene sentido superar los comportamientos de reciclaje / descarga de IIS y ejecutar Quartz dentro de una aplicación web.

O tal vez tienes otras razones.

Tienda de trabajo persistente

En su escenario, el estado de la ejecución del trabajo debe mantenerse fuera de proceso. Por lo tanto, RAMJobStore predeterminado no se ajusta, y tiene que usar el almacén de trabajos ADO.NET .

Como mencionó a SQL Server en la pregunta, proporcionaré una configuración de ejemplo para la base de datos de SQL Server.

Cómo iniciar (y detener con gracia) el programador

Supongo que utiliza Visual Studio 2017 y la versión más reciente / reciente de las herramientas de .NET Core. El mío es .NET Core Runtime 1.1 y .NET Core SDK 1.0.

Para el ejemplo de configuración de la base de datos, Quartz una base de datos llamada Quartz en SQL Server 2016 Express LocalDB. Los scripts de configuración de DB se pueden encontrar aquí .

Primero, agregue las referencias de paquetes requeridas a la aplicación web .csproj (o hágalo con la GUI del administrador de paquetes de NuGet en 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>

Con la ayuda de la quartz-scheduler.net/documentation/quartz-3.x/… de quartz-scheduler.net/documentation/quartz-3.x/… y el quartz-scheduler.net/documentation/quartz-3.x/tutorial/… , podemos descubrir cómo iniciar y detener el programador. Prefiero encapsular esto en una clase separada, llamémoslo 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... } } } }

Nota 1. En el ejemplo anterior, SendUserEmailsJob y SendAdminEmailsJob son clases que implementan IJob . La interfaz de IJob es ligeramente diferente de IMyEmailService , ya que devuelve Task nula y no Task<bool> . Ambas clases de trabajo deberían obtener IMyEmailService como una dependencia (probablemente inyección del constructor).

Nota 2. Para que un trabajo de larga duración pueda salir de manera oportuna, en el método IJob.Execute , debe observar el estado de IJobExecutionContext.CancellationToken . Esto puede requerir un cambio en la interfaz de IMyEmailService , para que sus métodos reciban el parámetro CancellationToken :

public interface IMyEmailService { Task<bool> SendAdminEmails(CancellationToken cancellation); Task<bool> SendUserEmails(CancellationToken cancellation); }

¿Cuándo y dónde iniciar y detener el programador?

En ASP.NET Core, el código bootstrap de la aplicación reside en la clase Program , al igual que en la aplicación de consola. Se llama al método Main para crear un host web, ejecutarlo y esperar hasta que salga:

public class Program { public static void Main(string[] args) { var host = new WebHostBuilder() .UseKestrel() .UseContentRoot(Directory.GetCurrentDirectory()) .UseIISIntegration() .UseStartup<Startup>() .UseApplicationInsights() .Build(); host.Run(); } }

Lo más sencillo es llamar a QuartzStartup.Start desde el método Main , como lo hice en TL; DR. Pero como también tenemos que manejar adecuadamente el cierre del proceso, prefiero enganchar los códigos de inicio y apagado de una manera más consistente.

Esta línea:

.UseStartup<Startup>()

se refiere a una clase llamada Startup , que se crea como andamio cuando se crea un nuevo proyecto de aplicación web principal ASP.NET en Visual Studio. La clase de Startup ve así:

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... } }

Está claro que una llamada a QuartzStartup.Start debe insertarse en uno de los métodos en la clase de Startup . La pregunta es, donde QuartzStartup.Stop debe estar enganchado.

En el .NET Framework heredado, ASP.NET proporcionó la interfaz IRegisteredObject . Según esta publicación , y la documentation , en ASP.NET Core se reemplazó con IApplicationLifetime . Bingo. Una instancia de IApplicationLifetime se puede inyectar en el método Startup.Configure mediante un parámetro.

Por coherencia, engancharé tanto QuartzStartup.Start como QuartzStartup.Stop a 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 .... }

Tenga en cuenta que he extendido la firma del método Configure con un parámetro IApplicationLifetime adicional. Según la documentation , ApplicationStopping se bloqueará hasta que se completen las devoluciones de llamada registradas.

Cierre elegante en IIS Express y en el módulo Core de ASP.NET

Pude observar el comportamiento esperado del gancho IApplicationLifetime.ApplicationStopping solo en IIS, con el último módulo Core de ASP.NET instalado. Tanto IIS Express (instalado con Visual Studio 2017 Community RTM) como IIS con una versión obsoleta del módulo Core de ASP.NET no IApplicationLifetime.ApplicationStopping sistemáticamente IApplicationLifetime.ApplicationStopping . Creo que es debido a este error que se ha solucionado.

Puede instalar la última versión del módulo Core de ASP.NET desde aquí . Siga las instrucciones en la sección "Instalación del último Módulo Core de ASP.NET" .

Cuarzo vs. FluentScheduler

También eché un vistazo a FluentScheduler, ya que fue propuesto como una biblioteca alternativa por @Brice Molesti. Para mi primera impresión, FluentScheduler es una solución bastante simplista e inmadura, en comparación con el cuarzo. Por ejemplo, FluentScheduler no proporciona características fundamentales como la persistencia del estado del trabajo y la ejecución en clúster.

Tengo la siguiente clase

public class MyEmailService { public async Task<bool> SendAdminEmails() { ... } public async Task<bool> SendUserEmails() { ... } } public interface IMyEmailService { Task<bool> SendAdminEmails(); Task<bool> SendUserEmails(); }

He instalado el último paquete Nuget de Quartz 2.4.1, ya que quería un programador ligero en mi aplicación web sin una base de datos de SQL Server separada.

Necesito programar los métodos.

  • SendUserEmails para ejecutar todas las semanas los lunes 17: 00, martes 17:00 y miércoles 17:00
  • SendAdminEmails para ejecutar todas las semanas los jueves de 09:00 a viernes de 9:00 a.

¿Qué código necesito para programar estos métodos usando Quartz en ASP.NET Core? También necesito saber cómo iniciar Quartz en ASP.NET Core, ya que todos los ejemplos de código en Internet aún se refieren a versiones anteriores de ASP.NET.

Puedo encontrar un ejemplo de código para la versión anterior de ASP.NET pero no sé cómo iniciar Quartz en ASP.NET Core para iniciar las pruebas. ¿Dónde pongo el JobScheduler.Start(); en ASP.NET Core?


Además de la respuesta @ felix-b. Añadiendo DI a los trabajos. También QuartzStartup Start se puede hacer asíncrono.

Basado en esta respuesta: https://.com/a/42158004/1235390

public class QuartzStartup { public QuartzStartup(IServiceProvider serviceProvider) { _serviceProvider = serviceProvider; } public async Task Start() { // other code is same _scheduler = await schedulerFactory.GetScheduler(); _scheduler.JobFactory = new JobFactory(_serviceProvider); await _scheduler.Start(); var sampleJob = JobBuilder.Create<SampleJob>().Build(); var sampleTrigger = TriggerBuilder.Create().StartNow().WithCronSchedule("0 0/1 * * * ?").Build(); await _scheduler.ScheduleJob(sampleJob, sampleTrigger); } }

Clase JobFactory

public class JobFactory : IJobFactory { private IServiceProvider _serviceProvider; public JobFactory(IServiceProvider serviceProvider) { _serviceProvider = serviceProvider; } public IJob NewJob(TriggerFiredBundle bundle, IScheduler scheduler) { return _serviceProvider.GetService(bundle.JobDetail.JobType) as IJob; } public void ReturnJob(IJob job) { (job as IDisposable)?.Dispose(); } }

Clase de inicio

public void ConfigureServices(IServiceCollection services) { // other code is removed for brevity // need to register all JOBS by their class name services.AddTransient<SampleJob>(); } public void Configure(IApplicationBuilder app, IHostingEnvironment env, IApplicationLifetime applicationLifetime) { var quartz = new QuartzStartup(_services.BuildServiceProvider()); applicationLifetime.ApplicationStarted.Register(() => quartz.Start()); applicationLifetime.ApplicationStopping.Register(quartz.Stop); // other code removed for brevity }

Clase SampleJob con inyección de dependencia contructora:

public class SampleJob : IJob { private readonly ILogger<SampleJob> _logger; public SampleJob(ILogger<SampleJob> logger) { _logger = logger; } public async Task Execute(IJobExecutionContext context) { _logger.LogDebug("Execute called"); } }


La respuesta aceptada cubre el tema muy bien, pero algunas cosas han cambiado con la última versión de Quartz. Lo siguiente se basa en que este artículo muestra un inicio rápido con Quartz 3.0.xy ASP.NET Core 2.2:

Fábrica de trabajo

public class QuartzJobFactory : IJobFactory { private readonly IServiceProvider _serviceProvider; public QuartzJobFactory(IServiceProvider serviceProvider) { _serviceProvider = serviceProvider; } public IJob NewJob(TriggerFiredBundle bundle, IScheduler scheduler) { var jobDetail = bundle.JobDetail; var job = (IJob)_serviceProvider.GetService(jobDetail.JobType); return job; } public void ReturnJob(IJob job) { } }

Una muestra de trabajo que también trata sobre la salida en el reciclaje / salida del grupo de aplicaciones

[DisallowConcurrentExecution] public class TestJob : IJob { private ILoggingService Logger { get; } private IApplicationLifetime ApplicationLifetime { get; } private static object lockHandle = new object(); private static bool shouldExit = false; public TestJob(ILoggingService loggingService, IApplicationLifetime applicationLifetime) { Logger = loggingService; ApplicationLifetime = applicationLifetime; } public Task Execute(IJobExecutionContext context) { return Task.Run(() => { ApplicationLifetime.ApplicationStopping.Register(() => { lock (lockHandle) { shouldExit = true; } }); try { for (int i = 0; i < 10; i ++) { lock (lockHandle) { if (shouldExit) { Logger.LogDebug($"TestJob detected that application is shutting down - exiting"); break; } } Logger.LogDebug($"TestJob ran step {i+1}"); Thread.Sleep(3000); } } catch (Exception exc) { Logger.LogError(exc, "An error occurred during execution of scheduled job"); } }); } }

Configuración de Startup.cs

private void ConfigureQuartz(IServiceCollection services, params Type[] jobs) { services.AddSingleton<IJobFactory, QuartzJobFactory>(); services.Add(jobs.Select(jobType => new ServiceDescriptor(jobType, jobType, ServiceLifetime.Singleton))); services.AddSingleton(provider => { var schedulerFactory = new StdSchedulerFactory(); var scheduler = schedulerFactory.GetScheduler().Result; scheduler.JobFactory = provider.GetService<IJobFactory>(); scheduler.Start(); return scheduler; }); } protected void ConfigureJobsIoc(IServiceCollection services) { ConfigureQuartz(services, typeof(TestJob), /* other jobs come here */); } public void ConfigureServices(IServiceCollection services) { ConfigureJobsIoc(services); // other stuff comes here AddDbContext(services); AddCors(services); services .AddMvc() .SetCompatibilityVersion(CompatibilityVersion.Version_2_2); } protected void StartJobs(IApplicationBuilder app, IApplicationLifetime lifetime) { var scheduler = app.ApplicationServices.GetService<IScheduler>(); //TODO: use some config QuartzServicesUtilities.StartJob<TestJob>(scheduler, TimeSpan.FromSeconds(60)); lifetime.ApplicationStarted.Register(() => scheduler.Start()); lifetime.ApplicationStopping.Register(() => scheduler.Shutdown()); } public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory, ILoggingService logger, IApplicationLifetime lifetime) { StartJobs(app, lifetime); // other stuff here }


No sé cómo hacerlo con Quartz, pero había experimentado el mismo escenario con otra biblioteca que funciona muy bien. Aqui como lo dí

  • Instalar FluentScheduler

    Install-Package FluentScheduler

  • Usalo asi

    var registry = new Registry(); JobManager.Initialize(registry); JobManager.AddJob(() => MyEmailService.SendAdminEmails(), s => s .ToRunEvery(1) .Weeks() .On(DayOfWeek.Monday) .At(17, 00)); JobManager.AddJob(() => MyEmailService.SendAdminEmails(), s => s .ToRunEvery(1) .Weeks() .On(DayOfWeek.Wednesday) .At(17, 00)); JobManager.AddJob(() => MyEmailService.SendUserEmails(), s => s .ToRunEvery(1) .Weeks() .On(DayOfWeek.Thursday) .At(09, 00)); JobManager.AddJob(() => MyEmailService.SendUserEmails(), s => s .ToRunEvery(1) .Weeks() .On(DayOfWeek.Friday) .At(09, 00));

La documentación se puede encontrar aquí FluentScheduler en GitHub