89

How can I use .NET Core's default dependency injection in Hangfire?

I am new to Hangfire and searching for an example which works with ASP.NET Core.

Pang
  • 9,564
  • 146
  • 81
  • 122
eadam
  • 23,151
  • 18
  • 48
  • 71

9 Answers9

79

See full example on GitHub https://github.com/gonzigonz/HangfireCore-Example.
Live site at http://hangfirecore.azurewebsites.net/

  1. Make sure you have the Core version of Hangfire:
    dotnet add package Hangfire.AspNetCore

  2. Configure your IoC by defining a JobActivator. Below is the config for use with the default asp.net core container service:

    public class HangfireActivator : Hangfire.JobActivator
    {
        private readonly IServiceProvider _serviceProvider;
    
        public HangfireActivator(IServiceProvider serviceProvider)
        {
            _serviceProvider = serviceProvider;
        }
    
        public override object ActivateJob(Type type)
        {
            return _serviceProvider.GetService(type);
        }
    }  
    
  3. Next register hangfire as a service in the Startup.ConfigureServices method:

    services.AddHangfire(opt => 
        opt.UseSqlServerStorage("Your Hangfire Connection string"));
    
  4. Configure hangfire in the Startup.Configure method. In relationship to your question, the key is to configure hangfire to use the new HangfireActivator we just defined above. To do so you will have to provide hangfire with the IServiceProvider and this can be achieved by just adding it to the list of parameters for the Configure method. At runtime, DI will providing this service for you:

    public void Configure(
        IApplicationBuilder app, 
        IHostingEnvironment env, 
        ILoggerFactory loggerFactory,
        IServiceProvider serviceProvider)
    {
        ...
    
        // Configure hangfire to use the new JobActivator we defined.
        GlobalConfiguration.Configuration
            .UseActivator(new HangfireActivator(serviceProvider));
    
        // The rest of the hangfire config as usual.
        app.UseHangfireServer();
        app.UseHangfireDashboard();
    }  
    
  5. When you enqueue a job, use the registered type which usually is your interface. Don't use a concrete type unless you registered it that way. You must use the type registered with your IoC else Hangfire won't find it. For Example say you've registered the following services:

    services.AddScoped<DbManager>();
    services.AddScoped<IMyService, MyService>();
    

Then you could enqueue DbManager with an instantiated version of the class:

    BackgroundJob.Enqueue(() => dbManager.DoSomething());

However you could not do the same with MyService. Enqueuing with an instantiated version would fail because DI would fail as only the interface is registered. In this case you would enqueue like this:

    BackgroundJob.Enqueue<IMyService>( ms => ms.DoSomething());
Gonzalo Lucero
  • 1,717
  • 17
  • 16
  • 1
    It is worth noting that in the `ms => ms.DoSomething()` case, the expression _must_ call a method on the passed in parameter. You can't do `ms => Foo.StaticMethod(ms)`; Hangfire gives a cryptic exception in this case. – Roman Starkov May 14 '18 at 18:16
  • 46
    Current Hangfire version (1.6.19) automatically registers AspNetCoreJobActivator. https://github.com/HangfireIO/Hangfire/blob/master/src/Hangfire.AspNetCore/HangfireServiceCollectionExtensions.cs#L107 – Der_Meister Jun 22 '18 at 07:54
  • Also, extension methods do not work with Hangfire. Need to create a wrapper. – Der_Meister Jun 22 '18 at 09:12
  • Also if using an interface you must put the hang fire attributes on the interface and not the concrete class - such as attributes affecting retry policy. – Simon_Weaver Nov 03 '18 at 02:40
  • 17
    **This is implementation is wrong and should not be used**. There is a reason the job activator has those `BeginScope` functions. The only way to use this implementation here is to pass it the root service provider. Which means it is never disposed, meaning every time you create a job, you leak all the dependencies that are created. – Voo Jul 24 '19 at 08:11
  • @Voo we have found this out the hard way. We are using HF within a .net core console application, so we don't have ASP.NET Core to handle the lifecycle of the scoped service providers for us. Is there somewhere you could point me to with more information on how to properly register a custom activator using the Microsoft DI container that won't leak the dependencies? – lukevp Mar 02 '20 at 20:40
  • 1
    @Lukevp The MS DI container has a `CreateScope` method for exactly this. If you're running in a console application you presumably manually create a root scope - pass that one into your custom activator and then implement BeginScope and so on. Pretty simple. SInce Hangfire has a correct implementation already you might be able to simply instantiate that one. – Voo Mar 03 '20 at 15:30
  • Hello. I tried this solution, but when it reaches the ActivateJob-method of the JobActivator, it'll throw an exception because the iserviceprovider is disposed.How can I solve this? – Roel de Wit Dec 03 '20 at 16:14
  • 1
    @Voo - mentioning "this implementation is wrong" yet you haven't provided what is the "correct" implementation. Very helpful. thanks – curiousBoy Nov 18 '22 at 06:49
  • @curiousboy I guess my explanation for Luke (which given the upvote and no further communication seems to have been just fine) was too complicated, sorry for that, but a comment isn't the right place for a detailed answer. Also RTFM - the whole thing hasn't been necessary for years. – Voo Nov 18 '22 at 09:17
