Our final goal is to create a windows service that runs jobs effectively.
The TopShelf library is used to create a windows service, install it and react to various events such as pause, stop, recover e.t.c, previously I've shown some basic service setup with TopShelf: https://alexrait.blogspot.com/2022/12/using-topshelf-library-in-c-to-create.html
The Quartz .NET library is used to create jobs, schedule them accourding to various rules, triggers and configurations
Let's see how we can join these two to create a window service that once in a while runs some set of jobs
Install these packages:
- Microsoft.Extensions.Configuration.Json
- Microsoft.Extensions.DependencyInjection
- Microsoft.Extensions.Logging
- Microsoft.Extensions.Logging.Configuration
- NLog
- NLog.Extensions.Logging
- Quartz
- Quartz.Extensions.DependencyInjection
- Quartz.Jobs
- Topshelf
- Topshelf.NLog
- TopShelf.ServiceInstaller
For TopShelf create the host
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using System;
using Topshelf;
Host service = HostFactory.New(x =>
{
ServiceProvider serviceProvider = Container.RegisterServices();
var loggerFactory = serviceProvider.GetRequiredService<ILoggerFactory>();
Quartz.Logging.LogContext.SetCurrentLogProvider(loggerFactory);
x.Service<MyWinService>(s =>
{
s.ConstructUsing(name => serviceProvider.GetRequiredService<MyWinService>());
s.WhenStarted((tc, hostControl) => tc.Start(hostControl));
s.WhenStopped((tc, hostControl) => tc.Stop(hostControl));
})
.RunAsLocalSystem()
.StartAutomaticallyDelayed();
x.SetDescription("Sample Windows Service");
x.SetDisplayName("MyWinService");
x.SetServiceName("MyWinService");
x.EnableShutdown();
x.UseNLog();
});
service.Run();
Now, define the service service itself
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Quartz;
using Quartz.Impl;
using Quartz.Spi;
using System.Data;
using Topshelf;
internal class MyService : ServiceControl
{
private readonly IServiceProvider _serviceProvider;
private readonly ILogger<MyService> _logger;
private readonly ISchedulerFactory _schedulerFactory;
private readonly IConfiguration _configuration;
public MyService(
IServiceProvider serviceProvider,
ILogger<MyService> logger,
ISchedulerFactory schedulerFactory,
IConfiguration configuration
) :base()
{
_serviceProvider = serviceProvider;
_logger = logger;
_schedulerFactory = schedulerFactory;
_configuration = configuration;
}
public bool Start(HostControl hostControl)
{
Task.Run(async () =>
{
try
{
string chronExpression = _configuration["Schedule"];
IScheduler scheduler = await _schedulerFactory.GetScheduler();
IJobFactory jobFactory = _serviceProvider.GetRequiredService<IJobFactory>();
scheduler.JobFactory = jobFactory;
ITrigger myJobTrigger = TriggerBuilder.Create()
.WithIdentity("my-triggers", "my-triggers")
.WithCronSchedule(chronExpression)
.Build();
for (int i = 0; i < 10; i++)
{
JobDataMap map = new ();
map.Put("index", i);
IJobDetail myJobDetail = JobBuilder.Create<MyJob>()
.WithDescription($"my-job-{i}")
.WithIdentity($"my-job-{i}")
.SetJobData(map)
.Build();
await scheduler.ScheduleJob(myJobDetail, myJobTrigger);
}
await scheduler.Start();
}
catch (Exception e)
{
_logger.LogError(e, "");
}
});
_logger.LogInformation("Service started");
return true;
}
public bool Stop(HostControl hostControl)
{
return true;
}
}
The an example of a configuration file for this setup is (appsettings.json):
"Schedule": "0 0/10 * ? * *"
This is a standard chronjob syntax which indicates in this case to run the job every 10 minutes starting from the 0 second of the 0 minute of each hour
Define the job
using Microsoft.Extensions.Logging;
using Quartz;
internal class MyJob : IJob
{
private readonly ILogger<MyJob> _logger;
public MyJob(ILogger<MyJob> logger)
{
_logger = logger;
}
public async Task Execute(IJobExecutionContext context)
{
int index = (int)context.MergedJobDataMap.Get("index");
_logger.LogDebug("Running job with index: {i}", index);
await Task.Delay(1000);
}
}
To make it all work with DI, we need to create a custom JobFactory
using Microsoft.Extensions.DependencyInjection;
using Quartz;
using Quartz.Spi;
public class MyJobFactory : IJobFactory
{
private readonly IServiceProvider _provider;
public MyJobFactory(IServiceProvider provider)
{
_provider = provider;
}
public IJob NewJob(TriggerFiredBundle bundle, IScheduler scheduler)
{
var job = _provider.GetRequiredService(bundle.JobDetail.JobType) as IJob;
if (job == null)
{
throw new NotSupportedException($"{bundle.JobDetail.JobType.Name} is not supported!");
}
return job;
}
public void ReturnJob(IJob job)
{
if (job is IDisposable disposable)
{
disposable.Dispose();
}
}
}
Now, create a container and register all
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using NLog.Extensions.Logging;
using Quartz;
using Quartz.Impl;
using Quartz.Simpl;
using Quartz.Spi;
internal class Container
{
public static ServiceProvider RegisterServices()
{
IServiceCollection services = new ServiceCollection();
IConfiguration configuration = new ConfigurationBuilder()
.AddJsonFile("appsettings.json")
.Build();
services.AddScoped(_ => configuration);
services.AddSingleton<ISchedulerFactory, StdSchedulerFactory>();
services.AddSingleton<MyService>();
services.AddLogging((l) =>
{
l.ClearProviders();
l.SetMinimumLevel(LogLevel.Debug);
l.AddNLog();
});
services.AddSingleton<IJobFactory>(provider =>
{
var jobFactory = new MyJobFactory(provider);
return jobFactory;
});
services.AddTransient<MyJob>();
var provider = services.BuildServiceProvider();
return provider;
}
}