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;
...
}
}
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.