RW

How To Schedule Tasks With A Quartz.NET Windows Service

Published: 2013-10-19 | By: Ryan Williams

Goals

Any website with a substantial user base should engage in automated communication. In my case, I wanted to send birthday emails to people using a daily task. I wanted to find them, email them a customized message and then log a receipt of the message in a SQL Server database. I also wanted to have a monitoring task that would serve as a health check so I could know that the service application was still working in the case there were no birthday emails to send out. To do this I wanted to use Quartz.NET, you can read their tutorial here.
 

Debugging Challenges

The problem with making a Windows Service is that putting breakpoints on them at runtime doesn't work. I recommend watching this small video series on learning the basics of building and debugging a Windows Service which tells you how to invoke your class in debug mode to make development much easier.

After all of those pieces are in place, the Program.cs file should be modified to ease debugging like the video above suggests.

using System;
using System;
using System.Diagnostics;
using System.ServiceProcess;
using Quartz;
using Quartz.Impl;
using Quartz.Impl.Triggers;

namespace MySite.EmailBlasterService
{
    internal class Program
    {
        private static void Main(string[] args)
        {
        #if DEBUG
            var service1 = new ContactService();
            service1.OnDebug();
            System.Threading.Thread.Sleep(System.Threading.Timeout.Infinite);
        #else
            ServiceBase[] ServicesToRun;
            ServicesToRun = new ServiceBase[] 
            { 
                new ContactService() 
            };
            ServiceBase.Run(ServicesToRun);
        #endif
        }
    }
}

Business Logic

At a high level there are a bunch of things needed to get this working like I wanted: a Windows Service project, an App.config file with database and other settings, a Program.cs file to start the application, a Windows Service class that inherits from ServiceBase that does the business logic, my models, a way to send emails, and Nuget packages for Entity Framework, Quartz.NET, Amazon (to send email) and log4net.

The ContactService class is where the magic happens:

using System;
using System.Linq;
using System.Reflection;
using System.ServiceProcess;
using MySite.Lib.Services;
using MySite.Models;
using log4net;
using Quartz;
using Quartz.Impl;
using Quartz.Impl.Triggers;

namespace MySite.EmailBlasterService
{
    public partial class ContactService : ServiceBase
    {
        private const string Group1 = "BusinessTasks";
        private const string Job = "Job";
        private static readonly ILog Log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);

        public ContactService()
        {
            InitializeComponent();
        }

        private static IScheduler _scheduler;

        protected override void OnStart(string[] args)
        {
            ISchedulerFactory schedulerFactory = new StdSchedulerFactory();
            _scheduler = schedulerFactory.GetScheduler();
            _scheduler.Start();

            Log.Info("Starting Windows Service: " + MySiteLib.Configs.GeneralConfigs.SiteName);

            AddJobs();
        }

        private void AddJobs()
        {
            AddHealthMonitoringJob();
            AddBirthdayJob();
        }

        public static void AddHealthMonitoringJob()
        {
            const string trigger1 = "HealthMonitoring";

            IDoJob myJob = new HealthMonitiorJob();
            var jobDetail = new JobDetailImpl(trigger1 + Job, Group1, myJob.GetType());
            var trigger = new CronTriggerImpl(
                trigger1, 
                Group1, 
                "0 0/10 * * * ?" /* every 10 minutes */
                ) 
                {TimeZone = TimeZoneInfo.Utc};
            _scheduler.ScheduleJob(jobDetail, trigger);
            var nextFireTime = trigger.GetNextFireTimeUtc();
            if (nextFireTime != null)
                Log.Info(Group1 + "+" + trigger1, new Exception(nextFireTime.Value.ToString("u")));
        }

        public class HealthMonitiorJob : IDoJob
        {
            public void Execute(IJobExecutionContext context)
            {
                Log.Info(DateTime.UtcNow);
            }
        }

        public static void AddBirthdayJob( )
        {
            const string trigger1 = "EmailTasksTrigger";
            const string jobName = trigger1 + Job;
            IDoJob myJob = new BirthdayJob();  
            var jobDetail = new JobDetailImpl(jobName, Group1, myJob.GetType());
            var trigger = new CronTriggerImpl(
                trigger1,
                Group1, 
                "0 0 2 * * ?"  /* run every day at 2:00 UTC */ ) 
                {TimeZone = TimeZoneInfo.Utc}; 
            _scheduler.ScheduleJob(jobDetail, trigger);
            var nextFireTime = trigger.GetNextFireTimeUtc();
            if (nextFireTime != null)
                Log.Debug(Group1 + "+" + trigger1, new Exception(nextFireTime.Value.ToString("u")));
        }

        public void OnDebug()
        {
            OnStart(null);
        }

        protected override void OnStop()
        {
            Log.Info("Stopping Windows Service: " + MySiteLib.Configs.GeneralConfigs.SiteName);
        }

        internal class BirthdayJob : IDoJob
        {
            private readonly IMailService _mail;

            public BirthdayJob()
            {
                _mail = new MailService();
            }

