Hangfire dependency injection lifetime scope

18,264

Solution 1

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));

Solution 2

We have created a new pull request in the Hangfire.Autofac with the work around described by Dresel. Hopefully it gets merged in the main branch:

https://github.com/HangfireIO/Hangfire.Autofac/pull/4

Solution 3

Edit: With Autofac, .NET 4.5 and Hangfire >= 1.5.0, use the Hangfire.Autofac nuget package (github).

Working with .NET 4.0 (Autofac 3.5.2 and Hangfire 1.1.1), we set up Dresel's solution with Autofac. Only difference is in the JobActivator:

using System;
using Autofac;
using Hangfire;

namespace MyApp.DependencyInjection
{
    public class ContainerJobActivator : JobActivator
    {
        [ThreadStatic]
        private static ILifetimeScope _jobScope;
        private readonly IContainer _container;

        public ContainerJobActivator(IContainer container)
        {
            _container = container;
        }

        public void BeginJobScope()
        {
            _jobScope = _container.BeginLifetimeScope();
        }

        public void DisposeJobScope()
        {
            _jobScope.Dispose();
            _jobScope = null;
        }

        public override object ActivateJob(Type type)
        {
            return _jobScope.Resolve(type);
        }
    }
}

Solution 4

To work around this problem, I've created a disposable JobContext class that has a ILifetimeScope that will be disposed when Hangfire completes the job. The real job is invoked by reflection.

public class JobContext<T> : IDisposable
{
    public ILifetimeScope Scope { get; set; }

    public void Execute(string methodName, params object[] args)
    {
        var instance = Scope.Resolve<T>();
        var methodInfo = typeof(T).GetMethod(methodName);
        ConvertParameters(methodInfo, args);
        methodInfo.Invoke(instance, args);
    }

    private void ConvertParameters(MethodInfo targetMethod, object[] args)
    {
        var methodParams = targetMethod.GetParameters();

        for (int i = 0; i < methodParams.Length && i < args.Length; i++)
        {
            if (args[i] == null) continue;
            if (!methodParams[i].ParameterType.IsInstanceOfType(args[i]))
            {
                // try convert 
                args[i] = args[i].ConvertType(methodParams[i].ParameterType);
            }
        }
    }

    void IDisposable.Dispose()
    {
        if (Scope != null)
            Scope.Dispose();
        Scope = null;
    }
}

There is a JobActivator that will inspect the action and create the LifetimeScope if necessary.

public class ContainerJobActivator : JobActivator
{
    private readonly IContainer _container;
    private static readonly string JobContextGenericTypeName = typeof(JobContext<>).ToString();

    public ContainerJobActivator(IContainer container)
    {
        _container = container;
    }

    public override object ActivateJob(Type type)
    {
        if (type.IsGenericType && type.GetGenericTypeDefinition().ToString() == JobContextGenericTypeName)
        {
            var scope = _container.BeginLifetimeScope();
            var context = Activator.CreateInstance(type);
            var propertyInfo = type.GetProperty("Scope");
            propertyInfo.SetValue(context, scope);
            return context;
        }
        return _container.Resolve(type);
    }
}

To assist with creating jobs, without using string parameters there is another class with some extensions.

public static class JobHelper
{
    public static object ConvertType(this object value, Type destinationType)
    {
        var sourceType = value.GetType();

        TypeConverter converter = TypeDescriptor.GetConverter(sourceType);
        if (converter.CanConvertTo(destinationType))
        {
            return converter.ConvertTo(value, destinationType);
        }
        converter = TypeDescriptor.GetConverter(destinationType);
        if (converter.CanConvertFrom(sourceType))
        {
            return converter.ConvertFrom(value);
        }
        throw new Exception(string.Format("Cant convert value '{0}' or type {1} to destination type {2}", value, sourceType.Name, destinationType.Name));
    }

    public static Job CreateJob<T>(Expression<Action<T>> expression, params object[] args)
    {
        MethodCallExpression outermostExpression = expression.Body as MethodCallExpression;
        var methodName = outermostExpression.Method.Name;
        return Job.FromExpression<JobContext<T>>(ctx => ctx.Execute(methodName, args));
    }
}

So to queue up a job, e.g. with the following signature:

public class ResidentUploadService
{
    public void Load(string fileName)
    {
       //...
    }

The code to create the job looks like

    var localFileName = "Somefile.txt";
    var job = ContainerJobActivator
                 .CreateJob<ResidentUploadService>(service => service.Load(localFileName), localFileName);
    var state = new EnqueuedState("queuename");
    var client = new BackgroundJobClient();
    client.Create(job,state);

Solution 5

A solution is supported out-of-the-box since hangfire.autofac 2.2.0.

In your situation, where your dependency is being registered per-lifetime-scope, you should be able to use non-tagged scopes when setting up hangfire.autofac. From the link:

GlobalConfiguration.Configuration.UseAutofacActivator(builder.Build(), false);
Share:
18,264

Related videos on Youtube

parliament
Author by

parliament

Serial Entrepreneur Full Stack Developer (C#/AngularJS/D3.js) Contact me if you want to work on cool shit with me. Currently in the bitcoin space, starting a cryptocurrency exchange. Also have a body of work on automated trading systems. [email protected]

Updated on October 23, 2022

Comments

  • parliament
    parliament over 1 year

    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.

    • Sergey Kolodiy
      Sergey Kolodiy over 9 years
      Are you reading enum value from the database view or directly from the table? Perhaps the problem lies in the EF materialization and not in the enum itself.
    • parliament
      parliament over 9 years
      Sergey, I'm sure you are right. I added more details to the question
    • Sergey Kolodiy
      Sergey Kolodiy over 9 years
      If you believe that the problem is EF materialization, have a look at this question, maybe it will help.
    • parliament
      parliament over 9 years
      Sergey, thanks. I found the cause and it's not related to that. It has to do with depedency injection lifetime scope in hangfire. I rewrote the question entirely
  • moribvndvs
    moribvndvs over 9 years
    The problem with this approach is it is not thread safe. If you have concurrent jobs running, it's possible the wrong scope could be used for the wrong job. What we need is for JobActivator to provide a method to create a JobActivationScope (which DI implementations would have to provide a trivial implementation, along with the JobActivator implementation). Then we change Job.Perform to create this scope from the activator in a using statement, and pass that in as the parameter for Activate, rather than JobActivator.
  • Dresel
    Dresel over 9 years
    "If you have concurrent jobs running, it's possible the wrong scope could be used for the wrong job." - But aren't concurrent jobs running on different workers (threads)?
  • moribvndvs
    moribvndvs over 9 years
    I apologize. I overlooked the [ThreadStatic] attribution. Provided we make some guarantees about the internals of the thread context at various stages of execution, then yeah, this could work as a temporary measure until 2.0.0. I would still want a model more like what I described, in terms of the permanent solution.
  • Dresel
    Dresel over 9 years
    Yep the current design of JobActivator is not optimal, I hope this will improve for future versions. The workaround at least makes it possible for now without too much effort. It also makes it possible to pass context information to jobs (e.g. JobID) if you register it within the filter.
  • Marc L.
    Marc L. about 7 years
    Per-job activator scopes have been supported since 1.5.0-beta1. Combined with non-tagged scopes in the Autofac integration (since 2.2.0) this moots the original question.