【问题标题】:Hangfire RecurringJob + Simple Injector + MVCHangfire RecurringJob + 简单注入器 + MVC
【发布时间】:2017-05-17 01:03:37
【问题描述】:

我正在使用 Hangfire v1.6.12、Simple Injector v4.0.6、Hangfire.SimpleInjector v1.3.0 和 ASP.NET MVC 5 项目。我想创建recurringjob,它将触发并调用一个以用户标识符作为输入参数的方法。 这是我的配置:

public class BusinessLayerBootstrapper
{
    public static void Bootstrap(Container container)
    {
        if(container == null)
        {
            throw new ArgumentNullException("BusinessLayerBootstrapper container");
        }

        container.RegisterSingleton<IValidator>(new DataAnnotationsValidator(container));

        container.Register(typeof(ICommandHandler<>), AppDomain.CurrentDomain.GetAssemblies());
        container.Register(typeof(ICommandHandler<>), typeof(CreateCommandHandler<>));
        container.Register(typeof(ICommandHandler<>), typeof(ChangeCommandHandler<>));
        container.Register(typeof(ICommandHandler<>), typeof(DeleteCommandHandler<>));

        container.RegisterDecorator(typeof(ICommandHandler<>), typeof(TransactionCommandHandlerDecorator<>));

        container.RegisterDecorator(typeof(ICommandHandler<>), typeof(PostCommitCommandHandlerDecorator<>));

        container.Register<IPostCommitRegistrator>(() => container.GetInstance<PostCommitRegistrator>(), Lifestyle.Scoped);

        container.RegisterDecorator(typeof(ICommandHandler<>), typeof(ValidationCommandHandlerDecorator<>));
        container.RegisterDecorator(typeof(ICommandHandler<>), typeof(AuthorizationCommandHandlerDecorator<>));

        container.Register(typeof(IQueryHandler<,>), AppDomain.CurrentDomain.GetAssemblies());
        container.Register(typeof(IQueryHandler<,>), typeof(GetAllQueryHandler<>));
        container.Register(typeof(IQueryHandler<,>), typeof(GetByIdQueryHandler<>));
        container.Register(typeof(IQueryHandler<,>), typeof(GetByPrimaryKeyQueryHandler<>));

        container.RegisterDecorator(typeof(IQueryHandler<,>), typeof(ValidationQueryHandlerDecorator<,>));
        container.RegisterDecorator(typeof(IQueryHandler<,>), typeof(AuthorizationQueryHandlerDecorator<,>));

        container.Register<IScheduleService>(() => container.GetInstance<ScheduleService>(), Lifestyle.Scoped);
    }

public class Bootstrapper
{
    public static Container Container { get; internal set; }

    public static void Bootstrap()
    {
        Container = new Container();

        Container.Options.DefaultScopedLifestyle = Lifestyle.CreateHybrid(
                defaultLifestyle: new WebRequestLifestyle(),
                fallbackLifestyle: new AsyncScopedLifestyle());

        Business.BusinessLayerBootstrapper.Bootstrap(Container);

        Container.Register<IPrincipal>(() => HttpContext.Current !=null ? (HttpContext.Current.User ?? Thread.CurrentPrincipal) : Thread.CurrentPrincipal);
        Container.RegisterSingleton<ILogger>(new FileLogger());

        Container.Register<IUnitOfWork>(() => new UnitOfWork(ConfigurationManager.ConnectionStrings["PriceMonitorMSSQLConnection"].ProviderName, 
                                                             ConfigurationManager.ConnectionStrings["PriceMonitorMSSQLConnection"].ConnectionString), Lifestyle.Scoped);

        Container.RegisterSingleton<IEmailSender>(new EmailSender());

        Container.RegisterMvcControllers(Assembly.GetExecutingAssembly());
        //container.RegisterMvcAttributeFilterProvider();

        DependencyResolver.SetResolver(new SimpleInjectorDependencyResolver(Container));

        Container.Verify(VerificationOption.VerifyAndDiagnose);
    }
}

public class HangfireBootstrapper : IRegisteredObject
{
    public static readonly HangfireBootstrapper Instance = new HangfireBootstrapper();

    private readonly object _lockObject = new object();
    private bool _started;