36

DoritoBandito's answer is incomplete or deprecated.

public class EmailSender {
     public EmailSender(IDbContext dbContext, IEmailService emailService)
     {
         _dbContext = dbContext;
         _emailService = emailService;
     }
}

Register services:

services.AddTransient<IDbContext, TestDbContext>();
services.AddTransient<IEmailService, EmailService>();

Enqueue:

BackgroundJob.Enqueue<EmailSender>(x => x.Send(13, "Hello!"));

Source: http://docs.hangfire.io/en/latest/background-methods/passing-dependencies.html

Daniel Genezini
  • 694
  • 9
  • 12
  • 2
    This is the correct way to achieve the OP's requirements. – Nick Nov 27 '19 at 14:34
  • The key here is when you use the in brackets, a new object is created; and then an action is performed on it . Make sure you add the SERVICE so it works. – FlyingV Oct 20 '20 at 17:57
23

Note: if you want a full sample, see my blog post on this.


All of the answers in this thread are wrong/incomplete/outdated. Here's an example with ASP.NET Core 3.1 and Hangfire.AspnetCore 1.7.

Client:

//...
using Hangfire;
// ...

public class Startup
{
    // ...

    public void ConfigureServices(IServiceCollection services)
    {
        //...
        services.AddHangfire(config =>
        {
            // configure hangfire per your requirements
        });
    }
}

public class SomeController : ControllerBase
{
    private readonly IBackgroundJobClient _backgroundJobClient;

    public SomeController(IBackgroundJobClient backgroundJobClient)
    {
        _backgroundJobClient = backgroundJobClient;
    }
    
    [HttpPost("some-route")]
    public IActionResult Schedule([FromBody] SomeModel model)
    {
        _backgroundJobClient.Schedule<SomeClass>(s => s.Execute(model));
    }
}

Server (same or different application):

{
    //...
    services.AddScoped<ISomeDependency, SomeDependency>();

    services.AddHangfire(hangfireConfiguration =>
    {
        // configure hangfire with the same backing storage as your client
    });
    services.AddHangfireServer();
}

public interface ISomeDependency { }
public class SomeDependency : ISomeDependency { }

public class SomeClass
{
    private readonly ISomeDependency _someDependency;

    public SomeClass(ISomeDependency someDependency)
    {
        _someDependency = someDependency;
    }

    // the function scheduled in SomeController
    public void Execute(SomeModel someModel)
    {

    }
}
Camilo Terevinto
  • 31,141
  • 6
  • 88
  • 120
  • 5
    If I run the server on the same application as the client and I have registered the dependency like this: `services.AddScoped();`, what would the scope of that dependency be? – R. Schreurs Mar 16 '21 at 14:03
  • I would like to know this also. – WiseGuy Oct 03 '22 at 21:05
15

As far as I am aware, you can use .net cores dependency injection the same as you would for any other service.

You can use a service which contains the jobs to be executed, which can be executed like so

var jobId = BackgroundJob.Enqueue(x => x.SomeTask(passParamIfYouWish));

Here is an example of the Job Service class

public class JobService : IJobService
{
    private IClientService _clientService;
    private INodeServices _nodeServices;

    //Constructor
    public JobService(IClientService clientService, INodeServices nodeServices)
    {
        _clientService = clientService;
        _nodeServices = nodeServices;
    }

