How to integrate Quartz.Net jobs that require "scoped" services injected in them (ASP.NET Core 2.0)?
Asked Answered
P

2

5

I am trying to create some Quartz.Net jobs following my own answer from this question. However, if the job is fairly complex and required "scoped" (services.AddScoped<.., ...>) services, the example does not work because jobs are created as singletons.

If I change them to be scoped, the serviceProvider does not contain the services I need. I have managed to make it work using the following code:

Startup.cs

/// <summary>
/// service provider to be used by qiaryz job factory which cannot use its default provider
/// since child services are scoped and the jobs are singleton
/// </summary>
public static IServiceProvider QuartzScopedProvider { get; private set; }

private void ConfigureQuartz(IServiceCollection services, params Type[] jobs)
{
    services.AddSingleton<IJobFactory, QuartzJobFactory>();
    services.Add(jobs.Select(jobType => new ServiceDescriptor(jobType, jobType, ServiceLifetime.Singleton)));

    QuartzScopedProvider = services.BuildServiceProvider();

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

/// <summary>
/// configures quartz services
/// </summary>
/// <param name="services"></param>
protected virtual void ConfigureJobsIoc(IServiceCollection services)
{
    // all custom services are already defined at this point

    ConfigureQuartz(services, typeof(ComplexJob));
}

/// <summary>
/// configures and starts async jobs (Quartz)
/// </summary>
/// <param name="app"></param>
/// <param name="lifetime"></param>
protected virtual void StartJobs(IApplicationBuilder app, IApplicationLifetime lifetime)
{
    var scheduler = app.ApplicationServices.GetService<IScheduler>();

    QuartzServicesUtilities.StartJob<ComplexJob>(scheduler, TimeSpan.FromMinutes(60));

    lifetime.ApplicationStarted.Register(() => scheduler.Start());
    lifetime.ApplicationStopping.Register(() => scheduler.Shutdown(waitForJobsToComplete: true));
}

QuartzJobFactory.cs

The job factory does not use the injected service provider, but the one explicitly constructed in Startup.cs

public class QuartzJobFactory : IJobFactory
{
    private readonly IServiceProvider _serviceProvider;

    /// <inheritdoc/>
    public QuartzJobFactory()
    {
        // _serviceProvider = serviceProvider;
        _serviceProvider = Startup.QuartzScopedProvider;
    }

    /// <inheritdoc/>
    public IJob NewJob(TriggerFiredBundle bundle, IScheduler scheduler)
    {
        var jobDetail = bundle.JobDetail;

        // this fails with injected service provider:
        // 1: cannot inject scoped services in singleton service
        // 2: if jobs are scoped, the provider cannot solve the injected services
        var job = (IJob)_serviceProvider.GetService(jobDetail.JobType);
        return job;
    }

    /// <inheritdoc/>
    public void ReturnJob(IJob job) { }
}

I am wondering if this is a good way to deal with Quartz jobs in ASP.NET Core 2.0 because it looks more like a hack than a real solution.

Question: How to integrate Quartz.Net jobs that require "scoped" services injected in them (ASP.NET Core 2.0)?

Pinprick answered 14/3, 2019 at 18:56 Comment(2)
For Scoped service which is corresponding to every request, you should not define as static property. Try to inject IServiceProvider to QuartzJobFactory(IServiceProvider serviceProvider).Buncombe
@TaoZhou - my initial code inject IServiceProvider, but fails because the jobs are defined as singletons (which might make sense, since I typically want a single running instance) and other services as "scoped" (I need this because they provide business logic to other parts of the application and have to be scoped). If I make the jobs scoped, the injected provider is not able to get me an instance of the job. The code I have written seem to work OK, I just do not like the pattern (does not seem right).Pinprick
B
5

For resoving scoped service from IServiceProvider, try

    using (var scope = _serviceProvider.CreateScope())
    {
        var job = (IJob)scope.ServiceProvider.GetService(jobDetail.JobType);            
    }
Buncombe answered 15/3, 2019 at 5:20 Comment(4)
I have tried this and had to change add jobs as scoped instead of singletons (due to dependent scoped service). However, after doing this the dbcontext seems messed up (tried to use an already disposed context). There is nothing strange about how I add it (services.AddDbContext<CustomApiContext>(opt => opt.UseSqlServer(Configuration.GetConnectionString("Default")));)Pinprick
@Alexei What do you mean by had to change add jobs as scoped? For this 2: if jobs are scoped, the provider cannot solve the injected services, it indicates that you are injected as scoped job. Is there any demo to reproduce your issue?Buncombe
My initial jobs registration is as following: services.Add(jobs.Select(jobType => new ServiceDescriptor(jobType, jobType, ServiceLifetime.Singleton))). This fails with an error like "cannot inject scoped service ... into MyJob" since MyJob is a singleton. It makes sense, so I have changed to "services.Add(jobs.Select(jobType => new ServiceDescriptor(jobType, jobType, ServiceLifetime.Scoped)));" . Now the database context says that I am trying to access a disposed context and it is not clear what to do next.Pinprick
Your answer is correct and my issue relied somewhere as I have finally found out here. Thanks.Pinprick
S
3

Starting from Quartz 3.1 there is no need to create custom JobFactory (or JobRunner) to use scoped services. We can use in-build job factory, that alredy create scope for us:

services.AddQuartz(q =>  
{                   
    q.UseMicrosoftDependencyInjectionScopedJobFactory();
});

From the docs. Under the hood, MicrosoftDependencyInjectionJobFactory creates scope before job execution and use wrapper ScopedJob to run target job inside scope.

Sari answered 8/12, 2020 at 8:4 Comment(3)
Thanks for the update. In the meantime, I have switched to HangFire which did not have any of Quartz' issues back then, but it is good to know anyway.Pinprick
Do you guys know any example of how to inject a service into a job in version 3.3.2 of Quartz?Hankering
Deprecated from version 3.7 onwardsNarcotism

© 2022 - 2024 — McMap. All rights reserved.