Background working scheduled task in ASP.NET Core MVC application

Using Quartz for scheduling background tasks in .NET Core MVC applications

Caching common data like lookups for example in memory of your app can increase significantly your MVC web application performances and response time. Of course this data has to be periodically refreshed.

You can use various approaches to refresh data including expiry, but this can create potential bottle necks in your application since once data expires, you will use request thread to pull the data, cache and serve the request response back. Another approach that I prefer is to create a scheduled cache refresh with the background worker which periodically refreshes the cache. This is easy to implement in .NET but you can always rely on out of the box libraries like Quartz.

Quartz is well supported and cross platform library for scheduling tasks inside your application. Because of the ASP.NET Core architecture and out of the box middleware support it is a bit different to use it in a .NET Core MVC web application. In this article I will try try to explain simple schedule of a background worker scheduled task using Quartz in ASP.NET Core application.

Project setup

I am using simple ASP.NET Core WebApi project template as a base for the example project. Since we will not focus on the actual response from the endpoint, but rather on the Startup.cs dependency injection part and the middleware, default WebApi project template is just fine.

As a first step we need to add Quartz NuGet package to our project. I will use NuGet package manager since I am using Visual Studio 2017 Community edition.

PM> Install-Package Quartz -Version 3.0.6

If you are doing development on a Mac or Linux, you will have to use dotnet CLI from your console

> dotnet add package Quartz --version 3.0.6

Dependency injection setup

As ASP.NET Core has out of the box dependency injection support, we need to setup our resolvers in the startup, but before we do that we need to write our implementation of some of the Quartz interfaces we are going to use to setup the scheduled task in our project.

We first need to write our task, the unit of code which will be executed on a specific schedule. For this we need to implement Quartz.IJob interface in our job class.

using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Quartz;
using System;
using System.Threading.Tasks;

namespace Schedule.WebApiCore.Sample.Schedule
{
    public class ScheduledJob : IJob
    {
        private readonly IConfiguration configuration;
        private readonly ILogger<ScheduledJob> logger;


        public ScheduledJob(IConfiguration configuration, ILogger<ScheduledJob> logger)
        {
            this.logger = logger;
            this.configuration = configuration;
        }

        public async Task Execute(IJobExecutionContext context)
        {

            this.logger.LogWarning($"Hello from scheduled task {DateTime.Now.ToLongTimeString()}");

            await Task.CompletedTask;
            
        }
    }
}

    

Since it is just an example application I will only write the message with he current time in the output every time job executes. Instances of Microsoft.Extensions.Configuration.IConfiguration and Microsoft.Extensions.Logging.ILogger interface implementations will be injected from the Startup.cs class methods.

Next interface we need to implement is Quartz.Spi.IjobFactory.

using Quartz;
using Quartz.Spi;
using System;

namespace Schedule.WebApiCore.Sample.Schedule
{
    public class ScheduledJobFactory : IJobFactory
    {
        private readonly IServiceProvider serviceProvider;

        public ScheduledJobFactory(IServiceProvider serviceProvider)
        {
            this.serviceProvider = serviceProvider;
        }

        public IJob NewJob(TriggerFiredBundle bundle, IScheduler scheduler)
        {
            return serviceProvider.GetService(typeof(IJob)) as IJob;
        }

        public void ReturnJob(IJob job) {
            var disposable = job as IDisposable;
            if (disposable != null)
            {
                disposable.Dispose();
            }
        }
        
    }
}

    

Instance of our IJobFactory interface implementation will we assigned to our IScheduler instance which will be used to instantiate a job instance on every schedule trigger. Now to setup the dependency injection resolvers in our Startup.cs

We'll need to setup resolvers for our job instances, our scheduler and our trigger. There is one one class related to job and that is IJobDetail which wraps the IJob instance and provides additional details for the IJob instance.

using System;
using System.IO;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Quartz;
using Quartz.Impl;
using Quartz.Spi;
using Schedule.WebApiCore.Sample.Schedule;


namespace Schedule.WebApiCore.Sample
{
    public class Startup
    {

        public IConfiguration Configuration { get; }
        public IHostingEnvironment HostingEnvironment { get; }

        public Startup(IConfiguration configuration, IHostingEnvironment hostingEnvironment)
        {
            this.HostingEnvironment = hostingEnvironment;
            this.Configuration = configuration;
        }


        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddLogging();

            services.AddSingleton<IConfiguration>(new ConfigurationBuilder()
                .SetBasePath(Directory.GetCurrentDirectory())
                .AddJsonFile("appsettings.json")
                .AddJsonFile($"appsettings.{this.HostingEnvironment.EnvironmentName.ToLower()}.json")
                .Build());

            #region Configure Quartz DI

            services.Add(new ServiceDescriptor(typeof(IJob), typeof(ScheduledJob), ServiceLifetime.Transient));
            services.AddSingleton<IJobFactory, ScheduledJobFactory>();
            services.AddSingleton<IJobDetail>(provider =>
            {
                return JobBuilder.Create<ScheduledJob>()
                  .WithIdentity("Sample.job", "group1")
                  .Build();
            });