    //Some task to execute
    public async Task SomeTask(Guid subject)
    {
        // Do some job here
        Client client = _clientService.FindUserBySubject(subject);
    }      
}

And in your projects Startup.cs you can add a dependency as normal

services.AddTransient< IClientService, ClientService>();

Not sure this answers your question or not

DoritoBandito
  • 192
  • 1
  • 4
2

Currently, Hangfire is deeply integrated with Asp.Net Core. Install Hangfire.AspNetCore to set up the dashboard and DI integration automatically. Then, you just need to define your dependencies using ASP.NET core as always.

Ehsan Mirsaeedi
  • 6,924
  • 1
  • 41
  • 46
  • I wanted to send an ILogger into hangfire, but it does not work out of the box by just using that package. – Carlos Jimenez Bermudez Dec 08 '21 at 23:03
  • @CarlosJimenezBermudez is this still the case as far as you know? I'm looking at moving our hangfire possibly to Hangfire.DotNetCore, since I'm running into troubles on the normal 'Hangfire' on NETStandard 2.0 – mochsner Aug 18 '22 at 20:17
  • @mochsner, I was able to work in .Net Core with Hangfire as the answer states, however the hangfire documentation wasn't clear enough to me about it. My mistake had to do with a ILogger not being injected anywhere in the application, rather than a hangfire only problem. – Carlos Jimenez Bermudez Aug 22 '22 at 20:45
2

If you are trying to quickly set up Hangfire with ASP.NET Core (tested in ASP.NET Core 2.2) you can also use Hangfire.MemoryStorage. All the configuration can be performed in Startup.cs:

using Hangfire;
using Hangfire.MemoryStorage;

public void ConfigureServices(IServiceCollection services) 
{
    services.AddHangfire(opt => opt.UseMemoryStorage());
    JobStorage.Current = new MemoryStorage();
}

protected void StartHangFireJobs(IApplicationBuilder app, IServiceProvider serviceProvider)
{
    app.UseHangfireServer();
    app.UseHangfireDashboard();

    //TODO: move cron expressions to appsettings.json
    RecurringJob.AddOrUpdate<SomeJobService>(
        x => x.DoWork(),
        "* * * * *");

    RecurringJob.AddOrUpdate<OtherJobService>(
        x => x.DoWork(),
        "0 */2 * * *");
}

public void Configure(IApplicationBuilder app, IServiceProvider serviceProvider)
{
    StartHangFireJobs(app, serviceProvider)
}

Of course, everything is store in memory and it is lost once the application pool is recycled, but it is a quick way to see that everything works as expected with minimal configuration.

To switch to SQL Server database persistence, you should install Hangfire.SqlServer package and simply configure it instead of the memory storage:

services.AddHangfire(opt => opt.UseSqlServerStorage(Configuration.GetConnectionString("Default")));
Alexei - check Codidact
  • 22,016
  • 16
  • 145
  • 164
0

I had to start HangFire in main function. This is how I solved it:

public static void Main(string[] args)
    {
        var host = CreateWebHostBuilder(args).Build();
        using (var serviceScope = host.Services.CreateScope())
        {
            var services = serviceScope.ServiceProvider;

            try
            {
                var liveDataHelper = services.GetRequiredService<ILiveDataHelper>();
                var justInitHangfire = services.GetRequiredService<IBackgroundJobClient>();
                //This was causing an exception (HangFire is not initialized)
                RecurringJob.AddOrUpdate(() => liveDataHelper.RePopulateAllConfigDataAsync(), Cron.Daily());
                // Use the context here
            }
            catch (Exception ex)
            {
                var logger = services.GetRequiredService<ILogger<Program>>();
                logger.LogError(ex, "Can't start " + nameof(LiveDataHelper));
            }
        }
        host.Run();
    }

    public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
        WebHost.CreateDefaultBuilder(args)
            .UseStartup<Startup>();
}
Obay Abd-Algader
  • 1,079
  • 12
  • 25
0

Actually there is an easy way for dependency injection based job registration. You just need to use the following code in your Startup:

public class Startup {
    public void Configure(IApplicationBuilder app)
    {
        var factory = app.ApplicationServices
            .GetService<IServiceScopeFactory>();

        GlobalConfiguration.Configuration.UseActivator(
            new Hangfire.AspNetCore.AspNetCoreJobActivator(factory));
    }
}

