What is an correct way to inject db context to Hangfire Recurring job?
Asked Answered
K

4

14

I'm using HangFire to send emails to users in the background, regularly.

I'm obtaining email addresses from database, but I'm not sure whether I'm "injecting" database context to service that's responsible for sending emails correctly

This works correctly, is there a better way to do it?

public void Configure(IApplicationBuilder app, IHostingEnvironment env, Context context)
{
    (...)

    app.UseHangfireDashboard();
    app.UseHangfireServer(new BackgroundJobServerOptions
    {
        HeartbeatInterval = new System.TimeSpan(0, 0, 5),
        ServerCheckInterval = new System.TimeSpan(0, 0, 5),
        SchedulePollingInterval = new System.TimeSpan(0, 0, 5)
    });

    RecurringJob.AddOrUpdate(() => new MessageService(context).Send(), Cron.Daily);

    (...)
    app.UseMvc();
}

public class MessageService
{
    private Context ctx;

    public MessageService(Context c)
    {
        ctx = c;
    }

    public void Send()
    {
        var emails = ctx.Users.Select(x => x.Email).ToList();

        foreach (var email in emails)
        {
            sendEmail(email, "sample body");
        }
    }
}
Krimmer answered 28/11, 2018 at 8:42 Comment(1)
Hangfire Docs: Using IoC containersTortfeasor
S
20

I just looked to the similar question and did not find the information in one place, so posting my solution here.

Assume you have your Context configured as a service, i.e.

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers();
    ....
    services.AddDbContext<Context>(options => { ... });
    ....
}

This makes the IServiceProvider capable to resolve the Context dependency.

Next, we need to update the MessageService class in order to not hold the Context forever but instantiate it only to perform the task.

public class MessageService
{
    IServiceProvider _serviceProvider;
    public MessageService(IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider;
    }

    public void Send()
    {
        using (IServiceScope scope = _serviceProvider.CreateScope())
        using (Context ctx = scope.ServiceProvider.GetRequiredService<Context>())
        {
            var emails = ctx.Users.Select(x => x.Email).ToList();

            foreach (var email in emails)
            {
                sendEmail(email, "sample body");
            }
        }
    }
}

And finally we ask Hangfire to instantiate the MessageService for us, it will also kindly resolve the IServiceProvider dependency for us:

RecurringJob.AddOrUpdate<MessageService>(x => x.Send(), Cron.Daily);
Schoonover answered 5/10, 2019 at 4:23 Comment(3)
No, no, no, NNNOOOOOO. Please don't inject the IServiceProvider. Change ctor to be public MessageService(Context context) and use the given object in your .Send() method. Providing the IServiceProvider is the service-locator anti-pattern.Ambry
@Ambry did you see the context? It is a background scheduler so there is nothing to automatically handle the scope of injected service. Am I missing something?Cervical
@Christian: Added my own answer to show how to avoid forwarding the service provider in recurring jobs.Ambry
A
4

When adding a recurring job to Hangfire you should use one of the generic method overloads available. In that case you will get your instances in your own scope:

public void Configure(IApplicationBuilder app, IHostingEnvironment env, Context context)
{
    (...)

    app.UseHangfireDashboard();
    app.UseHangfireServer(new BackgroundJobServerOptions
    {
        HeartbeatInterval = new System.TimeSpan(0, 0, 5),
        ServerCheckInterval = new System.TimeSpan(0, 0, 5),
        SchedulePollingInterval = new System.TimeSpan(0, 0, 5)
    });

    RecurringJob.AddOrUpdate<MessageService>("DailyMail", service => service.Send(), Cron.Daily);

    (...)
    app.UseMvc();
}

By using this setup you can get your services as in any normal job you have enqueued:

public class MessageService
{
    private Context ctx;

    public MessageService(Context c)
    {
        ctx = c;
    }

    public void Send()
    {
        var emails = ctx.Users.Select(x => x.Email).ToList();

        foreach (var email in emails)
        {
            sendEmail(email, "sample body");
        }
    }
}
Ambry answered 22/12, 2023 at 7:22 Comment(2)
but how do you manage the lifetime of MessageService if it has been registered as a scoped service? Also imagine it is an EF Core DbContext and it has to be disposed since EF Core is using a pool of connection to database server between multiple DbContext instances and thus each one has to release/return the connection to the pool at the end of the scope? In the latter case we would also need to call DbContext.Dispose() when we are done and it could be an approach (this done automatically by the service scope when used though). But what about the lifetime of the a scoped services??Cervical
All services will be instantiated in an job scope, when the job needs to run. The recurring entry does nothing. It is just the serialized class and method names and the serialized arguments. When the job must be run, a normal job will be enqueued and instantiated like any other job that was enqueued.Ambry
D
1

Definitely need to use DI (StructureMap or etc) for your issue. Please refactor your config file and decouple the "Context" class dependency from config class. Also introduce a container class to map DI classes (auto or manual).

Create Container class

Add container to Hangfire:

GlobalConfiguration.Configuration.UseStructureMapActivator(Bootstrapper.Bootstrap());

Also change job registration in config class:

RecurringJob.AddOrUpdate<MessageService>(x => x.Send(), Cron.Daily);
Drachm answered 2/12, 2018 at 4:52 Comment(0)
V
1

When you access IServiceProvider directly, you circumvent the natural scoping mechanism provided by the ASP.NET dependency injection container, leading to potential issues with the lifetime and management of your service instances.

Using IServiceProvider directly in a controller may result in unintended behavior and problems like:

Scope-related bugs: Resolving a scoped or singleton service in a transient controller can lead to unexpected shared state between requests, causing data corruption or concurrency issues.

Resource Leaks: Manually managing the lifetime of services obtained through IServiceProvider can result in improper disposal, leading to resource leaks and degraded application performance.

Inconsistent Scoping: Different parts of your application might expect different service lifetimes. Using IServiceProvider directly in multiple places can make it challenging to maintain a consistent scope for a service throughout the application.

While creating your own scope, as suggested in the top-rated answer, might technically work, it's considered a code smell and unnecessary.

Valentinvalentina answered 27/7, 2023 at 6:41 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.