J'ai la classe suivante
public class MyEmailService
{
public async Task<bool> SendAdminEmails()
{
...
}
public async Task<bool> SendUserEmails()
{
...
}
}
public interface IMyEmailService
{
Task<bool> SendAdminEmails();
Task<bool> SendUserEmails();
}
J'ai installé le dernier paquet Quartz 2.4.1 Nuget car je voulais un planificateur léger dans mon application Web sans base de données SQL Server séparée.
J'ai besoin de planifier les méthodes
SendUserEmails
à courir chaque semaine les lundis 17h00, les mardis 17h00 et les mercredis 17h00SendAdminEmails
à courir toutes les semaines les jeudis à 9h00 et les vendredis à 9h00De quel code ai-je besoin pour planifier ces méthodes à l'aide de Quartz dans ASP.NET Core? J'ai également besoin de savoir comment démarrer Quartz dans ASP.NET Core, car tous les exemples de code sur Internet font toujours référence aux versions précédentes d'ASP.NET.
Je peux trouve un exemple de code pour la version précédente d’ASP.NET mais je ne sais pas comment démarrer Quartz dans ASP.NET Core pour commencer à tester . Où dois-je placer le JobScheduler.Start();
dans ASP.NET Coeur?
Outils supposés: Visual Studio 2017 RTM, .NET Core 1.1, Kit de développement .NET Core SDK 1.0, SQL Server Express 2016 LocalDB.
Dans l'application 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>
Dans la classe Program
(telle que scaffoldée par Visual Studio par défaut):
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 exemple de classe d'emploi:
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();
}
}
Tout d’abord, vous devez utiliser la v3 de Quartz, car elle cible .NET Core, conformément à cette annonce .
Actuellement, seules les versions alpha des packages v3 sont disponibles sur NuGet . Il semble que l'équipe ait déployé beaucoup d'efforts pour la publication de la version 2.5.0, qui ne cible pas .NET Core. Néanmoins, dans leur rapport GitHub, la branche master
est déjà dédiée à la v3 et, en gros, les problèmes en suspens pour la version v3 ne semblent pas être critiques, surtout les anciens éléments de liste de souhaits, IMHO. Étant donné que l'activité récente de commit est relativement faible, la version 3 devrait être publiée dans quelques mois, voire dans un an et demi - mais personne ne le sait.
Si l'application Web doit être hébergée sous IIS, vous devez prendre en compte le comportement de recyclage/déchargement des processus de travail. L'application Web ASP.NET Core s'exécute en tant que processus .NET Core standard, distinct de w3wp.exe - IIS ne sert que de proxy inverse. Néanmoins, lorsqu'une instance de w3wp.exe est recyclée ou déchargée, le processus d'application .NET Core associé est également signalé à quitter (selon this ).
Une application Web peut également s'auto-héberger derrière un proxy inverse non-IIS (par exemple, NGINX), mais je supposerai que vous utilisez IIS et affine ma réponse en conséquence.
Les problèmes introduits par le recyclage/déchargement sont bien expliqués dans le post référencé par @ darin-dimitrov :
Je peux penser à une justification du fait que ces travaux de messagerie sont hébergés dans une application Web, malgré les problèmes énumérés ci-dessus. C'est la décision de n'avoir qu'un seul type de modèle d'application (ASP.NET). Une telle approche simplifie la courbe d’apprentissage, la procédure de déploiement, le suivi de la production, etc.
Si vous ne souhaitez pas introduire de microservices dorsaux (ce qui serait un bon endroit pour déplacer les travaux de messagerie), il est alors logique de supprimer les comportements de recyclage/déchargement IIS et d'exécuter Quartz dans une application Web.
Ou peut-être vous avez d'autres raisons.
Dans votre scénario, le statut d'exécution du travail doit rester en dehors du processus. Par conséquent, RAMJobStore par défaut ne convient pas et vous devez utiliser le magasin de travaux ADO.NET.
Puisque vous avez mentionné SQL Server dans la question, je vais vous donner un exemple d'installation pour la base de données SQL Server.
Je suppose que vous utilisez Visual Studio 2017 et la version la plus récente/récente des outils .NET Core. Le mien est .NET Core Runtime 1.1 et .NET Core SDK 1.0.
Pour l'exemple de configuration de base de données, j'utiliserai une base de données nommée Quartz
dans SQL Server 2016 Express LocalDB. Les scripts de configuration de base de données peuvent être trouvés ici .
Tout d’abord, ajoutez les références de package requises à l’application Web .csproj (ou utilisez l’interface graphique du gestionnaire de packages de NuGet dans 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>
Avec l'aide de Guide de migration et du didacticiel V3 , nous pouvons déterminer comment démarrer et arrêter le planificateur. Je préfère encapsuler ceci dans une classe séparée, appelons-le 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...
}
}
}
}
Remarque 1. Dans l'exemple ci-dessus, SendUserEmailsJob
et SendAdminEmailsJob
sont des classes qui implémentent IJob
. L'interface IJob
est légèrement différente de IMyEmailService
, car elle renvoie void Task
et non Task<bool>
. La dépendance des deux classes de travail devrait être IMyEmailService
(probablement l'injection du constructeur).Remarque 2. Pour qu'un travail de longue durée puisse se terminer rapidement, la méthode IJob.Execute
doit observer l'état IJobExecutionContext.CancellationToken
. Cela peut nécessiter une modification de l'interface IMyEmailService
, pour que ses méthodes reçoivent le paramètre CancellationToken
:.
public interface IMyEmailService
{
Task<bool> SendAdminEmails(CancellationToken cancellation);
Task<bool> SendUserEmails(CancellationToken cancellation);
}
Program
, un peu comme dans l'application console. La méthode Main
est appelée pour créer un hôte Web, l'exécuter et attendre jusqu'à ce qu'il se ferme:public class Program
{
public static void Main(string[] args)
{
var Host = new WebHostBuilder()
.UseKestrel()
.UseContentRoot(Directory.GetCurrentDirectory())
.UseIISIntegration()
.UseStartup<Startup>()
.UseApplicationInsights()
.Build();
Host.Run();
}
}
Cette ligne:.
.UseStartup<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...
}
}
Dans l’ancien .NET Framework, ASP.NET fournissait une interface IRegisteredObject
. Selon this post et la documentation , dans ASP.NET Core, il a été remplacé par IApplicationLifetime
. Bingo Une instance de IApplicationLifetime
peut être injectée dans la méthode Startup.Configure
via un paramètre.
Par souci de cohérence, je vais associer les QuartzStartup.Start
et 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 ....
}
Arrêt en douceur sur IIS Express et le module ASP.NET Core.
IApplicationLifetime.ApplicationStopping
hook uniquement sur IIS, avec le dernier module ASP.NET Core installé. IIS Express (installé avec Visual Studio 2017 Community RTM) et IIS avec une version obsolète du module ASP.NET Core n'a pas invoqué de manière cohérente IApplicationLifetime.ApplicationStopping
. Je crois que c'est à cause de ce bogue qui a été corrigé.Vous pouvez installer la dernière version du module ASP.NET Core d'ici . Suivez les instructions de la section "Installation du dernier module ASP.NET Core".
Quartz vs FluentScheduler.
I also took a look at FluentScheduler, as it was proposed as an alternative library by @Brice Molesti. To my first impression, FluentScheduler is quite a simplistic and immature solution, compared to Quartz. For example, FluentScheduler doesn't provide such fundamental features as job status persistence and clustered execution.
En plus de @ felix-b answer. Ajout de DI aux travaux. De plus, QuartzStartup Start peut être rendu asynchrone.
Sur la base de cette réponse: https://stackoverflow.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);
}
}
Classe 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();
}
}
Classe de démarrage:
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
}
Classe SampleJob avec injection de dépendance de constructeur:
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");
}
}
Je ne sais pas comment faire avec Quartz, mais j’avais expérimenté le même scénario avec une autre bibliothèque qui fonctionne très bien. Voici comment je le dis
Installer FluentScheduler
Install-Package FluentScheduler
Utilisez-le comme ça
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 documentation peut être trouvée ici FluentScheduler sur GitHub
La réponse acceptée couvre très bien le sujet, mais certaines choses ont changé avec la dernière version de Quartz. Ce qui suit est basé sur cet article illustre un démarrage rapide avec Quartz 3.0.x et ASP.NET Core 2.2:
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) { }
}
[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");
}
});
}
}
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
}