Search code examples
c#dependency-injection.net-8.0quartz.net

Quartz Dependency Injection with Net8


I'm working with .NET 8 and Quartz 3.11 following the official documentation, and I've encountered an issue: I can't correctly configure Dependency Injection for a job that implements the IJob interface. For example, MyExampleJob has a constructor with parameters that should be injected, but they are not. I tested with an empty constructor, and it enters the empty constructor but not the one with dependencies. I've configured the necessary dependencies, but it doesn't work. I also tried using x.UseMicrosoftDependencyInjectionJobFactory(), but this method will be deprecated. I read several posts, but they were all published several years ago.

public static async Task<IServiceCollection> AddJobServices(this IServiceCollection services)
    {
        services.AddQuartz(q =>
        {
            q.UseMicrosoftDependencyInjectionJobFactory();
            q.UseInMemoryStore();
            q.UseDedicatedThreadPool(tp =>
            {
                tp.MaxConcurrency = 10;
            });

            
            q.AddJob<EventWithoutDrawReminderJob>(opts => opts
                .WithIdentity(nameof(EventWithoutDrawReminderJob))
                .StoreDurably()
                .RequestRecovery());
        });
        
        services.AddQuartzHostedService(options =>
        {
            options.WaitForJobsToComplete = true;
            options.AwaitApplicationStarted = true;
        });
        
        // Register jobs with DI
        services.AddTransient<EventWithoutDrawReminderJob>();
        
        var quartzProperties = new NameValueCollection
        {
            ["quartz.serializer.type"] = "newtonsoft"
        };
        var factory = new StdSchedulerFactory(quartzProperties);
        var list = await factory.GetAllSchedulers(); // Here it's a test an alternative but returns an empty list
        var scheduler = await factory.GetScheduler();
        scheduler.Start().Wait();
        
        services.AddSingleton(scheduler);

        return services;
    }

Definition of Job:

public class EventWithoutDrawReminderJob : IJob
    {
        public IApplicationDbContext DbContext { get; set; }
        public INotificationService NotificationService { get; set; }
        public UserManager<ApplicationUser> UserManager { get; set; }
        public RoleManager RoleManager { get; set; }
        public ILogger Logger { get; set; }

        // public EventWithoutDrawReminderJob()
        // {
        //     Console.WriteLine(" > Starting JobExecution: EventWithoutDrawReminderJob"); // If I uncomment this line, the breakpoint stops here
        // }

        public EventWithoutDrawReminderJob(ApplicationDbContext dbContext,
            INotificationService notificationService, 
            UserManager<ApplicationUser> userManager,
            RoleManager roleManager,
            ILogger logger)
        {
            // This code never runs 
            DbContext = dbContext;
            NotificationService = notificationService;
            UserManager = userManager;
            RoleManager = roleManager;
            Logger = logger;
        }

        public async Task Execute(IJobExecutionContext context)
        {
            // My code using the dependencies
        }
    }

in program.cs

...
builder.Services.AddApplicationServices(appSettings);
await builder.Services.AddJobServices();
...

the AddApplicationServices method has:

public static IServiceCollection AddApplicationServices(this IServiceCollection services, IAppConfigurations appSettings)
    {
        services.AddDbContext<ApplicationDbContext>(options =>
            options.UseSqlServer(configuration.GetConnectionString("testDB"),
                builder =>
                {
                    builder.MigrationsAssembly(typeof(ApplicationDbContext).Assembly.FullName);
                    builder.EnableRetryOnFailure(3);
                    builder.CommandTimeout(30);
                }
            ));

        services.AddScoped<IApplicationDbContext>(provider => provider.GetRequiredService<ApplicationDbContext>());
        
        // Configure Identity
        services
            .AddIdentityCore<ApplicationUser>(options =>
            {
                options.Password.RequiredLength = 6;
                options.Password.RequireNonAlphanumeric = false;
                options.Password.RequireLowercase = false;
                options.Password.RequireUppercase = false;
                options.Password.RequireDigit = false;
            })
            .AddRoles<ApplicationRole>()
            .AddTokenProvider<DataProtectorTokenProvider<ApplicationUser>>(TokenOptions.DefaultProvider)
            .AddEntityFrameworkStores<ApplicationDbContext>()
            .AddDefaultTokenProviders();

        services
            .AddScoped<ApplicationUserManager>()
            .AddScoped<RoleManager>();
        
        services.AddScoped<INotificationService, NotificationService>();
        
        return services;
    }