    private BackgroundJobServer _backgroundJobServer;

    private HangfireBootstrapper() { }

    public void Start()
    {
        lock(_lockObject)
        {
            if (_started) return;
            _started = true;

            HostingEnvironment.RegisterObject(this);

            //JobActivator.Current = new SimpleInjectorJobActivator(Bootstrapper.Container);

            GlobalConfiguration.Configuration
                .UseNLogLogProvider()
                .UseSqlServerStorage(ConfigurationManager.ConnectionStrings["HangfireMSSQLConnection"].ConnectionString);

            GlobalConfiguration.Configuration.UseActivator(new SimpleInjectorJobActivator(Bootstrapper.Container));

            GlobalJobFilters.Filters.Add(new AutomaticRetryAttribute { LogEvents = true, Attempts = 0 });
            GlobalJobFilters.Filters.Add(new DisableConcurrentExecutionAttribute(15));                

            _backgroundJobServer = new BackgroundJobServer();
        }
    }

    public void Stop()
    {
        lock(_lockObject)
        {
            if (_backgroundJobServer != null)
            {
                _backgroundJobServer.Dispose();
            }

            HostingEnvironment.UnregisterObject(this);
        }
    }

    void IRegisteredObject.Stop(bool immediate)
    {
        this.Stop();
    }

    public bool JobExists(string recurringJobId)
    {
        using (var connection = JobStorage.Current.GetConnection())
        {
            return connection.GetRecurringJobs().Any(j => j.Id == recurringJobId);
        }
    }
}

主要起点:

public class MvcApplication : HttpApplication
{
    protected void Application_Start()
    {
        AreaRegistration.RegisterAllAreas();
        RouteConfig.RegisterRoutes(RouteTable.Routes);
        BundleConfig.RegisterBundles(BundleTable.Bundles);
        // SimpleInjector
        Bootstrapper.Bootstrap();
        // Hangfire
        HangfireBootstrapper.Instance.Start();
    }

    protected void Application_End(object sender, EventArgs e)
    {
        HangfireBootstrapper.Instance.Stop();
    }
}

我在控制器中调用我的方法(我知道它不是最好的变体,只是用于测试):

public class AccountController : Controller
{
    ICommandHandler<CreateUserCommand> CreateUser;
    ICommandHandler<CreateCommand<Job>> CreateJob;
    IQueryHandler<GetByPrimaryKeyQuery<User>, User> UserByPk;
    IScheduleService scheduler;

    public AccountController(ICommandHandler<CreateUserCommand> CreateUser,
                             ICommandHandler<CreateCommand<Job>> CreateJob,
                             IQueryHandler<GetByPrimaryKeyQuery<User>, User> UserByPk,
                             IScheduleService scheduler)
    {
        this.CreateUser = CreateUser;
        this.CreateJob = CreateJob;
        this.UserByPk = UserByPk;
        this.scheduler = scheduler;
    }

    // GET: Account
    public ActionResult Login()
    {
        // создаём повторяющуюся задачу, которая ссылается на метод 
        string jobId = 1 + "_RecurseMultiGrabbing";
        if (!HangfireBootstrapper.Instance.JobExists(jobId))
        {
            RecurringJob.AddOrUpdate<ScheduleService>(jobId, scheduler => scheduler.ScheduleMultiPricesInfo(1), Cron.MinuteInterval(5));
            // добавляем в нашу БД
            var cmdJob = new CreateCommand<Job>(new Job { UserId = 1, Name = jobId });
            CreateJob.Handle(cmdJob);
        }
        return View("Conf", new User());
    }
}

我的类的方法看起来像:

public class ScheduleService : IScheduleService
{
    IQueryHandler<ProductGrabbedInfoByUserQuery, IEnumerable<ProductGrabbedInfo>> GrabberQuery;
    IQueryHandler<GetByPrimaryKeyQuery<User>, User> UserQuery;
    ICommandHandler<CreateMultiPriceStatCommand> CreatePriceStats;
    ICommandHandler<CreateCommand<Job>> CreateJob;
    ICommandHandler<ChangeCommand<Job>> ChangeJob;
    ILogger logger;
    IEmailSender emailSender;

