Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

Implementing Background Job Scheduling with Quartz.NET and Topshelf

Tech May 16 2

Quartz.NET is a powerful open-source job scheduling library for .NET applications. When combined with Topshelf, it enables the creation of robust Windows services capable of executing scheduled background tasks.

Project Setup

Create a new console application named QuartzJobDemo. The following components are required:

  • Quartz.NET (version 2.x or 3.x)
  • Topshelf
  • Topshelf.Log4Net (matching Topshelf version)
  • log4net for logging

Core Implementation

Scheduler Host: ServiceRunner.cs

using log4net;
using Quartz;
using Quartz.Impl;
using Topshelf;

namespace QuartzJobDemo.Services
{
    public sealed class ServiceRunner : ServiceControl, ServiceSuspend
    {
        private readonly IScheduler _scheduler;
        private readonly ILog _logger = LogManager.GetLogger(typeof(ServiceRunner));

        // For Quartz 2.x
        public ServiceRunner()
        {
            _scheduler = StdSchedulerFactory.GetDefaultScheduler();
        }

        // For Quartz 3.x (uncomment if upgrading)
        // public ServiceRunner()
        // {
        //     _scheduler = StdSchedulerFactory.GetDefaultScheduler().GetAwaiter().GetResult();
        // }

        public bool Start(HostControl hostControl)
        {
            _logger.InfoFormat("Service started at {0}", DateTime.Now);
            _scheduler.Start();
            return true;
        }

        public bool Stop(HostControl hostControl)
        {
            _logger.InfoFormat("Service stopped at {0}", DateTime.Now);
            _scheduler.Shutdown(waitForJobsToComplete: false);
            return true;
        }

        public bool Continue(HostControl hostControl)
        {
            _logger.InfoFormat("Service resumed at {0}", DateTime.Now);
            _scheduler.ResumeAll();
            return true;
        }

        public bool Pause(HostControl hostControl)
        {
            _logger.InfoFormat("Service paused at {0}", DateTime.Now);
            _scheduler.PauseAll();
            return true;
        }
    }
}

Job Implementation: TestJob.cs

using log4net;
using Quartz;
using System.Threading.Tasks;

namespace QuartzJobDemo
{
    public sealed class TestJob : IJob
    {
        private readonly ILog _logger = LogManager.GetLogger(typeof(TestJob));

        // Quartz 2.x: void Execute
        public void Execute(IJobExecutionContext context)
        {
            _logger.Info("Executing TestJob");
        }

        // Quartz 3.x: Task Execute (uncomment when upgrading)
        // public Task Execute(IJobExecutionContext context)
        // {
        //     _logger.Info("Executing TestJob");
        //     return Task.CompletedTask;
        // }
    }
}

Main Entry Point: Program.cs

using System.IO;
using log4net;
using QuartzJobDemo.Services;
using Topshelf;

namespace QuartzJobDemo
{
    class Program
    {
        static void Main(string[] args)
        {
            var logConfig = new FileInfo(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "log4net.config"));
            log4net.Config.XmlConfigurator.ConfigureAndWatch(logConfig);

            HostFactory.Run(config =>
            {
                config.UseLog4Net();
                config.Service<ServiceRunner>();

                config.SetDescription("Quartz.NET Demo Service");
                config.SetDisplayName("QuartzJobDemo");
                config.SetServiceName("QuartzJobDemo");

                config.EnablePauseAndContinue();
            });
        }
    }
}

Configuration Files

Ensure the following configuration files are set to Copy Always in project properties.

log4net.config

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <configSections>
    <section name="log4net" type="log4net.Config.Log4NetConfigurationSectionHandler, log4net"/>
  </configSections>

  <log4net>
    <appender name="RollingLogFileAppender" type="log4net.Appender.RollingFileAppender">
      <file value="D:\QuartzJobDemo_Log\servicelog\" />
      <appendToFile value="true" />
      <maxSizeRollBackups value="10" />
      <staticLogFileName value="false" />
      <datePattern value="yyyy-MM-dd".read.log"" />
      <rollingStyle value="Date" />
      <layout type="log4net.Layout.PatternLayout">
        <conversionPattern value="%d [%t] %-5p %c - %m%n" />
      </layout>
    </appender>

    <appender name="ColoredConsoleAppender" type="log4net.Appender.ColoredConsoleAppender">
      <mapping>
        <level value="ERROR" />
        <foreColor value="Red, HighIntensity" />
      </mapping>
      <mapping>
        <level value="INFO" />
        <foreColor value="Green" />
      </mapping>
      <layout type="log4net.Layout.PatternLayout">
        <conversionPattern value="%n%date{HH:mm:ss,fff} [%-5level] %m" />
      </layout>
      <filter type="log4net.Filter.LevelRangeFilter">
        <levelMin value="INFO" />
        <levelMax value="FATAL" />
      </filter>
    </appender>

    <root>
      <level value="ALL" />
      <appender-ref ref="ColoredConsoleAppender" />
      <appender-ref ref="RollingLogFileAppender" />
    </root>
  </log4net>
</configuration>

quartz.config

quartz.scheduler.instanceName = QuartzJobDemo

quartz.threadPool.type = Quartz.Simpl.SimpleThreadPool, Quartz
quartz.threadPool.threadCount = 10
quartz.threadPool.threadPriority = Normal

quartz.plugin.xml.type = Quartz.Plugin.Xml.XMLSchedulingDataProcessorPlugin, Quartz
quartz.plugin.xml.fileNames = ~/quartz_jobs.xml

quartz_jobs.xml

<?xml version="1.0" encoding="utf-8"?>
<job-scheduling-data 
    xmlns="http://quartznet.sourceforge.net/JobSchedulingData"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    version="2.0">

  <processing-directives>
    <overwrite-existing-data>true</overwrite-existing-data>
  </processing-directives>

  <schedule>
    <job>
      <name>TestJob</name>
      <group>TestGroup</group>
      <description>Sample recurring job</description>
      <job-type>QuartzJobDemo.TestJob, QuartzJobDemo</job-type>
      <durable>true</durable>
      <recover>false</recover>
    </job>
    <trigger>
      <cron>
        <name>TestTrigger</name>
        <group>TestGroup</group>
        <job-name>TestJob</job-name>
        <job-group>TestGroup</job-group>
        <start-time>2020-01-01T00:00:00+08:00</start-time>
        <cron-expression>0/5 * * * * ?</cron-expression> <!-- Every 5 seconds -->
      </cron>
    </trigger>
  </schedule>
</job-scheduling-data>

The cron expression 0/5 * * * * ? triggers the job every 5 seconds. Adjust as needed for production use.

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.