Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Hangfire RecurringJob + Simple Injector + MVC

I'm using Hangfire v1.6.12, Simple Injector v4.0.6, Hangfire.SimpleInjector v1.3.0 and ASP.NET MVC 5 project. I want to create recurringjob which will trigger and call a method with user identifier as input parameter. Here is my configuration:

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

And main start point:

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

I call my method in controller (I know that it is not best variant but simply for testing):

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

And my class with method looks like:

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

As a result when my recurring job tries to run method, there is an exception thrown:

SimpleInjector.ActivationException: No registration for type ScheduleService could be found and an implicit registration could not be made. The IUnitOfWork is registered as 'Hybrid Web Request / Async Scoped' lifestyle, but the instance is requested outside the context of an active (Hybrid Web Request / Async Scoped) scope. ---> SimpleInjector.ActivationException: The IUnitOfWork is registered as 'Hybrid Web Request / Async Scoped' lifestyle, but the instance is requested outside the context of an active (Hybrid Web Request / Async Scoped) scope. at SimpleInjector.Scope.GetScopelessInstance[TImplementation](ScopedRegistration1 registration) at SimpleInjector.Scope.GetInstance[TImplementation](ScopedRegistration1 registration, Scope scope) at 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 continuation) at Hangfire.Server.BackgroundJobPerformer.<>c__DisplayClass8_1.b__2() at Hangfire.Server.BackgroundJobPerformer.InvokePerformFilter(IServerFilter filter, PerformingContext preContext, Func1 continuation) at Hangfire.Server.BackgroundJobPerformer.<>c__DisplayClass8_1.<PerformJobWithFilters>b__2() at Hangfire.Server.BackgroundJobPerformer.PerformJobWithFilters(PerformContext context, IEnumerable1 filters) at Hangfire.Server.BackgroundJobPerformer.Perform(PerformContext context) at Hangfire.Server.Worker.PerformJob(BackgroundProcessContext context, IStorageConnection connection, String jobId)

Can't understand what else I need to do. I have one idea that I need to manually begin execution scope but where to begin and close it I can't figure out. Could you give me some advices?

UPDATED

I changed my recurring job call to this one:

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

And registration to this:

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

This helps me to solve registration question for ScheduleService, but second part of exception is the same (StackTrace is also same as was mentioned above):

SimpleInjector.ActivationException: The IUnitOfWork is registered as 'Hybrid Web Request / Async Scoped' lifestyle, but the instance is requested outside the context of an active (Hybrid Web Request / Async Scoped) scope. at SimpleInjector.Scope.GetScopelessInstance[TImplementation](ScopedRegistration1 registration) at SimpleInjector.Scope.GetInstance[TImplementation](ScopedRegistration1 registration, Scope scope) at 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 continuation) at Hangfire.Server.BackgroundJobPerformer.<>c__DisplayClass8_1.b__2() at Hangfire.Server.BackgroundJobPerformer.InvokePerformFilter(IServerFilter filter, PerformingContext preContext, Func1 continuation) at Hangfire.Server.BackgroundJobPerformer.<>c__DisplayClass8_1.<PerformJobWithFilters>b__2() at Hangfire.Server.BackgroundJobPerformer.PerformJobWithFilters(PerformContext context, IEnumerable1 filters) at Hangfire.Server.BackgroundJobPerformer.Perform(PerformContext context) at Hangfire.Server.Worker.PerformJob(BackgroundProcessContext context, IStorageConnection connection, String jobId)

like image 901
Dmitriy Avatar asked May 17 '17 01:05

Dmitriy


Video Answer


2 Answers

The exception states:

The IUnitOfWork is registered as 'Hybrid Web Request / Async Scoped' lifestyle, but the instance is requested outside the context of an active (Hybrid Web Request / Async Scoped) scope.

So in other words, you created a Hybrid lifestyle consisting of a WebRequestLifestyle and AsyncScopedLifestyle, but there is neither an active web request nor an async scope. This means that you're running on a background thread (and the stack trace confirms this), while you are resolving from Simple Injector while you haven't explicitly wrapped the operation in an async scope. There is no indication in all the code you shown that you actually do this.

To start and end a scope just before Hangfire creates a job, you can implement a custom JobActivator. For instance:

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();
    }        
}
like image 64
Steven Avatar answered Oct 26 '22 07:10

Steven


I created ScopeFilter class as Steven(SimpleInjector creator) has given me advise with code sample, which looks like:

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

Then all we need is to add this filter in global hangfire configuration:

GlobalConfiguration.Configuration.UseActivator(new SimpleInjectorJobActivator(Bootstrapper.Container));
GlobalJobFilters.Filters.Add(new SimpleInjectorAsyncScopeFilterAttribute(Bootstrapper.Container));
like image 36
Dmitriy Avatar answered Oct 26 '22 07:10

Dmitriy