    public ScheduleService(IQueryHandler<ProductGrabbedInfoByUserQuery, IEnumerable<ProductGrabbedInfo>> GrabberQuery,
                           IQueryHandler<GetByPrimaryKeyQuery<User>, User> UserQuery,
                           ICommandHandler<CreateMultiPriceStatCommand> CreatePriceStats,
                           ICommandHandler<CreateCommand<Job>> CreateJob,
                           ICommandHandler<ChangeCommand<Job>> ChangeJob,
                           ILogger logger,
                           IEmailSender emailSender)
    {
        this.GrabberQuery = GrabberQuery;
        this.UserQuery = UserQuery;
        this.CreatePriceStats = CreatePriceStats;
        this.CreateJob = CreateJob;
        this.ChangeJob = ChangeJob;
        this.logger = logger;
        this.emailSender = emailSender;
    }

    public void ScheduleMultiPricesInfo(int userId)
    {
        // some operations
    }
}

结果当我的循环作业尝试运行方法时,抛出异常:

SimpleInjector.ActivationException:没有注册类型 可以找到 ScheduleService 并且无法进行隐式注册 做出来。 IUnitOfWork 注册为“混合 Web 请求/异步” Scoped 的生活方式,但在上下文之外请求实例 活动(混合 Web 请求/异步范围)范围。 ---> SimpleInjector.ActivationException:IUnitOfWork 注册为 “混合 Web 请求/异步范围”生活方式,但实例是 在活动上下文之外请求(混合 Web 请求/异步 范围)范围。在 SimpleInjector.Scope.GetScopelessInstance[TImplementation](ScopedRegistration1 registration) at SimpleInjector.Scope.GetInstance[TImplementation](ScopedRegistration1 注册,范围范围)在 SimpleInjector.Advanced.Internal.LazyScopedRegistration1.GetInstance(Scope scope) at lambda_method(Closure ) at SimpleInjector.InstanceProducer.GetInstance() --- End of inner exception stack trace --- at SimpleInjector.InstanceProducer.GetInstance() at SimpleInjector.Container.GetInstance(Type serviceType) at Hangfire.SimpleInjector.SimpleInjectorScope.Resolve(Type type) at Hangfire.Server.CoreBackgroundJobPerformer.Perform(PerformContext context) at Hangfire.Server.BackgroundJobPerformer.<>c__DisplayClass8_0.<PerformJobWithFilters>b__0() at Hangfire.Server.BackgroundJobPerformer.InvokePerformFilter(IServerFilter filter, PerformingContext preContext, Func1 续)在 Hangfire.Server.BackgroundJobPerformer.c__DisplayClass8_1.b__2() 在 Hangfire.Server.BackgroundJobPerformer.InvokePerformFilter(IServerFilter 过滤器,PerformingContext preContext,Func1 continuation) at Hangfire.Server.BackgroundJobPerformer.<>c__DisplayClass8_1.<PerformJobWithFilters>b__2() at Hangfire.Server.BackgroundJobPerformer.PerformJobWithFilters(PerformContext context, IEnumerable1 过滤器)在 Hangfire.Server.BackgroundJobPerformer.Perform(PerformContext 上下文) 在 Hangfire.Server.Worker.PerformJob(BackgroundProcessContext 上下文, IStorageConnection 连接,字符串 jobId)

不明白我还需要做什么。我有一个想法,我需要手动开始执行范围,但是我不知道从哪里开始和关闭它。你能给我一些建议吗?

更新

我将经常性的工作电话改为这个:

RecurringJob.AddOrUpdate<IScheduleService>(jobId, scheduler => scheduler.ScheduleMultiPricesInfo(1), Cron.MinuteInterval(5));

并注册到这个:

public class Bootstrapper
{
    public static Container Container { get; internal set; }

