Asynchronous programming
.NET .NET Core Asynchronous C# Hosted Services Quartz.NET Services Threading Visual Studio

How to Schedule Hosted Services using .NET Core

Welcome to today’s post.

In today’s post I will be showing you how to schedule a hosted worker service using .NET Core.

In a previous post I showed how to create a .NET Core application that can run as Windows Service. In that post I showed how to start and stop the service. I did not demonstrate how we could poll or schedule the service to execute a worker process.

In another previous post I showed how to create a Windows Service that can execute a task with a .NET timer component.

I will first show how to incorporate a polled timer into your hosted worker service as this is the next most basic trigger we can implement. I will then show how to create jobs which will have their own schedules and trigger rules.

Using a Polled Timer to Schedule Events

With a polled timer we set a fixed interval we wish to execute the timer event. Within the timer event we can then perform some work, which runs in the background of the client process. In a previous post I showed how to incorporate a background task into a timed hosted service, which would execute periodic tasks based on a timer event.

We first define our polling interval as shown in our appsettings.json:

{
  "ScheduleSettings": {
    "pollingInterval": 20
  }
}

We store the configuration settings within a class as shown:

using System;
using System.Collections.Generic;
using System.Text;

namespace BookLoanScheduledHostedServiceApp
{
    public class AppConfiguration
    {
        public int pollingInterval { get; set; }
    }
}

In the example we will use a console application to run the host worker service. Before we can setup our start up class, we will need to use the web host builder to use the start up as shown in Program.cs:

using System;
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Builder;

namespace BookLoanScheduledHostedServiceApp
{
    static class Program
    {
        public static void Main(string[] args)
        {
            CreateWebHostBuilder(args).Build().Run();
        }

        public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
            WebHost.CreateDefaultBuilder(args)
                .UseStartup<Startup>();
    }
}	

Within our Startup class we can configure the timer service class as shown:

using System;
using System.IO;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using BackgroundTasksSample.Services;


namespace BookLoanScheduledHostedServiceApp
{
    public class Startup
    {
        private IConfiguration _configuration;
        private readonly IServiceProvider _provider;

        public IServiceProvider serviceProvider
        {
            get { return this._provider; }
        }
        public IConfiguration configuration
        {
            get { return this._configuration; }
        }

        public Startup()
        {	
            var services = new ServiceCollection();
            this.ConfigureServices(services);
            services.BuildServiceProvider();
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
        }

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddHostedService<TimedHostedService>();
        }
    }
}

What we did here was to inject the class into the service collection as a hosted service using:

services.AddHostedService<TimedHostedService>();

The timer class is as shown:

using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;


namespace BackgroundTasksSample.Services
{
    internal class TimedHostedService : IHostedService, IDisposable
    {
        private readonly ILogger _logger;
        private Timer _timer;

        public TimedHostedService(ILogger<TimedHostedService> logger)
        {
            _logger = logger;
        }

        public Task StartAsync(CancellationToken cancellationToken)
        {
            _logger.LogInformation("Timed Background Service is starting.");

            _timer = new Timer(DoWork, null, TimeSpan.Zero, 
                TimeSpan.FromSeconds(60));

            return Task.CompletedTask;
        }

        private void DoWork(object state)
        {
            _logger.LogInformation("Timed Background Service is working.");
        }

        public Task StopAsync(CancellationToken cancellationToken)
        {
            _logger.LogInformation("Timed Background Service is stopping.");

            _timer?.Change(Timeout.Infinite, 0);

            return Task.CompletedTask;
        }

        public void Dispose()
        {
            _timer?.Dispose();
        }
    }
}

The above background worker service has the start, stop and a DoWork() method that performs the actual task(s) and is run periodically scheduled every 60 seconds into the future under a timer component:

_timer = new Timer(DoWork, null, 
TimeSpan.Zero, TimeSpan.FromSeconds(60));

Now that I have explained the basic triggered timer schedule, when run the following output is shown:

In the next section I will show how to implement scheduled tasks within the hosted service.

Implementing Scheduled Tasks in the Hosted Service

I will show how to implement scheduled tasks within our hosted service.

To do this we either use a manual method of checking dates and times during polling, which is not a clean way of doing this or use a third-party package, Quartz.NET, to store schedules and trigger them to run specific jobs within our background process.

We first install the following NuGet packages:

If you are using .NET Core 2.2 or older then install the Microsoft.AspNetCore package, else you can install the generic hosting services library, Microsoft.Extensions.Hosting for .NET Core 3.0 and above.

With generic hosting within the Program class, you can use the following:

Host.CreateDefaultBuilder()

To build the generic host pipeline instead of the HTTP web hosting pipeline:

WebHost.CreateDefaultBuilder()

Below are some jobs we have defined for the following scheduled tasks:

  1. A basic polled job that runs every N seconds.
  2. A scheduled lunchtime job that runs at 1pm every day. 
  3. A scheduled pay time job that runs at the 15th day of every month at a specific time.

