Hangfire RecurringJob + простой инжектор + MVC

Я использую Hangfire v1.6.12, Simple Injector v4.0.6, Hangfire.SimpleInjector v1.3.0 и проект ASP.NET MVC 5. Я хочу создать повторяющуюся задачу, которая будет запускать и вызывать метод с идентификатором пользователя в качестве входного параметра. Вот моя конфигурация:

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 зарегистрирован как образ жизни «гибридный веб-запрос / асинхронная область действия», но экземпляр запрашивается вне контекста активной (гибридный веб-запрос / асинхронная область действия). ---> SimpleInjector.ActivationException: IUnitOfWork зарегистрирован как образ жизни «гибридный веб-запрос / асинхронная область действия», но экземпляр запрашивается вне контекста активной области (гибридный веб-запрос / асинхронная область действия). в 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.BackgroundJobPer Hangfire. filter, 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, String 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 зарегистрирован как образ жизни «гибридный веб-запрос / асинхронная область действия», но экземпляр запрашивается вне контекста активной области (гибридный веб-запрос / асинхронная область действия). в 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.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.BackgroundJobPer Hangfire. filter, PerformingContext preContext, Func1 continuation) at Hangfire.Server.BackgroundJobPerformer.<>c__DisplayClass8_1.<PerformJobWithFilters>b__2() at Hangfire.Server.BackgroundJobPerformer.PerformJobWithFilters(PerformContext context, IEnumerable1 Filters) в Hangfire.Server.BackgroundJobPerformer.Perform (контекст PerformContext) в Hangfire.Server.Worker.PerformJob (контекст BackgroundProcessContext, соединение IStorageConnection, String jobId)


person Dmitriy    schedule 17.05.2017    source источник
comment
Возможно, вы получили идею «фиксации публикации» из моего блога, но обратите внимание на предупреждение в этой статье. Я обычно не рекомендую использовать этот подход, как описано в предупреждении.   -  person Steven    schedule 17.05.2017
comment
@ Стивен. Ты прав, Стивен. Мне нравится ваш подход, и я решил выбрать его в своем приложении. Конечно, я понимаю ваше предупреждение. Но ... правильно ли хранить информацию о пользователе в базе данных, используя GUID в качестве идентификатора (целое число занимает меньше места)? А что, если мне нужно, чтобы экземпляр возвращал не только идентификатор, но и другие свойства или весь объект?   -  person Dmitriy    schedule 17.05.2017
comment
По сравнению с INT, GUID занимает 12 байт дополнительного дискового пространства. Это не должно быть проблемой. Однако при использовании GUIDS снижается производительность, но я никогда не работал в системе, где это могло бы вызвать неразрешимые проблемы с производительностью. С другой стороны, использование Guids дает много преимуществ. И я не вижу проблем при возврате всего объекта. Этот объект просто содержит идентификатор GUID вместо идентификатора INT.   -  person Steven    schedule 17.05.2017
comment
@Steven Говоря о возврате всего объекта, я имею в виду вернуть его из Command ... Как вы думаете, когда он понадобится. В моем приложении мне нужно вернуть данные пользователя после создания.   -  person Dmitriy    schedule 17.05.2017
comment
Ну .. тебе действительно нужно вернуть это напрямую? Или вы можете разделить это и запросить эту информацию после того, как вы выполнили команду, используя тот же идентификатор, который уже сгенерировал клиент?   -  person Steven    schedule 17.05.2017
comment
@ Стивен Надо подумать ... Лично мне тоже нравится разделять ответственность. Это дает более четкое понимание и умение правильно использовать. Что ж, я думаю, что в нашем проекте это не такая важная вещь, поэтому в будущем мы станем использовать GUID.   -  person Dmitriy    schedule 17.05.2017


Ответы (2)


В исключении говорится:

IUnitOfWork зарегистрирован как образ жизни «гибридный веб-запрос / асинхронная область действия», но экземпляр запрашивается вне контекста активной области (гибридный веб-запрос / асинхронная область действия).

Другими словами, вы создали гибридный образ жизни, состоящий из WebRequestLifestyle и AsyncScopedLifestyle, но нет ни активного веб-запроса, ни асинхронной области. Это означает, что вы работаете в фоновом потоке (и трассировка стека подтверждает это), в то время как вы выполняете разрешение из 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();
    }        
}
person Steven    schedule 17.05.2017
comment
Хм ... означает ли это, что это неверная реализация? - person Dmitriy; 17.05.2017
comment
Я основал свой пример кода на этой реализации, так что да, это правильно. - person Steven; 17.05.2017
comment
Но я уже использую такую ​​реализацию, о которой упоминал в своем ответе ..._ 1_. Может быть, мне нужно вручную установить в другом месте что-то вроде _container.BeginExecutionScope()? Если да, то куда мне нужно его положить. - person Dmitriy; 17.05.2017
comment
Я обновил свой вопрос новой информацией, не могли бы вы помочь мне с этим. - person Dmitriy; 17.05.2017
comment
@Dmitry: Полезна полная трассировка стека. - person Steven; 17.05.2017
comment
Я добавил StackTrace, он похож на ранее упомянутый - person Dmitriy; 17.05.2017
comment
Я смотрел трассировку стека и Hangfire исходный код и активатор довольно долго, но я не могу понять, что здесь происходит. Должна быть активная асинхронная область. Пора вовлечь в это разработчиков Hangfire. - person Steven; 17.05.2017
comment
Большое спасибо тебе и твоему вкладу в развитие и обучение, Стивен! Я задам им вопрос на github, а потом дам вам знать. - person Dmitriy; 18.05.2017
comment
У меня нет слов, чтобы выразить вам свою благодарность, Стивен. См. Мой ответ, который я придумал (с вашей помощью на GitHub =)) - person Dmitriy; 19.05.2017

Я создал класс ScopeFilter, поскольку Стивен (создатель SimpleInjector) дал мне совет с образцом кода, который выглядит так:

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));
person Dmitriy    schedule 18.05.2017
comment
Не могли бы вы объяснить, почему SimpleInjectorJobActivator по умолчанию не справляется со своей задачей? И вы изменили реализацию активатора заданий, чтобы этот компонент не запускал AsyncScope? - person Ric .Net; 06.03.2018