    public static void Bootstrap()
    {
        Container = new Container();

        Container.Options.DefaultScopedLifestyle = Lifestyle.CreateHybrid(
                defaultLifestyle: new WebRequestLifestyle(),
                fallbackLifestyle: new AsyncScopedLifestyle());

        Business.BusinessLayerBootstrapper.Bootstrap(Container);
        Container.Register<Hangfire.JobActivator, Hangfire.SimpleInjector.SimpleInjectorJobActivator>(Lifestyle.Scoped);

        Container.Register<IPrincipal>(() => HttpContext.Current !=null ? (HttpContext.Current.User ?? Thread.CurrentPrincipal) : Thread.CurrentPrincipal);
        Container.RegisterSingleton<ILogger, FileLogger>();
        Container.RegisterSingleton<IEmailSender>(new EmailSender());
        // this line was moved out from BusinessLayerBootstrapper to Web part
        Container.Register<IScheduleService, Business.Concrete.ScheduleService>();

        string provider = ConfigurationManager.ConnectionStrings["PriceMonitorMSSQLConnection"].ProviderName;
        string connection = ConfigurationManager.ConnectionStrings["PriceMonitorMSSQLConnection"].ConnectionString;
        Container.Register<IUnitOfWork>(() => new UnitOfWork(provider, connection), 
                                        Lifestyle.Scoped);

        Container.RegisterMvcControllers(Assembly.GetExecutingAssembly());
        DependencyResolver.SetResolver(new SimpleInjectorDependencyResolver(Container));

        Container.Verify(VerificationOption.VerifyAndDiagnose);
    }
}

这有助于我解决 ScheduleService 的注册问题,但异常的第二部分是相同的(StackTrace 也与上面提到的相同):

SimpleInjector.ActivationException:IUnitOfWork 注册为“混合 Web 请求/异步范围”生活方式,但实例是 在活动上下文之外请求(混合 Web 请求/异步 范围)范围。 在 SimpleInjector.Scope.GetScopelessInstance[TImplementation](ScopedRegistration1 registration) at SimpleInjector.Scope.GetInstance[TImplementation](ScopedRegistration1 注册,Scope 范围) 在 SimpleInjector.Advanced.Internal.LazyScopedRegistration1.GetInstance(Scope scope) at lambda_method(Closure ) at SimpleInjector.InstanceProducer.BuildAndReplaceInstanceCreatorAndCreateFirstInstance() at SimpleInjector.InstanceProducer.GetInstance() at SimpleInjector.Container.GetInstanceForRootType(Type serviceType) at SimpleInjector.Container.GetInstance(Type serviceType) at Hangfire.SimpleInjector.SimpleInjectorScope.Resolve(Type type) at Hangfire.Server.CoreBackgroundJobPerformer.Perform(PerformContext context) at Hangfire.Server.BackgroundJobPerformer.<>c__DisplayClass8_0.<PerformJobWithFilters>b__0() at Hangfire.Server.BackgroundJobPerformer.InvokePerformFilter(IServerFilter filter, PerformingContext preContext, Func1 继续) 在 Hangfire.Server.BackgroundJobPerformer.c__DisplayClass8_1.b__2() 在 Hangfire.Server.BackgroundJobPerformer.InvokePerformFilter(IServerFilter 过滤器,PerformingContext preContext,Func1 continuation) at Hangfire.Server.BackgroundJobPerformer.<>c__DisplayClass8_1.<PerformJobWithFilters>b__2() at Hangfire.Server.BackgroundJobPerformer.PerformJobWithFilters(PerformContext context, IEnumerable1 过滤器) 在 Hangfire.Server.BackgroundJobPerformer.Perform(PerformContext 上下文) 在 Hangfire.Server.Worker.PerformJob(BackgroundProcessContext context, IStorageConnection connection, String jobId)

【问题讨论】:

