【问题标题】:Hangfire dependency injection lifetime scopeHangfire 依赖注入生命周期范围
【发布时间】:2017-04-28 04:07:10
【问题描述】:

我正在重写整个问题,因为我意识到了原因,但仍然需要解决方案:

我在 Hangfire 中有一个重复性工作,每分钟运行一次并检查数据库,可能会更新一些内容,然后退出。

我将我的 dbcontext 注入到包含作业方法的类中。我注册这个 dbcontext 以使用以下方式注入

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

但是,Hangfire 似乎不会在每次作业运行时创建单独的生命周期范围,因为构造函数只被调用一次,尽管每分钟调用一次作业方法。

这给我带来了问题。如果用户更新数据库中的某些值(dbcontext 被注入到其他地方,并用于更新值),仍在使用的上下文 Hangfire 开始返回已更改的过时值。

【问题讨论】:

  • 您是从数据库视图还是直接从表中读取枚举值?也许问题出在 EF 物化而不是枚举本身。
  • 谢尔盖,我相信你是对的。我在问题中添加了更多细节
  • 如果您认为问题在于 EF 实现,请查看 this question,也许会有所帮助。
  • 谢尔盖,谢谢。我找到了原因,与此无关。它与hangfire中的依赖注入生命周期范围有关。我完全重写了这个问题

标签: c# autofac hangfire


【解决方案1】:

Hangfire 当前为每个 Worker 使用JobActivator 的共享实例,它们使用以下方法来解决依赖关系:

    public override object ActivateJob(Type jobType)

计划在此方法中为Milestone 2.0.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));

【讨论】:

  • 这种方法的问题是它不是线程安全的。如果您正在运行并发作业,则可能将错误的范围用于错误的作业。我们需要 JobActivator 提供一种方法来创建 JobActivationScope(DI 实现必须提供一个简单的实现,以及 JobActivator 实现)。然后我们更改 Job.Perform 以在 using 语句中从激活器创建此范围,并将其作为参数传递给 Activate,而不是 JobActivator。
  • "如果您有并发作业正在运行,则错误的范围可能用于错误的作业。" - 但是并发作业不是在不同的工人(线程)上运行吗?
  • 我很抱歉。我忽略了[ThreadStatic] 归属。如果我们在执行的各个阶段对线程上下文的内部做出一些保证,那么是的,这可以作为一个临时措施,直到 2.0.0。就永久解决方案而言,我仍然想要一个更像我所描述的模型。
  • 是的,JobActivator 的当前设计不是最优的,我希望这将在未来的版本中得到改进。解决方法至少现在可以不用太多努力。如果您在过滤器中注册它,它还可以将上下文信息传递给作业(例如 JobID)。
  • 自 1.5.0-beta1 起支持按作业激活器作用域。结合 Autofac 集成中的非标记范围(自 2.2.0 起),这解决了原始问题。
【解决方案2】:

我们在 Hangfire.Autofac 中创建了一个新的拉取请求,使用了 Dresel 描述的解决方法。希望它被合并到主分支中:

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

【讨论】:

    【解决方案3】:

    编辑:对于 Autofac、.NET 4.5 和 Hangfire >= 1.5.0,使用 Hangfire.Autofac nuget package (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);
            }
        }
    }
    

    【讨论】:

      【解决方案4】:

      为了解决这个问题,我创建了一个一次性的 JobContext 类,它有一个 ILifetimeScope,它会在 Hangfire 完成工作时被释放。真正的工作是通过反射调用的。

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

      有一个 JobActivator 将检查操作并在必要时创建 LifetimeScope。

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

      【讨论】:

        【解决方案5】:

        hangfire.autofac 2.2.0 以来,支持开箱即用的解决方案。

        在您的情况下,您的依赖项是在每个生命周期范围内注册的,您应该能够在设置 hangfire.autofac 时使用non-tagged scopes。从链接:

        GlobalConfiguration.Configuration.UseAutofacActivator(builder.Build(), false);
        

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2021-02-07
          • 2017-06-04
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多