Implementing Background Job Scheduling with Quartz.NET and Topshelf
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.