Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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.

like image 350
parliament Avatar asked Jan 15 '15 10:01

parliament


4 Answers

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));
like image 61
Dresel Avatar answered Oct 12 '22 21:10

Dresel


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

like image 40
Milen Kovachev Avatar answered Oct 12 '22 21:10

Milen Kovachev


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);
        }
    }
}
like image 4
Lauri Harpf Avatar answered Oct 12 '22 20:10

Lauri Harpf


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);
like image 2
Colin S Avatar answered Oct 12 '22 20:10

Colin S