            services.AddSingleton<ITrigger>(provider =>
            {
                return TriggerBuilder.Create()
                .WithIdentity($"Sample.trigger", "group1")
                .StartNow()
                .WithSimpleSchedule
                 (s =>
                    s.WithInterval(TimeSpan.FromSeconds(5))
                    .RepeatForever()
                 )
                 .Build();
            });

            services.AddSingleton<IScheduler>(provider =>
            {
                var schedulerFactory = new StdSchedulerFactory();
                var scheduler = schedulerFactory.GetScheduler().Result;
                scheduler.JobFactory = provider.GetService<IJobFactory>();
                scheduler.Start();
                return scheduler;
            });

            #endregion


            services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IHostingEnvironment env, IScheduler scheduler)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            scheduler.ScheduleJob(app.ApplicationServices.GetService<IJobDetail>(), app.ApplicationServices.GetService<ITrigger>());

            app.UseMvc();
        }
    }
}

    

Now we have all setup for a test run. I published the sample project targeting linux-x64 platform in order to run in on my Ubuntu Server VM. Quartz as recognized as an important library is already ported for Linux host, so you can include it in your application regardless if you are going to run it on Windows or maybe Linux or Docker container.

Quartz Linux

It is easier like this to explain the whole DI setup when you have all parts in one place in Startup.cs, where you can see how all dependencies and interfaces are resolved, but for a long run, this Startup.cs is to messy and it will eventually become a nightmare to maintain. For this reason I wrapped the whole DI into the extension method.

Wrapping dependency injection into extension method

As mentioned, leaving Startup.cs like this is not a god practice, especially when you know you will probably add more middleware components to the pipeline and more dependency injection settings. For this reason, I moved whole Quartz code to a static class which will extend Microsoft.Extensions.DependencyInjection.IServiceCollection with dependency injection statements for Quartz and Microsoft.AspNetCore.Builder.IApplicationBuilder for the pipeline

using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Quartz;
using Quartz.Impl;
using Quartz.Spi;
using System;

namespace Schedule.WebApiCore.Sample.Schedule
{
    public static class QuartzExtensions
    {
        public static void AddQuartz(this IServiceCollection services, Type jobType)
        {
            services.Add(new ServiceDescriptor(typeof(IJob), jobType, ServiceLifetime.Transient));
            services.AddSingleton<IJobFactory, ScheduledJobFactory>();
            services.AddSingleton<IJobDetail>(provider =>
            {
                return JobBuilder.Create<ScheduledJob>()
                  .WithIdentity("Sample.job", "group1")
                  .Build();
            });

            services.AddSingleton<ITrigger>(provider =>
            {
                return TriggerBuilder.Create()
                .WithIdentity($"Sample.trigger", "group1")
                .StartNow()
                .WithSimpleSchedule
                 (s =>
                    s.WithInterval(TimeSpan.FromSeconds(5))
                    .RepeatForever()
                 )
                 .Build();
            });

            services.AddSingleton<IScheduler>(provider =>
            {
                var schedulerFactory = new StdSchedulerFactory();
                var scheduler = schedulerFactory.GetScheduler().Result;
                scheduler.JobFactory = provider.GetService<IJobFactory>();
                scheduler.Start();
                return scheduler;
            });

        }

        public static void UseQuartz (this IApplicationBuilder app)
        {
            app.ApplicationServices.GetService<IScheduler>()
                .ScheduleJob(app.ApplicationServices.GetService<IJobDetail>(), 
                app.ApplicationServices.GetService<ITrigger>()
                );
        }
    }
}

    

Now our Startup.cs is more cleaner and a lot easier to maintain and extend

using System;
using System.IO;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Quartz;
using Quartz.Impl;
using Quartz.Spi;
using Schedule.WebApiCore.Sample.Schedule;

namespace Schedule.WebApiCore.Sample
{
    public class Startup
    {
        public IConfiguration Configuration { get; }
        public IHostingEnvironment HostingEnvironment { get; }

        public Startup(IConfiguration configuration, IHostingEnvironment hostingEnvironment)
        {
            this.HostingEnvironment = hostingEnvironment;
            this.Configuration = configuration;
        }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddLogging();

            services.AddSingleton<IConfiguration>(new ConfigurationBuilder()
                .SetBasePath(Directory.GetCurrentDirectory())
                .AddJsonFile("appsettings.json")
                .AddJsonFile($"appsettings.{this.HostingEnvironment.EnvironmentName.ToLower()}.json")
                .Build());

            services.AddQuartz(typeof(ScheduledJob));

            services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
			
            app.UseQuartz();
			
            app.UseMvc();
        }
    }
}

    

If you want to try this code, there is complete sample project can be found on GitHub at https://github.com/dejanstojanovic/dotnetcore-mvc-quartz.

References

Disclaimer

Purpose of the code contained in snippets or available for download in this article is solely for learning and demo purposes. Author will not be held responsible for any failure or damages caused due to any other usage.


About the author

DEJAN STOJANOVIC

Dejan is a passionate Software Architect/Developer. He is highly experienced in .NET programming platform including ASP.NET MVC and WebApi. He likes working on new technologies and exciting challenging projects

CONNECT WITH DEJAN  Loginlinkedin Logintwitter Logingoogleplus Logingoogleplus

JavaScript

read more

SQL/T-SQL

read more

Umbraco CMS

read more

PowerShell

read more

Comments for this article