web-dev-qa-db-ja.com

Hangfire依存性注入のライフタイムスコープ

原因を理解したのでこの質問全体を書き直していますが、それでも解決策が必要です。

Hangfireには毎分実行される定期的なジョブがあり、データベースをチェックし、場合によっては更新して終了します。

ジョブメソッドを含むクラスにdbcontextを挿入します。このdbcontextを登録して、以下を使用して注入されます

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

ただし、ジョブメソッドは1分ごとに呼び出されますが、コンストラクターは1回しか呼び出されないため、Hangfireはジョブが実行されるたびに個別のライフタイムスコープを作成しないようです。

これは私に問題を引き起こします。ユーザーがデータベース内のいくつかの値を更新すると(dbcontextは別の場所に挿入され、値の更新に使用されます)、まだ使用されているコンテキストHangfireは、すでに変更されている古い値を返し始めます。

19
parliament

Hangfireは現在、すべてのワーカーに対してJobActivatorの共有インスタンスを使用しています。ワーカーは、依存関係を解決するために次のメソッドを使用しています。

    public override object ActivateJob(Type jobType)

Milestone 2.0. の場合、このメソッドにJobActivationContextを追加する予定です。

現時点では、依存関係が解決されるジョブを特定する方法はありません。この問題を回避するために私が考えることができる唯一の方法は、ジョブが異なるスレッドでシリアルで実行されているという事実を使用することです(私はAutoFacを知らないので、例としてUnityを使用しています)。

スレッドごとに個別のスコープを格納できるJobActivatorを作成できます。

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

JobFilterIServerFilter実装を使用して、このスコープをすべてのジョブ(スレッド)に設定します。

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

最後にDIをセットアップします。

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

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

Hangfire.Autofacに新しいプルリクエストを作成し、Dreselによって説明されている回避策を追加しました。うまくいけば、メインブランチにマージされます:

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

5
Milen Kovachev

Edit:Autofac、.NET 4.5およびHangfire> = 1.5.0の場合、Hangfire.Autofacを使用 nugetパッケージ (- github )。

.NET 4.0(Autofac 3.5.2およびHangfire 1.1.1)を使用して、AutofacでDreselのソリューションをセットアップしました。唯一の違いは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

この問題を回避するために、Hangfireがジョブを完了すると破棄されるILifetimeScopeを持つ使い捨てのJobContextクラスを作成しました。実際のジョブはリフレクションによって呼び出されます。

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

アクションを検査し、必要に応じてLifetimeScopeを作成するJobActivatorがあります。

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

文字列パラメータを使用せずにジョブの作成を支援するために、いくつかの拡張機能を持つ別のクラスがあります。

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

たとえば、ジョブをキューに入れるには次のシグネチャで:

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

ジョブを作成するコードは次のようになります

    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

hangfire.autofac 2.2.0以降、ソリューションはそのままサポートされます。

依存関係がライフタイムスコープごとに登録されている状況では、hangfire.autofacを設定するときに non-tagged scopes を使用できるはずです。リンクから:

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