Running TopShelf with Quartz in .NET

Nlog

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

Post a Comment

Previous Post Next Post