  • 您可能已经从my blog 获得了“提交后”的想法,但请注意该文章中的警告。如警告中所述,我通常建议不要使用这种方法。
  • @Steven 你是对的,史蒂文。我喜欢你的方法,我决定在我的应用程序中选择它。当然,我理解你的警告。但是......使用 GUID 作为标识符将用户信息存储在数据库中是否正确(整数占用更少的空间)?如果我需要实例不仅返回标识符,还需要返回另一个属性或整个对象怎么办?
  • 与 INT 相比,GUID 占用 12 个字节的额外磁盘空间。这应该不是问题。然而,使用 GUIDS 时会有性能损失,但我从未在会导致无法解决的性能问题的系统中工作过。另一方面,使用 Guid 有很多好处。返回整个对象时我没有看到问题。该对象只包含一个 GUID Id 而不是一个 INT id。
  • @Steven 关于返回整个对象我的意思是从命令返回它......你认为什么时候需要它。在我的应用中,我需要在创建后返回用户数据。
  • @Steven 需要考虑一下……我个人也喜欢分担责任。它提供了更清晰的理解和正确使用的能力。好吧,我认为在我们的项目中它不是那么重要,所以将来我们会使用 GUID。

标签: c# asp.net-mvc simple-injector hangfire


【解决方案1】:

我创建了 ScopeFilter 类,因为 Steven(SimpleInjector creator) 给了我建议的代码示例,如下所示:

public class SimpleInjectorAsyncScopeFilterAttribute : JobFilterAttribute, IServerFilter
{
    private static readonly AsyncScopedLifestyle lifestyle = new AsyncScopedLifestyle();

    private readonly Container _container;

    public SimpleInjectorAsyncScopeFilterAttribute(Container container)
    {
        _container = container;
    }

    public void OnPerforming(PerformingContext filterContext)
    {
        AsyncScopedLifestyle.BeginScope(_container);
    }

    public void OnPerformed(PerformedContext filterContext)
    {
        var scope = lifestyle.GetCurrentScope(_container);
        if (scope != null)
            scope.Dispose();
    }
}

那么我们只需要在全局hangfire配置中添加这个过滤器:

GlobalConfiguration.Configuration.UseActivator(new SimpleInjectorJobActivator(Bootstrapper.Container));
GlobalJobFilters.Filters.Add(new SimpleInjectorAsyncScopeFilterAttribute(Bootstrapper.Container));

【讨论】:

  • 您能详细说明为什么默认的SimpleInjectorJobActivator 无法正常工作吗?您是否更改了作业激活器的实现以使该组件不会启动 AsyncScope?
【解决方案2】:

异常状态:

IUnitOfWork 注册为“混合 Web 请求/异步范围”生活方式,但在活动(混合 Web 请求/异步范围)范围的上下文之外请求实例。

换句话说,您创建了一个由WebRequestLifestyleAsyncScopedLifestyle 组成的混合生活方式,但既没有活动的Web 请求,也没有异步范围。这意味着您正在后台线程上运行(并且堆栈跟踪证实了这一点),而您正在从 Simple Injector 解析,而您尚未将操作显式包装在异步范围内。您显示的所有代码中都没有任何迹象表明您确实这样做了。

要在 Hangfire 创建作业之前开始和结束范围,您可以实现自定义 JobActivator。例如:

using SimpleInjector;
using SimpleInjector.Lifestyles;

public class SimpleInjectorJobActivator : JobActivator
{
    private readonly Container container;

    public SimpleInjectorJobActivator(Container container)
    {
        this.container = container;
    }

    public override object ActivateJob(Type jobType) => this.container.GetInstance(jobType);
    public override JobActivatorScope BeginScope(JobActivatorContext c)
        => new JobScope(this.container);

    private sealed class JobScope : JobActivatorScope
    {
        private readonly Container container;
        private readonly Scope scope;

        public JobScope(Container container)
        {
            this.container = container;
            this.scope = AsyncScopedLifestyle.BeginScope(container);
        }

        public override object Resolve(Type type) => this.container.GetInstance(type);
        public override void DisposeScope() => this.scope?.Dispose();
    }        
}

【讨论】:

  • 嗯...是不是说this的实现不正确?
  • 我的代码示例基于该实现,所以是的,它是正确的。
  • 但是我已经在使用我在回答中提到的这种实现...GlobalConfiguration.Configuration.UseActivator(new SimpleInjectorJobActivator(Bootstrapper.Container));。可能我需要在其他地方手动设置_container.BeginExecutionScope()?如果是,那么我需要把它放在哪里。
  • 我一直在查看堆栈跟踪和 Hangfire source codeactivator 很长一段时间,但我无法弄清楚这里发生了什么。应该有一个活动的异步范围。是时候让 Hangfire 开发人员参与其中了。
  • 非常感谢您以及您对发展和教学的贡献,史蒂文!我会在 github 上向他们提问,然后我会告诉你。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-08-14
  • 1970-01-01
相关资源
最近更新 更多