Solution

The following solution includes a dashboard for all Quartz jobs and triggers. You need to install SilkierQuartz in your project. In your program.cs you need to do: builder.Services.AddJobServices();

public static IServiceCollection AddJobServices(this IServiceCollection services)
    {
        services.AddTransient<MyJob>();

        // Add Quartz services
        services.AddQuartz(q =>
        {
            q.SchedulerName = "example-scheduler";
            
            q.UseDedicatedThreadPool(tp =>
            {
                tp.MaxConcurrency = 5;
            });
            
            // I configured the Quartz at the beginning with InMemoryStore, if you want to change you should comment the q.UsePersistentStore.
            // q.UseInMemoryStore();
            
            // Once you have your Quartz configured, you can switch to a persistent store like SQL Server
            q.UsePersistentStore(s =>
            {
                s.PerformSchemaValidation = true; 
                s.UseProperties = true; 
                s.RetryInterval = TimeSpan.FromSeconds(30);
                s.UseSqlServer(sqlServer =>
                {
                    sqlServer.ConnectionString = "your connection string";
                    sqlServer.TablePrefix = "QRTZ_";
                });
                s.UseNewtonsoftJsonSerializer();
                s.UseClustering(c =>
                {
                    c.CheckinMisfireThreshold = TimeSpan.FromSeconds(20);
                    c.CheckinInterval = TimeSpan.FromSeconds(10);
                });
            });
            
            // Register jobs
            q.AddJob<MyJob>(opts => opts
                .WithIdentity(nameof(MyJob))
                .StoreDurably()
                .RequestRecovery());
            
            // If you have triggers 
            // q.AddTrigger(opts => opts
            //     .ForJob(nameof(WeeklyEventsMailingJob))
            //     .WithIdentity("WeeklyEventsMailingTrigger")
            //     .WithCronSchedule("0 0 5 ? * MON *")
            //     .StartNow());
        });
        
        services.AddQuartzHostedService(options =>
        {
            options.WaitForJobsToComplete = true;
            options.AwaitApplicationStarted = true;
        });
        
        // Add Dashboard with SilkierQuartz and it was configured following the post: https://github.com/maikebing/SilkierQuartz/issues/150
        services.AddSingleton(new SilkierQuartzOptions
        {
            VirtualPathRoot = "/quartz",
            UseLocalTime = true,
            ProductName = "ExampleApp",
            DefaultDateFormat = "yyyy-MM-dd",
            DefaultTimeFormat = "HH:mm:ss",
            CronExpressionOptions = new CronExpressionDescriptor.Options()
            {
                DayOfWeekStartIndexZero = false //Quartz uses 1-7 as the range
            }
        });
        services.AddSingleton(new SilkierQuartzAuthenticationOptions
        {
            AccessRequirement = SilkierQuartzAuthenticationOptions.SimpleAccessRequirement.AllowAnonymous
        });
        services.AddAuthorization(
            opts => opts.AddPolicy(
                "SilkierQuartz",
                builder => builder.AddRequirements(
                    new SilkierQuartzDefaultAuthorizationRequirement(
                        SilkierQuartzAuthenticationOptions.SimpleAccessRequirement.AllowAnonymous))));
        services.AddScoped<IAuthorizationHandler, SilkierQuartzDefaultAuthorizationHandler>();

        return services;
    }

Then, when you need to use the Scheduler in your class, you should inject ISchedulerFactory and use it to obtain an IScheduler instance that has all your jobs with constructors that have dependencies.

public class MyServiceClass
    {
        public ISchedulerFactory SchedulerFactory { get; set; }
        public IScheduler Scheduler { get; set; }

        public MyServiceClass(ISchedulerFactory schedulerFactory, ...)
        {
            SchedulerFactory = schedulerFactory;
            Scheduler = schedulerFactory.GetScheduler().Result;
            ...
        }
    }

Solution

  • Well you aren't following the official documentation. You should not build StdSchedulerFactory yourself, it has no knowledge of the DI configuration you just have created, it's vanilla scheduler factory with defaults.

    You probably also want to configure hosted service integration which will handle starting and stopping of the scheduler automatically.

    There are also examples in Quartz.NET repository, here's one for ASP.NET Core usage.