Asp.net Core Quartz job calling controller method, but context

Viewed 845

I have an aspnetcore server (serving a Blazor wasm app), which has a Quartz scheduled job running. When the job triggers, it is calling a method on one of my server's controllers.

If I call this method on my controller normally (e.g. via a web API call), it works fine.

When I call the method from the Quartz IJob, the DbContext used in the controller seems to be disposed. I've tried injecting the controller into the job in the normal way, and also via IServiceProvider, and both have the same result.

Controller:

public class NotificationController : ControllerBase
{
    private readonly ApplicationDbContext context;
    
    public NotificationService(ApplicationDbContext context)
    {
        this.context = context;
    }

    public async Task MyMethod()
    {
        await context.SaveChangesAsync(); //This is where it fails when Quartz calls it, seems context is not populated
    }
}

My job (IServiceProvider attempt):

public class ReminderJob : IJob
{
    private readonly IServiceProvider serviceProvider;

    public ReminderJob(IServiceProvider serviceProvider)
    {
        this.serviceProvider = serviceProvider;
    }

    public async Task Execute(IJobExecutionContext jobcontext)
    {
        using (var scope = serviceProvider.CreateScope())
        {
            await scope.ServiceProvider.GetRequiredService<NotificationController>().MyMethod();
        }
    }
}

My job (DI attempt):

public class ReminderJob : IJob
{
    private readonly NotificationController notificationController;

    public ReminderJob(NotificationController notificationController)
    {
        this.notificationController = notificationController;
    }

    public async Task Execute(IJobExecutionContext jobcontext)
    {
        await notificationController.MyMethod();
    }
}

My Startup.cs (relevant lines in ConfigureServices):

public void ConfigureServices(IServiceCollection services)
    {
        services.AddSignalR();
        services.AddControllersWithViews();
        services.AddMvcCore().AddControllersAsServices();
        services.AddRazorPages();

        services.AddQuartz(q =>
        {
            q.UseMicrosoftDependencyInjectionScopedJobFactory(); //I also tried passing options.CreateScope to this method, but no difference
            q.AddJobAndTrigger<ReminderJob>(configuration);
        });
        services.AddQuartzHostedService(q => q.WaitForJobsToComplete = true);

        services.AddDbContext<ApplicationDbContext>(options => 
               options.UseSqlServer(configuration.GetConnectionString("DefaultConnection")));

}

No exception is thrown in VS when it attempts to do context.SaveChangesAsync(), however, a breakpoint directly after it is not hit, however when I check the details of context while debugging, it doesn't seem to be populated correctly.

How do I use the Controller from within the IJob, and ensure the Controller's dependencies are not disposed of?

1 Answers

How do I use the Controller from within the IJob

Do not use the controller in the Job.

Move/Extract/Refactor the desired functionality into a service abstraction

//Service abstraction
public interface INotificationService {
    Task MyMethod();
}

public class NotificationService : INotificationService {
    private readonly ApplicationDbContext context;

    public NotificationService(ApplicationDbContext context) {
        this.context = context;
    }

    public async Task MyMethod() {
        await context.SaveChangesAsync();
    }
}

and have the job and controller depend on the service to invoke the desired functionality.

public class NotificationController : ControllerBase {
    private readonly INotificationService service;
    
    public NotificationController (INotificationService service ) {
        this.service = service ;
    }    

    public async Task<IActionResult> MyMethod() {
        await service.MyMethod(); 
        return Ok();
    }
}

public class ReminderJob : IJob {
    private readonly INotificationService service;

    public ReminderJob(INotificationService service) {
        this.service = service;
    }

    public Task Execute(IJobExecutionContext jobcontext) {
        return service.MyMethod();
    }
}

And of course register all the necessary services with the DI container.

//...

services.AddScoped<INotificationService, NotificationService>();

//...
Related