We setup the classes for the three scheduled tasks as follows:

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

namespace BookLoanScheduledHostedServiceApp
{
    public class SampleJob : IJob
    {
        public async Task Execute(IJobExecutionContext context)
        {
            await Console.Out.WriteLineAsync("This is a triggered job.");
        }
    }

    public class LunchJob : IJob
    {
        public async Task Execute(IJobExecutionContext context)
        {
            await Console.Out.WriteLineAsync("Off for lunch now!");
        }
    }

    public class TransferPayJob : IJob
    {
        public async Task Execute(IJobExecutionContext context)
        {
            await Console.Out.WriteLineAsync("Transfer monthly pay cheques.");
        }
    }
}

In our Startup class source include the following imports:

using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Configuration.Json;

First, we can obtain the polling interval for our polled scheduled job a shown:

public void ConfigureServices(IServiceCollection services)
{
    var configBuilder = new ConfigurationBuilder()
       	.SetBasePath(Directory.GetCurrentDirectory())
        .AddJsonFile("appsettings.json", optional: true, reloadOnChange: false);
    IConfiguration config = configBuilder.Build();

    this._configuration = config;

    var pollingInterval = 0;

    try
    {
         pollingInterval = this._configuration
  		     .GetSection("ScheduleSettings")
  	         .GetValue<int>("pollingInterval");
    }
    catch (Exception ex)
    {
        pollingInterval = 30;
    }

    Console.Out.WriteLineAsync($"Polling interval set to {pollingInterval}");
	…

Defining Schedules and Triggers for the Jobs

To configure the jobs, schedules and triggers we apply the changes as follows:

Add the uses imports as shown:

using Quartz;
using Quartz.Impl;

We add the Quartz services as shown:

services.AddQuartz(q =>
{
	…
}
services.AddQuartzServer(options =>
{
    options.WaitForJobsToComplete = true;
}

Within the services.AddQuartz() block we setup a scheduler factory and start a schedule as shown:

StdSchedulerFactory factory = new StdSchedulerFactory();

IScheduler scheduler = factory.GetScheduler().GetAwaiter().GetResult();
scheduler.Start();	

For the first job we define as shown:

IJobDetail job = JobBuilder.Create<SampleJob>()
    .WithIdentity("mySampleJob", "group1")
    .Build();

Define the trigger for the above job with a name mySampleTrigger1:

ITrigger trigger = TriggerBuilder.Create()
    .WithIdentity("mySampleTrigger1", "group1")
    .StartNow()
    .WithSimpleSchedule(x => x
   	    .WithIntervalInSeconds(pollingInterval)
        .RepeatForever()
    ).Build();

We schedule the job as follows:

scheduler.ScheduleJob(job, trigger);

Another type of trigger is a CRON job. A CRON job is scheduled based on periodic patterns based on dates, times, days of a week, days in the month, months, years and so on. To define a schedule for the lunch time job, we do this as shown with the job definition:

IJobDetail lunchJob = JobBuilder.Create<LunchJob>()
    .WithIdentity("mySampleLunchJob", "group1")
    .Build();

the CRON trigger, and scheduling:

ITrigger cronTriggerLunch = TriggerBuilder.Create()
    .WithIdentity("mySampleTrigger2", "group1")
    .WithSchedule(CronScheduleBuilder.DailyAtHourAndMinute(13, 00))
    .ForJob(lunchJob)
    .Build();
scheduler.ScheduleJob(lunchJob, cronTriggerLunch);

With a specific time of the day, we use the CronScheduleBuilder class to define our schedule. In this case it is set to be at 1pm daily:

.WithSchedule(CronScheduleBuilder.DailyAtHourAndMinute(13, 00))

Another type of CRON schedule is defined using CRON patterns, where the following pattern:

"0 0 1 15 * ?"

Schedules at 1am on 15th day of the month.

The above parameters correspond to the following date, time, day, month, and year constituents:

1.Seconds

2.Minutes

3.Hours

4.Day-of-Month

5.Month

6.Day-of-Week

7.Year (optional field)

The job definition and schedule below correspond to a trigger than runs at 8am on the 15th of every month:

IJobDetail payTransferJob = JobBuilder.Create<TransferPayJob>()
    .WithIdentity("mySamplePayTransferJob", "group1")
    .Build();

ITrigger cronTriggerTheyGetPaid = TriggerBuilder.Create()
    .WithIdentity("mySampleTrigger3", "group1")
    .WithCronSchedule("0 0 8 15 * ? ")
    .ForJob(payTransferJob)
    .Build();

scheduler.ScheduleJob(payTransferJob, cronTriggerTheyGetPaid);

Once the above is build and run the scheduled tasks should as shown in the output console:

In the above scheduling definition, I tweaked the scheduled CRON expressions to coincide closely with my current time so that I could test the jobs being triggered.

In a real-world scenario, we may have schedules that run less frequently.

That is all for today’s post.

I hope you found this post useful and informative.

Social media & sharing icons powered by UltimatelySocial