Introduction
Quartz.NET is a handy library that allows you to schedule recurring tasks via implementing IJob interface. Yet the limitation of it is that, by default, it supports only a parameterless constructor which complicates injecting external service inside of it, i.e., for implementing repository pattern. In this article, we'll take a look at how we can tackle this problem using standard .NET Core DI container.
The whole project referred to in the article is provided inside the following Github repository. In order to better follow the code in the article, you might want to take a look at it.
Project Overview
Let's take a look at the initial solution structure.
The project QuartzDI.Demo.External.DemoService represents some external dependency we have no control over. For the sake of simplicity, it does quite a humble job.
The project QuartzDI.Demo is our working project which contains simple a Quartz.NET job.
- public class DemoJob : IJob
- {
- private const string Url = "https://i.ua";
-
- public static IDemoService DemoService { get; set; }
-
- public Task Execute(IJobExecutionContext context)
- {
- DemoService.DoTask(Url);
- return Task.CompletedTask;
- }
- }
It is set up in a straightforward way:
- var props = new NameValueCollection
- {
- { "quartz.serializer.type", "binary" }
- };
- var factory = new StdSchedulerFactory(props);
- var sched = await factory.GetScheduler();
- await sched.Start();
- var job = JobBuilder.Create<DemoJob>()
- .WithIdentity("myJob", "group1")
- .Build();
- var trigger = TriggerBuilder.Create()
- .WithIdentity("myTrigger", "group1")
- .StartNow()
- .WithSimpleSchedule(x => x
- .WithIntervalInSeconds(5)
- .RepeatForever())
- .Build();
- await sched.ScheduleJob(job, trigger);
We provide our external service via the job's static property.
- DemoJob.DemoService = new DemoService();
As the project is a console application, during the course of the article, we'll have to manually install all needed infrastructure and will be able to build a more thorough understanding of what .NET Core actually brings to the table.
At this point, our project is up and running. And what is most important is, it's dead simple, which is great. But we pay for that simplicity with a cost of application inflexibility, which is fine if we want to leave it as a small tool. But that's often not the case for production systems. So let's tweak it a bit to make it more flexible.
Creating a Configuration File
One of the inflexibilities is that we hard-code the URL we call into a DemoJob. Ideally, we would like to change it and also change it depending on our environment. .NET Core comes with an appsettings.json mechanism for that matter.
In order to start working with the .NET Core configuration mechanism, we have to install a couple of NuGet packages.
- Microsoft.Extensions.Configuration
- Microsoft.Extensions.Configuration.FileExtensions
- Microsoft.Extensions.Configuration.Json
Let's create a file with such a name and extract our URL there,
- {
- "connection": {
- "Url": "http://i.ua"
- }
- }
Now, we can extract our value from the config file as below.
- var builder = new ConfigurationBuilder()
- .SetBasePath(Directory.GetCurrentDirectory())
- .AddJsonFile("appsettings.json", true, true);
- var configuration = builder.Build();
- var connectionSection = configuration.GetSection("connection");
- DemoJob.Url = connectionSection["Url"];
Note that to make it happen, we had to change the URL from constant to property.
- public static string Url { get; set; }
Using Constructor Injection
Injecting service via a static property is fine for a simple project, but for a bigger one, it might carry several disadvantages: such as a job might be called without a service provided, thus failing or changing the dependency during the object runtime. To address these issues, we should employ constructor injection.
Although there is nothing wrong with Pure Dependency Injection and some people argue that you should strive for it, in this article, we'll use a built-in .NET Core DI container which comes with a NuGet package Microsoft.Extensions.DependencyInjection.
Now, let us specify a service we depend on inside constructor arguments.
- private readonly IDemoService _demoService;
- public DemoJob(IDemoService demoService)
- {
- _demoService = demoService;
- }
In order to invoke a parameterful constructor of the job, Quartz.NET provides IJobFactory interface. Here's our implementation.
- public class DemoJobFactory : IJobFactory
- {
- private readonly IServiceProvider _serviceProvider;
-
- public DemoJobFactory(IServiceProvider serviceProvider)
- {
- _serviceProvider = serviceProvider;
- }
-
- public IJob NewJob(TriggerFiredBundle bundle, IScheduler scheduler)
- {
- return _serviceProvider.GetService<DemoJob>();
- }
-
- public void ReturnJob(IJob job)
- {
- var disposable = job as IDisposable;
- disposable?.Dispose();
- }
- }
Let's register our dependencies.
- var serviceCollection = new ServiceCollection();
- serviceCollection.AddScoped<DemoJob>();
- serviceCollection.AddScoped<IDemoService, DemoService>();
- var serviceProvider = serviceCollection.BuildServiceProvider();
The final piece of the puzzle is to make Quartz.NET use our factory. IScheduler has property JobFactory just for that matter.
- sched.JobFactory = new DemoJobFactory(serviceProvider);
- Using Options Pattern
Now, we can pull the same trick with configuration options. Again, our routine starts with a Nuget package. This time Microsoft.Extensions.Options.
Let's create a strongly typed definition for configuration options.
- public class DemoJobOptions
- {
- public string Url { get; set; }
- }
Now, we populate them as below.
- serviceCollection.AddOptions();
- serviceCollection.Configure<DemoJobOptions>(options =>
- {
- options.Url = connectionSection["Url"];
- });
And inject them into a constructor. Note that we inject IOptions<T>, not the options instance directly.
- public DemoJob(IDemoService demoService, IOptions<DemoJobOptions> options)
- {
- _demoService = demoService;
- _options = options.Value;
- }
Conclusion
In this article, we've seen how we can leverage .NET Core functionality to make our use of Quartz.NET more flexible.