Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web / ASP.NET / ASP.NET-Core

Understanding Dependency Injection in .NET Core with Quartz.NET example

4.72/5 (16 votes)
14 Sep 2020CPOL5 min read 31.2K  
The article shows how to perform dependency injection when using Quartz.NET library employing standard .NET Core DI container library. Also, we'll focus on a couple of other useful .NET core techniques.
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.

Introduction

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.

C#
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:

JavaScript
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

JavaScript
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:

JavaScript
Microsoft.Extensions.Configuration
Microsoft.Extensions.Configuration.FileExtensions
Microsoft.Extensions.Configuration.Json

Let's create a file with such name and extract our URL there:

JavaScript
{
  "connection": {
    "Url": "http://i.ua"
  }
}

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

C#
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.

C#
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:

C#
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:

C#
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:

C#
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.

C#
sched.JobFactory = new DemoJobFactory(serviceProvider);

Understanding service lifetimes

In the previous section, we have registered our services with scoped lifetime. However, there is no actual thinking presented here why we have chosen it over other options such as transient or singleton lifetime.

Let’s examine what are the other options. In order to achieve this, we’ll add some trace statements to our classes constructors.

C#
public DemoService()
{
    Console.WriteLine("DemoService started");
}

And the job constructor

C#
public DemoJob(IDemoService demoService, IOptions<DemoJobOptions> options)
{
    _demoService = demoService;
    _options = options.Value;
    Console.WriteLine("Job started");
}

The service registration is as follows:

C#
serviceCollection.AddTransient<DemoJob>();
serviceCollection.AddTransient<IDemoService, DemoService>();

After we run the program we’ll observe the following output:

DemoService started
Job started
calling http://i.ua
DemoService started
Job started
calling http://i.ua
DemoService started
Job started
calling http://i.ua

The output is pretty self-explanatory: we create a new instance each time we call service. Changing both registrations to AddScoped or AddSingleton produces the same result:

DemoService started
Job started
calling http://i.ua
calling http://i.ua
calling http://i.ua

Both instances are constructed just once at application startup. Let’s consult with the documentation to see what are the difference between those lifetimes and why the produce the same result for a given example.

Scoped lifetime services are created once per client request (connection).

Here is what singleton does

Singleton lifetime services are created the first time they’re requested.

So in our case, we have a single request because we use console application. This is the reason why both service lifetimes act the same.

The last topic most of DI-related articles do not cover is a composition of services with different lifetimes. Although there is something worth mentioning. Here is the example of registration.

C#
serviceCollection.AddSingleton<DemoJob>();
serviceCollection.AddTransient<IDemoService, DemoService>();

which means that we inject transient dependency into singleton service. One might expect that since we declared IDemoService as transient it will be constructed each time. The output, however, is quite different:

DemoService started
Job started
calling http://i.ua
calling http://i.ua
calling http://i.ua

So again both services are constructed at the application startup. Here we see that lifetime of transient service gets promoted by the service that uses it. This leads to an important application. The service we’ve registered as transient might be not designed to be used as a singleton because it is not written in thread-safe fashion or for some other reasons. However, it becomes singleton in this case which may lead to some subtle bugs. This brings us to the conclusion that we shouldn’t register services as singletons unless we have some good reason for it i.e. service that manages global state. It’s is preferable to register services as transient.

The opposite, however, yields no surprises.

C#
serviceCollection.AddTransient<DemoJob>();
serviceCollection.AddSingleton<IDemoService, DemoService>();

produces

DemoService started
Job started
calling http://i.ua
Job started
calling http://i.ua
Job started
calling http://i.ua

Here each new instance of a job reuses the same singleton DemoService.

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:

JavaScript
public class DemoJobOptions
{
    public string Url { get; set; }
}

Now we populate them as follows:

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

And inject them into a constructor. Not that we inject IOptions<T>, not the options instance directly.

C#
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.

History

22nd February 2019 - initial version

14th September 2020 - added "Understanding service lifetimes" section.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)