Search code examples
c#autofachangfire

Hangfire dependency injection lifetime scope


I'm rewriting this entire question because I realize the cause, but still need a solution:

I have a recurring job in Hangfire that runs every minute and check the database, possibly updates some stuff, then exits.

I inject my dbcontext into the class containing the job method. I register this dbcontext to get injected using the following

builder.RegisterType<ApplicationDbContext>().As<ApplicationDbContext>().InstancePerLifetimeScope();

However, it seems that Hangfire does not create a seperate lifetime scope every time the job runs, because the constructor only gets called once, although the job method get's called every minute.

This causes issues for me. If the user updates some values in the database (dbcontext gets injected somewhere else, and used to update values), the context still being used Hangfire starts returning out-dated values that have already been changed.


Solution

  • Hangfire currently uses a shared Instance of JobActivator for every Worker, which are using the following method for resolving a dependency:

        public override object ActivateJob(Type jobType)
    

    It is planned to add a JobActivationContext to this method for Milestone 2.0.0.

    For now, there is no way to say for which job a dependency gets resolved. The only way I can think of to workaround this issue would be to use the fact that jobs are running serial on different threads (I don't know AutoFac so I use Unity as an example).

    You could create a JobActivator that can store separate scopes per thread:

    public class UnityJobActivator : JobActivator
    {
        [ThreadStatic]
        private static IUnityContainer childContainer;
    
        public UnityJobActivator(IUnityContainer container)
        {
            // Register dependencies
            container.RegisterType<MyService>(new HierarchicalLifetimeManager());
    
            Container = container;
        }
    
        public IUnityContainer Container { get; set; }
    
        public override object ActivateJob(Type jobType)
        {
            return childContainer.Resolve(jobType);
        }
    
        public void CreateChildContainer()
        {
            childContainer = Container.CreateChildContainer();
        }
    
        public void DisposeChildContainer()
        {
            childContainer.Dispose();
            childContainer = null;
        }
    }
    

    Use a JobFilter with IServerFilter implementation to set this scope for every job (thread):

    public class ChildContainerPerJobFilterAttribute : JobFilterAttribute, IServerFilter
    {
        public ChildContainerPerJobFilterAttribute(UnityJobActivator unityJobActivator)
        {
            UnityJobActivator = unityJobActivator;
        }
    
        public UnityJobActivator UnityJobActivator { get; set; }
    
        public void OnPerformed(PerformedContext filterContext)
        {
            UnityJobActivator.DisposeChildContainer();
        }
    
        public void OnPerforming(PerformingContext filterContext)
        {
            UnityJobActivator.CreateChildContainer();
        }
    }
    

    And finally setup your DI:

    UnityJobActivator unityJobActivator = new UnityJobActivator(new UnityContainer());
    JobActivator.Current = unityJobActivator;
    
    GlobalJobFilters.Filters.Add(new ChildContainerPerJobFilterAttribute(unityJobActivator));