However i personally wanted a job self registration including on demand jobs (recurring jobs which are never executed, except by manual trigger on hangfire dashboard), which was a little more complex then just that. I was (for example) facing issues with the job service activation, which is why i decided to share most of my implementation code.

//I wanted an interface to declare my jobs, including the job Id.
public interface IBackgroundJob { 
    string Id { get; set; } 
    void Invoke(); 
}

//I wanted to retrieve the jobs by id. Heres my extension method for that:
public static IBackgroundJob GetJob(
    this IServiceProvider provider,
    string jobId) => provider
        .GetServices<IBackgroundJob>()
        .SingleOrDefault(j => j.Id == jobId);

//Now i needed an invoker for these jobs.
//The invoker is basically an example of a dependency injected hangfire job.
internal class JobInvoker { 
    public JobInvoker(IServiceScopeFactory factory) { 
        Factory = factory; 
    }
    public IServiceScopeFactory Factory { get;  }
    public void Invoke(string jobId)
    {
        //hangfire jobs should always be executed within their own scope. 
        //The default AspNetCoreJobActivator should technically already do that.
        //Lets just say i have trust issues.
        using (var scope = Factory.CreateScope())
        {
            scope.ServiceProvider
              .GetJob(jobId)?
              .Invoke();
        }
}

//Now i needed to tell hangfire to use these jobs. 
//Reminder: The serviceProvider is in IApplicationBuilder.ApplicationServices
public static void RegisterJobs(IServiceProvider serviceProvider) { 
    var factory = serviceProvider.GetService();
    GlobalConfiguration.Configuration.UseActivator(new Hangfire.AspNetCore.AspNetCoreJobActivator(factory));
    var manager = serviceProvider.GetService<IRecurringJobManager>();
    var config = serviceProvider.GetService<IConfiguration>();
    var jobs = serviceProvider.GetServices<IBackgroundJob>();
    foreach (var job in jobs) {
        var jobConfig = config.GetJobConfig(job.Id);
        var schedule = jobConfig?.Schedule; //this is a cron expression
        if (String.IsNullOrWhiteSpace(schedule))
            schedule = Cron.Never(); //this is an on demand job only!

        manager.AddOrUpdate(
            recurringJobId: job.Id,
            job: GetJob(job.Id),
            cronExpression: schedule);
}
//and last but not least...  
//My Method for creating the hangfire job with injected job id 
private static Job GetJob(string jobId)
{
    var type = typeof(JobInvoker);
    var method = type.GetMethod("Invoke");
    return new Job(
        type: type,
        method: method,
        args: jobId);
}

Using the above code i was able to create hangfire job services with full dependency injection support. Hope it helps someone.

Canabale
  • 157
  • 1
  • 7
-1

Use the below code for Hangfire configuration

using eForms.Core;
using Hangfire;
using Hangfire.SqlServer;
using System;
using System.ComponentModel;
using System.Web.Hosting;

namespace eForms.AdminPanel.Jobs
{
    public class JobManager : IJobManager, IRegisteredObject
    {
        public static readonly JobManager Instance = new JobManager();
        //private static readonly TimeSpan ZeroTimespan = new TimeSpan(0, 0, 10);
        private static readonly object _lockObject = new Object();
        private bool _started;
        private BackgroundJobServer _backgroundJobServer;
        private JobManager()
        {
        }
        public int Schedule(JobInfo whatToDo)
        {
            int result = 0;
            if (!whatToDo.IsRecurring)
            {
                if (whatToDo.Delay == TimeSpan.Zero)
                    int.TryParse(BackgroundJob.Enqueue(() => Run(whatToDo.JobId, whatToDo.JobType.AssemblyQualifiedName)), out result);
                else
                    int.TryParse(BackgroundJob.Schedule(() => Run(whatToDo.JobId, whatToDo.JobType.AssemblyQualifiedName), whatToDo.Delay), out result);
            }
            else
            {
                RecurringJob.AddOrUpdate(whatToDo.JobType.Name, () => RunRecurring(whatToDo.JobType.AssemblyQualifiedName), Cron.MinuteInterval(whatToDo.Delay.TotalMinutes.AsInt()));
            }

            return result;
        }

        [DisplayName("Id: {0}, Type: {1}")]
        [HangFireYearlyExpirationTime]
        public static void Run(int jobId, string jobType)
        {
            try
            {
                Type runnerType;
                if (!jobType.ToType(out runnerType)) throw new Exception("Provided job has undefined type");
                var runner = runnerType.CreateInstance<JobRunner>();
                runner.Run(jobId);
            }
            catch (Exception ex)
            {
                throw new JobException($"Error while executing Job Id: {jobId}, Type: {jobType}", ex);
            }
        }

        [DisplayName("{0}")]
        [HangFireMinutelyExpirationTime]
        public static void RunRecurring(string jobType)
        {
            try
            {
                Type runnerType;
                if (!jobType.ToType(out runnerType)) throw new Exception("Provided job has undefined type");
                var runner = runnerType.CreateInstance<JobRunner>();
                runner.Run(0);
            }
            catch (Exception ex)
            {
                throw new JobException($"Error while executing Recurring Type: {jobType}", ex);
            }
        }

        public void Start()
        {
            lock (_lockObject)
            {
                if (_started) return;
                if (!AppConfigSettings.EnableHangFire) return;
                _started = true;
                HostingEnvironment.RegisterObject(this);
                GlobalConfiguration.Configuration
                    .UseSqlServerStorage("SqlDbConnection", new SqlServerStorageOptions { PrepareSchemaIfNecessary = false })
                   //.UseFilter(new HangFireLogFailureAttribute())
                   .UseLog4NetLogProvider();
                //Add infinity Expiration job filter
                //GlobalJobFilters.Filters.Add(new HangFireProlongExpirationTimeAttribute());

                //Hangfire comes with a retry policy that is automatically set to 10 retry and backs off over several mins
                //We in the following remove this attribute and add our own custom one which adds significant backoff time
                //custom logic to determine how much to back off and what to to in the case of fails

                // The trick here is we can't just remove the filter as you'd expect using remove
                // we first have to find it then save the Instance then remove it 
                try
                {
                    object automaticRetryAttribute = null;
                    //Search hangfire automatic retry
                    foreach (var filter in GlobalJobFilters.Filters)
                    {
                        if (filter.Instance is Hangfire.AutomaticRetryAttribute)
                        {
                            // found it
                            automaticRetryAttribute = filter.Instance;
                            System.Diagnostics.Trace.TraceError("Found hangfire automatic retry");
                        }
                    }
                    //Remove default hangefire automaticRetryAttribute
                    if (automaticRetryAttribute != null)
                        GlobalJobFilters.Filters.Remove(automaticRetryAttribute);
                    //Add custom retry job filter
                    GlobalJobFilters.Filters.Add(new HangFireCustomAutoRetryJobFilterAttribute());
                }
                catch (Exception) { }
                _backgroundJobServer = new BackgroundJobServer(new BackgroundJobServerOptions
                {
                    HeartbeatInterval = new System.TimeSpan(0, 1, 0),
                    ServerCheckInterval = new System.TimeSpan(0, 1, 0),
                    SchedulePollingInterval = new System.TimeSpan(0, 1, 0)
                });
            }
        }
        public void Stop()
        {
            lock (_lockObject)
            {
                if (_backgroundJobServer != null)
                {
                    _backgroundJobServer.Dispose();
                }
                HostingEnvironment.UnregisterObject(this);
            }
        }
        void IRegisteredObject.Stop(bool immediate)
        {
            Stop();
        }
    }
}

Admin Job Manager

    public class Global : System.Web.HttpApplication
    {
        void Application_Start(object sender, EventArgs e)
        {
            if (Core.AppConfigSettings.EnableHangFire)
            {
                JobManager.Instance.Start();

                new SchedulePendingSmsNotifications().Schedule(new Core.JobInfo() { JobId = 0, JobType = typeof(SchedulePendingSmsNotifications), Delay = TimeSpan.FromMinutes(1), IsRecurring = true });

            }
        }
        protected void Application_End(object sender, EventArgs e)
        {
            if (Core.AppConfigSettings.EnableHangFire)
            {
                JobManager.Instance.Stop();
            }
        }
    }
Jayakumar Thangavel
  • 1,884
  • 1
  • 22
  • 29