web-dev-qa-db-fra.com

Durée de vie de la durée d'injection de dépendance Hangfire

Je réécris toute cette question parce que je comprends la cause, mais j'ai encore besoin d'une solution:

J'ai un travail récurrent dans Hangfire qui s'exécute toutes les minutes et vérifie la base de données, met éventuellement à jour des éléments, puis se ferme.

J'injecte mon dbcontext dans la classe contenant la méthode de travail. J'inscris ce dbcontext pour être injecté en utilisant ce qui suit

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

Cependant, il semble que Hangfire ne crée pas une étendue de durée de vie distincte à chaque exécution du travail, car le constructeur n'est appelé qu'une seule fois, bien que la méthode de travail soit appelée toutes les minutes.

Cela cause des problèmes pour moi. Si l'utilisateur met à jour des valeurs dans la base de données (dbcontext est injecté ailleurs et utilisé pour mettre à jour des valeurs), le contexte toujours utilisé Hangfire commence à renvoyer des valeurs obsolètes qui ont déjà été modifiées.

18
parliament

Hangfire utilise actuellement une instance partagée de JobActivator pour chaque travailleur, qui utilise la méthode suivante pour résoudre une dépendance:

    public override object ActivateJob(Type jobType)

Il est prévu d'ajouter un JobActivationContext à cette méthode pour Milestone 2.0.0 .

Pour le moment, il n'y a aucun moyen de dire pour quel travail une dépendance est résolue. La seule façon pour moi de résoudre ce problème serait d'utiliser le fait que les travaux sont exécutés en série sur différents threads (je ne connais pas AutoFac, j'utilise donc Unity comme exemple).

Vous pouvez créer une JobActivator pouvant stocker des étendues distinctes par 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;
    }
}

Utilisez une implémentation JobFilter avec IServerFilter pour définir cette étendue pour chaque travail (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();
    }
}

Et enfin, configurez votre DI:

UnityJobActivator unityJobActivator = new UnityJobActivator(new UnityContainer());
JobActivator.Current = unityJobActivator;

GlobalJobFilters.Filters.Add(new ChildContainerPerJobFilterAttribute(unityJobActivator));
18
Dresel

Nous avons créé une nouvelle demande d'extraction dans Hangfire.Autofac avec le contournement décrit par Dresel. Espérons que cela soit fusionné dans la branche principale:

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

5
Milen Kovachev

Edit: Avec Autofac, .NET 4.5 et Hangfire> = 1.5.0, utilisez le package Hangfire.Autofac nuget ( github ).

En travaillant avec .NET 4.0 (Autofac 3.5.2 et Hangfire 1.1.1), nous avons mis en place la solution Dresel avec Autofac. La seule différence est dans 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);
        }
    }
}
3
Lauri Harpf

Pour contourner ce problème, j'ai créé une classe JobContext à usage unique qui contient un ILifetimeScope qui sera supprimé lorsque Hangfire aura terminé le travail. Le vrai travail est appelé par réflexion. 

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

Il existe un JobActivator qui inspectera l'action et créera le LifetimeScope si nécessaire.

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

Pour aider à la création de travaux, sans utiliser de paramètres de chaîne, il existe une autre classe avec certaines 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));
    }
}

Donc, pour mettre un travail en file d'attente, par exemple avec la signature suivante:

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

Le code pour créer le travail ressemble à

    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);
2
Colin S

Une solution est prise en charge immédiatement depuis hangfire.autofac 2.2.0.

Dans votre situation, où votre dépendance est enregistrée par étendue de durée de vie, vous devriez pouvoir utiliser étendues non marquées lors de la configuration de hangfire.autofac. Du lien:

GlobalConfiguration.Configuration.UseAutofacActivator(builder.Build(), false);
0
Marc L.