Resolving Hangfire dependencies/HttpContext in .NET Core Startup

6.4k views Asked by At

I've installed and configured Hangfire in my .NET Core web application's Startup class as follows (with a lot of the non-Hangfire code removed):

public class Startup
{
    public void Configuration(IAppBuilder app)
    {
        app.UseHangfireServer();
        //app.UseHangfireDashboard();
        //RecurringJob.AddOrUpdate(() => DailyJob(), Cron.Daily);
    }

    public IServiceProvider ConfigureServices(IServiceCollection services)
    {
        services.AddOptions();
        services.Configure<AppSettings>(Configuration);
        services.AddSingleton<IConfiguration>(Configuration);
        services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
        services.AddScoped<IPrincipal>((sp) => sp.GetService<IHttpContextAccessor>().HttpContext.User);
        services.AddScoped<IScheduledTaskService, ScheduledTaskService>();

        services.AddHangfire(x => x.UseSqlServerStorage(connectionString));    
        this.ApplicationContainer = getWebAppContainer(services);
        return new AutofacServiceProvider(this.ApplicationContainer);
    }
}

public interface IScheduledTaskService
{
    void OverduePlasmidOrdersTask();
}

public class ScheduledTaskService : IScheduledTaskService
{
    public void DailyJob()
    {
        var container = getJobContainer();
        using (var scope = container.BeginLifetimeScope())
        {
            IScheduledTaskManager scheduledTaskManager = scope.Resolve<IScheduledTaskManager>();
            scheduledTaskManager.ProcessDailyJob();
        }
    }

    private IContainer getJobContainer()
    {
        var builder = new ContainerBuilder();
        builder.RegisterModule(new BusinessBindingsModule());
        builder.RegisterModule(new DataAccessBindingsModule());
        return builder.Build();
    }
}

As you can see, I'm using Autofac for DI. I've set things up to inject a new container each time the Hangfire job executes.

Currently, I have UseHangfireDashboard() as well as the call to add my recurring job commented out and I'm receiving the following error on the line referencing IPrincipal:

System.NullReferenceException: 'Object reference not set to an instance of an object.'

I understand that Hangfire does not have an HttpContext. I'm not really sure why it's even firing that line of code for the Hangfire thread. I'm ultimately going to need to resolve a service account for my IPrincipal dependency.

How can I address my issue with Hangfire and HttpContext?

3

There are 3 answers

0
Kevin R. On

I'm not sure of the type for jobmanager off the top of my head, but you can resolve the dependency from the container using a scope. You'll want to resolve from the scope in a using statement to prevent memory leaks. See the Autofac Docs

// not sure what type "jobManager" is
TYPE jobManager;

using(var scope = ApplicationContainer.BeginLifetimeScope())
{
    jobManager = scope.Resolve<TYPE>();
}

RecurringJob.AddOrUpdate( ... );
1
ta.speot.is On

Why is Hangfire trying to resolve the .NET Core Startup class?

Hangfire doesn't store lambda expressions in the database, it stores the type and method being called. Then when the scheduled task is due to run, it resolves the type from the container and calls the method.

In your case, the method is on Startup.

You can register Startup with Autofac if you want, but it's probably easiest to have a scheduled task service:

AddOrUpdate<IScheduledTaskService>(x => x.DailyTask(), Cron.Daily);
4
Nkosi On

The main problem I'm having now is when I add UseHangfireServer, I then need to resolve HttpContext too

Found here Using IoC containers

HttpContext is not available

Request information is not available during the instantiation of a target type. If you register your dependencies in a request scope (InstancePerHttpRequest in Autofac, InRequestScope in Ninject and so on), an exception will be thrown during the job activation process.

So, the entire dependency graph should be available. Either register additional services without using the request scope, or use separate instance of container if your IoC container does not support dependency registrations for multiple scopes.

resolving scoped dependencies in .net core would require a request which is not available during startup when registering and activating jobs. Therefore make sure that your service required for activation during startup are not registered using scoped lifetimes.

 services.AddTransient<IScheduledTaskManager, ScheduledTaskManageImplementation>();

All that is left now is to configure the application to use that service with the recurring job,

public class Startup {    
    public IContainer ApplicationContainer { get; private set; }

    public Startup(IHostingEnvironment env) {
        var builder = new ConfigurationBuilder()
            .SetBasePath(env.ContentRootPath)
            .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
            .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)
            .AddEnvironmentVariables();
        Configuration = builder.Build();
    }

    public void Configuration(IApplicationBuilder app) {
        // app.AddLogger...

        //add hangfire features
        app.UseHangfireServer();
        app.UseHangfireDashboard();

        //Add the recurring job
        RecurringJob.AddOrUpdate<IScheduledTaskManager>(task => task.ProcessDailyJob(), Cron.Daily);

        //app.UseMvc...
        //...other code
    }

    public IServiceProvider ConfigureServices(IServiceCollection services) {    
        // Adding custom services
        services.AddTransient<IScheduledTaskManager, ScheduledTaskManageImplementation>();
        //add other dependencies...

        // add hangfire services
        services.AddHangfire(x => x.UseSqlServerStorage("<connection string>"));

        //configure Autofac
        this.ApplicationContainer = getWebAppContainer(services);
        //get service provider    
        return new AutofacServiceProvider(this.ApplicationContainer);
    }

    IContainer getWebAppContainer(IServiceCollection service) {
        var builder = new ContainerBuilder();        
        builder.RegisterModule(new BusinessBindingsModule());
        builder.RegisterModule(new DataAccessBindingsModule());
        builder.Populate(services);
        return builder.Build();
    }        


    //...other code
}

References

Hangfire 1.6.0

Integrate HangFire With ASP.NET Core

Using IoC containers