            public void ProcessBirthDayUsers()
            {
                using (var context = new MySiteUserDBContext())
                {
                    context.Configuration.ProxyCreationEnabled = false;
                    context.Configuration.LazyLoadingEnabled = false;

                    try
                    {
                        var results = from allUsers in context.MySiteUserAccountDetailEntity
                            where
                                allUsers.birthDate.Month == DateTime.UtcNow.Month && allUsers.birthDate.Day == DateTime.UtcNow.Day  &&
                                allUsers.emailMessages == true
                            select allUsers;

                        var users = results.ToList();

                        foreach (var user in users.Select(birthdayUser => context != null
                            ? context.UserAccountEntity.FirstOrDefault(
                                usr => usr.userAccountID == birthdayUser.userAccountID)
                            : null).Where(user => user != null))
                        {
                            if (user.createDate == null) continue;

                            var signUpDate = string.Format("{0} {1}, {2}", 
                                user.createDate.Value.ToString("MMM"), 
                                user.createDate.Value.Day, 
                                user.createDate.Value.Year);

                            _mail.SendMail(Lib.Configs.AmazonCloudConfigs.SendFromEmail, user.eMail,
                                string.Format("Happy Birthday {0}!", user.userName),
                                string.Format("Happy birthday! {1}{1} Visit: {0} {1}{1} Membership Sign Up Date: {1}{1}{2}",
                                    Lib.Configs.GeneralConfigs.SiteDomain, Environment.NewLine, signUpDate));

                            Log.Info("Sent to: " + user.eMail);
                        }
                    }
                    catch (Exception ex)
                    {
                        Log.Error(ex);
                    }
                }
            }

            public void Execute(IJobExecutionContext context)
            {
                ProcessBirthDayUsers();
            }
        }

        internal interface IDoJob : IJob
        {

        }
    }
}

 

In this class there is a logger for logging details to a database, the OnStart and OnStop methods that occur when the Windows Service begins and ends as well as classes and setup code for the tasks. There are two jobs, one for monitoring the health of the service which logs every 10 minutes and a class that is for the birthday email job, sending emails at 2:00 UTC each day.

The jobs are added by using their type and a time expression (cron syntax). The CronTriggerImpl defaults to local time, quite useless in a real world situation where a server is in a different time zone. Fortunately, you can print out the next time a job will fire by calling the GetNextFireTimeUtc method. I've changed the time zone for the trigger to UTC so the cron expressions will work anywhere and run at the right time regardless of the machine's timezone. Each of the jobs implements an interface that uses the Quartz.NET IJob interface. That interface requires an Execute method to be implemented. In that method is where your business logic resides.

In my case, my business logic for the health monitor is simply logging to the database. In the case of the birthday job, it's finding all the users with a birthday of the same day and month, formatting an email, sending it and logging a receipt of it. This is all based on some other classes I am not displaying that relate to my business objects for users and their details. Due to internationalist concerns, the birthday email is sent at 2:00 UTC, which is around 7:00 PM Pacific Time the day before. It's hard to know what the best time to send the birthday message to all users but I think too soon is better than too late.

The App.config contains the connection string, the log4net configuration for logging to the database, similar to how I've previously illustrated. It also contains configuration information such as the email address I'm using and the site name. One very frustrating thing with building this was getting a working connection to the database with a local account. It worked fine while debugging but would encounter a problem when I released it on the server. As a result, I decided to use a remote connection string, the kind you use for SQL Server authentication.
 

Deployment

note: do these commands with cmd.exe, not PowerShell

It would be nice to have an automated deployment script for the Windows Service. At the time of writing, I haven't found one so I installed it the manual way. Build the project in release mode, then go to the service's bin directory, copy the contents there to wherever you want to host the service, after that you can install it with sc like this (yes, the space after the = sign is needed):

PS> sc create YOUR_TASK_NAME binPath= "YOUR_FILEPATH\YOUR_SERVICE.exe" DisplayName= "YOUR_TASK_NAME" start= delayed-auto

It's set to autostart so that when the server/ computer reboots, it will begin. It's better to autostart with a delay so that SQL Server is ready. You can do either but I wanted to mitigate any startup issues. 

Since you probably want the service to run before a reboot, you can launch the services dialog or run the command: services.msc to open it. You could do this in PowerShell too, if you want. Essentially you just find your service in the list, right click it and start it. If it starts correctly, log4net will log in the OnStart method (assuming you set that up).

If you need to make changes, which you probably will, you will need to stop the service (with PowerShell or through the UI), close the dialog for services then delete the service like this:

PS> sc delete YOUR_TASK_NAME

If it deleted successfully you can install a new compiled version of your service again as directed above.

Another way to do this, if you have to let some less technical person install it, is by following the steps in this video which illustrate how you can make a step by step UI installation.
 

Concerns and Future Opportunities

In this case, I have no unit tests or integration tests. It would be nice to be able to mock the process and fake the time to check when it will run next. Given that I have two working tasks here, I can easily add more jobs that implement my interface. I might want to email inactive users over some time threshold, I might want to clear some log files out or I might want to have a task that reads from a queue to offload some work the web application is doing. Once it's working, you can use your imagination to automate other tasks to improve your user experience or whatever it is you want to do. It's a huge time saver once this is working.

Cheers to an automated life!

 

The real Quartz.NET scheduler source code on GitHub for this project (warning, URL may change)

 



No Comments... Yet


Comment On

Prove you are human 5 + 7 =