Hangfire定时任务 + Simple Injector + MVC

5

我正在使用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被注册为 "Hybrid Web Request / Async Scoped" 生命周期,但是在没有活动 (Hybrid Web Request / Async Scoped) 作用域的上下文中请求了该实例。---> SimpleInjector.ActivationException: IUnitOfWork 被注册为 "Hybrid Web Request / Async Scoped" 生命周期,但是在没有活动 (Hybrid Web Request / Async Scoped) 作用域的上下文中请求了该实例。 at SimpleInjector.Scope.GetScopelessInstance[TImplementation](ScopedRegistration`1 registration) at SimpleInjector.Scope.GetInstance[TImplementation](ScopedRegistration`1 registration, Scope scope) at SimpleInjector.Advanced.Internal.LazyScopedRegistration`1.GetInstance(Scope scope) at lambda_method(Closure ) at SimpleInjector.InstanceProducer.GetInstance() --- 内部异常堆栈跟踪的结尾 --- 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, Func`1 continuation) at Hangfire.Server.BackgroundJobPerformer.<>c__DisplayClass8_1.b__2() at Hangfire.Server.BackgroundJobPerformer.InvokePerformFilter(IServerFilter filter, PerformingContext preContext, Func`1 continuation) at Hangfire.Server.BackgroundJobPerformer.<>c__DisplayClass8_1.<PerformJobWithFilters>b__2() at Hangfire.Server.BackgroundJobPerformer.PerformJobWithFilters(PerformContext context, IEnumerable`1 filters) at Hangfire.Server.BackgroundJobPerformer.Perform(PerformContext context) at Hangfire.Server.Worker.PerformJob(BackgroundProcessContext context, IStorageConnection connection, 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被注册为“混合Web请求/异步作用域”生命周期,但在没有活动(混合Web请求/异步作用域)范围的上下文中请求实例。 在SimpleInjector.Scope.GetScopelessInstance[TImplementation] (ScopedRegistration < code> 1 registration)处 在SimpleInjector.Scope.GetInstance [TImplementation](ScopedRegistration 1 registration,Scope scope)处 在SimpleInjector.Advanced.Internal.LazyScopedRegistration < code> 1.GetInstance(Scope scope)处 在lambda_method(闭包)处 在SimpleInjector.InstanceProducer.BuildAndReplaceInstanceCreatorAndCreateFirstInstance()处 在SimpleInjector.InstanceProducer.GetInstance()处 在SimpleInjector.Container.GetInstanceForRootType(Type serviceType)处 在SimpleInjector.Container.GetInstance(Type serviceType)处 在Hangfire.SimpleInjector.SimpleInjectorScope.Resolve(Type type)处 在Hangfire.Server.CoreBackgroundJobPerformer.Perform(PerformContext context)处 在Hangfire.Server.BackgroundJobPerformer.<>c__DisplayClass8_0.<PerformJobWithFilters>b__0()处 在Hangfire.Server.BackgroundJobPerformer.InvokePerformFilter(IServerFilter filter, PerformingContext preContext, Func1 continuation)处 在Hangfire.Server.BackgroundJobPerformer.<>c__DisplayClass8_1.b__2()处 在Hangfire.Server.BackgroundJobPerformer.InvokePerformFilter(IServerFilter filter, PerformingContext preContext, Func1 continuation)处 在Hangfire.Server.BackgroundJobPerformer.<>c__DisplayClass8_1.<PerformJobWithFilters>b__2()处 在Hangfire.Server.BackgroundJobPerformer.PerformJobWithFilters(PerformContext context, IEnumerable1 filters)处 在Hangfire.Server.BackgroundJobPerformer.Perform(PerformContext context)处 在Hangfire.Server.Worker.PerformJob(BackgroundProcessContext context, IStorageConnection connection, String jobId)处


你可能从我的博客中得到了“提交后”这个想法,但请注意该文章中的警告。我通常建议不要使用这种方法,如警告所述。 - Steven
@Steven 你说得对,Steven。我喜欢你的方法,并决定在我的应用程序中选择它。当然,我理解你的警告。但是...使用GUID作为标识符在数据库中存储用户信息是否正确(整数占用更少的空间)?如果我需要返回不仅标识符而且其他属性或整个对象怎么办? - Dmitriy
1
相比于 INT,GUID占用了额外的 12 字节磁盘空间。这应该不是问题。然而,在使用 GUID 时会有性能惩罚,但我从未在这方面工作过,因此不会导致无法解决的性能问题。另一方面,使用GUID有很多好处。并且我并不认为返回整个对象会有问题。该对象只包含 GUID Id 而不是 INT id。 - Steven
@Steven 说的是从 Command 返回整个对象...你认为什么时候需要它。在我的应用程序中,我需要在创建之后返回用户数据。 - Dmitriy
嗯...你真的需要直接返回那个吗?或者在使用客户端已经生成的相同ID后,你可以将其拆分并查询该信息执行命令后再进行查询吗? - Steven
1
@Steven 需要考虑一下...个人而言,我也喜欢分割职责。这样可以更清晰地理解和正确使用。嗯,我想在我们的项目中这不是非常重要的事情,所以将来我们会开始使用 GUID。 - Dmitriy
2个回答

6
异常提示:

注册的IUnitOfWork的生命周期为'Hybrid Web Request / Async Scoped',但该实例在没有活动(Hybrid Web Request / Async Scoped)范围的上下文中被请求。

换句话说,你创建了一个由 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();
    }        
}

我基于那个实现编写了我的代码示例,所以是的,它是正确的。 - Steven
但是我已经在我的答案中提到了这样的实现... GlobalConfiguration.Configuration.UseActivator(new SimpleInjectorJobActivator(Bootstrapper.Container));。也许我需要手动设置其他地方的 _container.BeginExecutionScope()?如果是这样,那么我需要把它放在哪里呢? - Dmitriy
2
我一直在查看堆栈跟踪和Hangfire源代码以及激活器,但是我无法弄清楚这里发生了什么。应该有一个活动的异步范围。现在是时候让Hangfire开发人员介入了。 - Steven
1
非常感谢您对开发和教学的贡献,Steven!我会在GitHub上向他们提问,然后告诉您。 - Dmitriy
我无法用言语来表达我对你Steven的感激之情。请看我在Stack Overflow上的答案,是在你GitHub的帮助下完成的(=))。 - Dmitriy
显示剩余4条评论

6

我创建了ScopeFilter类,这是基于Steven(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));

1
你能详细说明为什么默认的 SimpleInjectorJobActivator 没有完成它的工作吗?你是否更改了作业激活器的实现,以便组件不会启动 AsyncScope? - Ric .Net

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接