Skip to content

Instantly share code, notes, and snippets.

@Antaris
Created May 21, 2019 19:57
Show Gist options
  • Star 6 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Antaris/9c3c097d31a90da279e3e6d78497a369 to your computer and use it in GitHub Desktop.
Save Antaris/9c3c097d31a90da279e3e6d78497a369 to your computer and use it in GitHub Desktop.
A Quartz.NET job factory using Microsoft.Extensions.DependencyInjection and scoped services
using System;
using System.Collections.Generic;
using Microsoft.Extensions.DependencyInjection;
using Quartz;
using Quartz.Spi;
public class ServiceProviderJobFactory : IJobFactory
{
private readonly IServiceProvider _rootServiceProvider;
private ConcurrentDictionary<Type, IServiceScope> _scopes = new ConcurrentDictionary<Type, IServiceScope>();
public ServiceProviderJobFactory(IServiceProvider rootServiceProvider)
{
_rootServiceProvider = rootServiceProvider ?? throw new ArgumentNullException(rootServiceProvider);
}
public IJob NewJob(TriggerFireBundle bundle, IScheduler scheduler)
{
var jobType = bundle.JobDetail.JobType;
// MA - Generate a scope for the job, this allows the job to be registered
// using .AddScoped<T>() which means we can use scoped dependencies
// e.g. database contexts
var scope = _scopes.GetOrAdd(jobType, t => _rootServiceProvider.CreateScope());
return (IJob)scope.ServiceProvider.GetRequiredService(jobType);
}
public void ReturnJob(IJob job)
{
var jobType = job?.GetType();
if (job != null && _scopes.TryGetValue(jobType, out var scope)
{
// MA - Dispose of the scope, which disposes of the job's dependencies
scope.Dispose();
// MA - Remove the scope so the next time the job is resolved,
// we can get a new job instance
_scopes.TryRemove(jobType, out _);
}
}
}
@Antaris
Copy link
Author

Antaris commented May 21, 2019

One problem with this design is that the cache key type is the Type instance, which means overlapping jobs would resolve to the same IServiceScope instance, which could lead to unexpected disposal of services in subsequent jobs if they do overlap.

Alternatively, instead of using the Type as a cache key, use the job instance itself:

IJob NewJob(...)
{
	var jobType = bundle.JobDetail.JobType;
	var scope = _rootServiceProvider.CreateScope();
	var job = (IJob)scope.ServiceProvider.GetRequiredService(jobType);
	
	_scopes.TryAdd(job, scope);
	return job;
}

void ReturnJob(IJob job)
{
	if (job != null && _scopes.TryGetValue(job, out var scope)
	{
		scope.Dispose();
		_scopes.TryRemove(job, out _);
	}
}

@AndyPook
Copy link

How about https://github.com/AndyPook/QuartzHostedService/blob/master/QuartzHostedService/ScopedJobFactory.cs as another alternative?
Tightly associates the scope with the job, avoids the dictionary...

@RuslanPr0g
Copy link

Why the factory pattern must be used? Are there other ways to achieve the same result in a much cleaner way?

@Antaris
Copy link
Author

Antaris commented Feb 19, 2021

Quartz now has a baked in solution using their new Quartz.Extensions.Hosting package

https://andrewlock.net/using-quartz-net-with-asp-net-core-and-worker-services/

@RuslanPr0g
Copy link

Quartz now has a baked in solution using their new Quartz.Extensions.Hosting package

https://andrewlock.net/using-quartz-net-with-asp-net-core-and-worker-services/

Thank you, I literally have found that blog at the same time you wrote me :)

@td28354
Copy link

td28354 commented Mar 2, 2021

I checked the provided link about Quartz.Extensions.Hosting but what happens If I need to create jobs based on some information received after the worker service is started.
Let's say I have a BackgroundService hosted in a windows service and this windows service is responsible for creating new jobs based on some information received from other applications, let's say, a desktop application; unfortunately the above link about Quartz.Extensions.Hosting, does not help at all. Please correct me if I'm wrong.

Thanks, AndyPook for the provided sample.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment