DEV Community

Cover image for Dependency Injection for Quartz.NET in .NET Core
Bohdan Stupak
Bohdan Stupak

Posted on

Dependency Injection for Quartz.NET in .NET Core

The post was originally published at codeproject

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 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 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 of. For the sake of simplicity, it does quite a humble job.

The project QuartzDI.Demo is our working project which contains simple 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;
    }
}

which 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 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 more thorough understanding what actually .NET Core brings to our table.

At this point, our project is up and running. And what is most important it is 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 a 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 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 appsettings.json mechanism for that matter.

In order to start working with .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 name and extract our URL there:

{
  "connection": {
    "Url": "https://i.ua"
  }
}

Now we can extract our value from the config file as follows:

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 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 job might be called without service provided thus failing or changing the dependency during the object runtime which makes it harder to reason about objects. 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 built-in .NET Core DI container which comes with a Nuget package Microsoft.Extensions.DependencyInjection.

Now we specify 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 a 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 follows:

serviceCollection.AddOptions();
serviceCollection.Configure<DemoJobOptions>(options =>
{
    options.Url = connectionSection["Url"];
});

And inject them into a constructor. Not that we inject IOptions, 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.

Top comments (2)

Collapse
 
jsheridanwells profile image
Jeremy Wells

Great tutorial. This actually gives me a much better understanding of all of the magic going on when configuring an Asp.NetCore Startup class.

Collapse
 
bohdanstupak1 profile image
Bohdan Stupak

Hi Jeremy :)
I'm glad it helped.