Hangfire .net core console Dependency Injection

Viewed 3489

I have a project with this structure

  • AdventureWorks.Web <- .NET Core web project (Services, Viewmodels, DataContext, Controller Views, etc )
  • AdventureWorks.Console <- .NET Core console app (hangfire config server)
  • AdventureWorks.Model <- just pure POCO model classes

I am trying to run the Hangfire server in a separate process (AdventureWorks.Console) [https://docs.hangfire.io/en/latest/background-processing/processing-jobs-in-console-app.html]

Processing jobs in a console application.

I added a reference to AdventureWorks.Web to the AdventureWorks.Console - this way the Hangfire server would execute the code

static void Main(string[] args)
{
    GlobalConfiguration.Configuration.UseSqlServerStorage("Connection");

    using (var server = new BackgroundJobServer())
    {
        Console.WriteLine("Hangfire Server started. Press any key to exit...");
        Console.ReadKey();
    }
}

In the web project I'm registering my services in the Startup class

services.AddTransient<IDepartmentServices, DepartmentServices>();

The constructor has a parameter witch is auto injected

private readonly AdventureWorksDataContext db;

    public DepartmentServices(AdventureWorksDataContext _db)
    {
        db = _db;
    }

In the controller the services are injected

private readonly IDepartmentServices Service;

    public DepartmentsController(IDepartmentServices service)
    {
        Service = service;
    }

In one method in the controller when the user click the button "create report" the method enqueue a job to create a report

BackgroundJob.Enqueue<**IDepartmentService**>((serv) => serv.CrearReport(data));

So far so good.

I can see in the dashboard the job created but it fails to execute

System.MissingMethodException Cannot create an instance of an interface.

which is obvious so my first question is

So to continue I change the interface with the class

BackgroundJob.Enqueue<DepartmentService>((serv) => serv.CrearReport(data));

so far so good the job is created. But it fails with the error

System.MissingMethodException
No parameterless constructor defined for this object

which is obvious so my second question is

  • how do I pass the parameter datacontext to hangfire server? My best guess is I can't because hangfire server is using an Activator which wants an empty constructor

So to continue I followed the instructions here [https://docs.hangfire.io/en/latest/background-methods/passing-dependencies.html]

I created an empty constructor

public EmployeeService()
        :this(new AdventureWorksDataContext())
    {

    }

and i need add an empty constructor to the datacontext

public AdventureWorksDataContext()
    {

    }

but now i need to override the "OnConfigure" method

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        base.OnConfiguring(optionsBuilder);
        optionsBuilder.UseSqlServer("????");<--connectionstring???
    }

so my question is how do i get the connectionstring from the appsettings.json my best guest i cant ignoring this problem i add the direct connection

optionsBuilder.UseSqlServer("Server=.;Database=AdventureWorks2017;Trusted_Connection=True;");

this way the job is completed correct

but this solution i think is bad maybe the project structure is bad or how the dependency injection works with hangfire if is running in a separete process, how IoC works en hangfire server console

some one point me a better solution how structure the project

sorry for my English i dont't speak

1 Answers

Try this: https://docs.hangfire.io/en/latest/getting-started/aspnet-core-applications.html. In the past I was using Hangfire.AutoFac nuget using a custom JobActivator suioted for that given DI framework.

A lot has changed, check out: https://docs.hangfire.io/en/latest/background-methods/using-ioc-containers.html and (this should work) https://docs.hangfire.io/en/latest/getting-started/aspnet-core-applications.html

--- what I had:

So actually you need to add some hangfire related object to the container. I used AutoFac (with .NET MVC)...

Hope this helps you to move forward.

so the dependency injection (service) setup via AutoFac (you could use pure MVC DI):

                //
                // Set up HANGFIRE
                var connectionStringName = config["Hangfire:ConnectionStringName"];
                var connectionSection = config.GetSection($"Data:{connectionStringName}");
                var connectionString = config[$"Data:{connectionStringName}:ConnectionString"];

                var queuePollIntervanInMs = int.Parse(config["Hangfire:QueuePollIntervalInMilliseconds"]);

                GlobalConfiguration.Configuration.UseLogProvider(new HangfireLogProvider());
                GlobalConfiguration.Configuration.UseSqlServerStorage(
                    connectionString,
                    new SqlServerStorageOptions() { QueuePollInterval = TimeSpan.FromMilliseconds(queuePollIntervanInMs) }
                );

                var serverEnabled = bool.Parse(config["Hangfire:ServerEnabled"]);
                if (serverEnabled)
                {
                    GlobalJobFilters.Filters.Add(new AutomaticRetryAttribute());
                    GlobalJobFilters.Filters.Add(new HangfirePermanentFailureNofitifactionAttribute());

                    // Setting up Hangfire background job processing (use SIGLETON!, keep reference for graceful shutdown - btw. app.UseHangfireServer() works in web-context and does register to graceful-shutdown)
                    GlobalConfiguration.Configuration.UseAutofacActivator(_container);

                    _hangfireServer = new BackgroundJobServer(
                        new BackgroundJobServerOptions()
                        {
                            Activator = JobActivator.Current
                        },
                        JobStorage.Current
                    );
                }

startup.cs (configure services - before adding the dependencies above) - ignore some comments, I don't find them useful:


                /*
                // Hangfire - since we plan to use it both in web and threaded (unit test) env., initialization is done with hangfire globals on a single place...
                services.AddHangfire(h =>
                {
                    // h.UseSqlServerStorage("<connection string or its name>");
                    // h.UseActivator(...)
                    // h.UseFilter(...)
                });
                */

                // NOTE: the following 2 objects needs to be registered for dashboard to work...https://github.com/HangfireIO/Hangfire/blob/129707d66fde24dc6379fb9d6b15fa0b8ca48605/src/Hangfire.AspNetCore/HangfireServiceCollectionExtensions.cs
                services.TryAddSingleton(_ => GlobalConfiguration.Configuration);
                services.TryAddSingleton(_ => DashboardRoutes.Routes);

and the startup.cs (configure iapp builder:

                var dashboardEnabled = bool.Parse(Configuration["Hangfire:DashboardEnabled"]);
                if (dashboardEnabled)
                {
                    // NOTE: Place a call to the UseHangfireDashboard method after other authentication methods in your OWIN Startup class. Otherwise authentication may not work for you.
                    var hangfireDashboarOptions = new DashboardOptions
                    {
                        Authorization = new IDashboardAuthorizationFilter[]
                        {
                            new LocalRequestsOnlyAuthorizationFilter()
                        },
                        StatsPollingInterval = int.Parse(Configuration["Hangfire:DashboardRefreshIntervalInMilliseconds"])
                    };

                    // https://github.com/HangfireIO/Hangfire/blob/129707d66fde24dc6379fb9d6b15fa0b8ca48605/src/Hangfire.AspNetCore/HangfireApplicationBuilderExtensions.cs
                    app.UseHangfireDashboard("/dashboard", hangfireDashboarOptions, JobStorage.Current);
                }

and a sample on invocation:

    /// <summary>
    /// Main event handler, can inherit from multiple IDomainEventHandler<TEvent>.
    /// NOTE: depending how event registration and dispatching is done, for each handler a separate instance might be served from the DI framework!
    /// </summary>
    public class UserEventHandler :
        IDomainEventHandler<UserRegisteredEvent>
    {
        private ILog _logger;
        private MyDbContext _myDbContext;

        public UserEventHandler(
            ILog logger,
            MyDbContext myDbContext,
            )
        {
            _logger = logger;
            _myDbContext = myDbContext;
        }

        public void Handle(UserRegisteredEvent e)
        {
            // Send Welcome Email
            if (!string.IsNullOrEmpty(e.User.Email))
                BackgroundJob.Enqueue<UserMessageHandler>(i => i.SendWelcomeEmail(e.User.Email, e.User.UserName, e.UserPassword, e.PRCo));
            else
                _logger.Warn(new { Message = $"Can't send notification email, user: '{e.User.GetFullName()}' with SSN: {e.User.Ssn}' has no email assigned!" });

            // Send Welcome SMS
            if (!string.IsNullOrEmpty(e.CellPhone))
                BackgroundJob.Enqueue<UserMessageHandler>(i => i.SendWelcomeSMS(e.CellPhone, e.User.UserName, e.UserPassword));
            else
                _logger.Warn(new { Message = $"Can't send notification message, user: '{e.User.GetFullName()}' with SSN: {e.User.Ssn}' has no cell phone assigned!" });
        }

The trick is as you see UserMessageHandler is not injected into the handler class itself, it's injected into the context (as a singleton - but not necessarily as a singleton) and Hangfire is picking up the object from the context!

Related