Hangfire RecurringJob + Simple Injector + MVC
Asked Answered
R

2

5

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)

Reflect answered 17/5, 2017 at 1:3 Comment(6)
You might have gotten the idea of the 'post commit' from my blog, but please note the warning on that article. I generally advise against using this approach, as explained in the warning.Soapy
@Soapy You're right, Steven. I like your approach and I decided to choose it in my app. Of course, I understand your warning. But...is it right to store user info in database using GUIDs as identifier (integer takes less space)? And what if I need for instanse to return not only identifier but another properties or whole object?Reflect
Compared to an INT, a GUID takes up 12 bytes of extra disk space. This shouldn't be a problem. There is however a performance penalty when using GUIDS, but I have never worked in a system where this would cause unsolvable performance problems. On the other hand, there are a lot of benefits of using Guids. And I don't see a problem when returning a whole object. That object just contains a GUID Id instead of an INT id.Soapy
@Soapy Saying about returning whole object I mean return it from Command...What do you think when it will be needed. In my app I need to return user data after creation.Reflect
Well.. do you really need to return that directly? Or can you split this and query this information after you executed the command using the same ID that the client already generated?Soapy
@Soapy Need to think about it...Personally, I also like to split responsibility. It gives more clear understanding and ability to use correctly. Well, I think in our project it is not an such important thing, so in future we'll become to use GUIDs.Reflect
R
6

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));
Reflect answered 18/5, 2017 at 22:7 Comment(1)
Can you elaborate why the default SimpleInjectorJobActivator is failing at it's job? And did you change the implementation of the job activator so that component won't start a AsyncScope?Feune
S
6

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();
    }        
}
Soapy answered 17/5, 2017 at 6:24 Comment(9)
Hmm...does it mean that this implementation is not correct?Reflect
I based my code example on that implementation, so yes, it is correct.Soapy
But I'm already using such implementation as I mentioned in my answer...GlobalConfiguration.Configuration.UseActivator(new SimpleInjectorJobActivator(Bootstrapper.Container));. May be I need manually set elsewhere something like _container.BeginExecutionScope()? If yes, so where I need to put it.Reflect
I've updated my question with new info, could you, please help me with this.Reflect
@Dmitry: A full stack trace would be helpful.Soapy
I've added StackTrace and it is similar to previous mentionedReflect
I've been looking at the stack trace and the Hangfire source code and the activator for quite some time, but I can't figure out what's going on here. There should be an active async scope. It's time you get the Hangfire developers involved in this.Soapy
Big thanks to you and your contribution to development and teaching, Steven! I'll ask a question to them on github and then I'll let you know.Reflect
I have no words to express my gratitude to you Steven. See my answer I came up with (with your help on GitHub=) )Reflect
R
6

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));
Reflect answered 18/5, 2017 at 22:7 Comment(1)
Can you elaborate why the default SimpleInjectorJobActivator is failing at it's job? And did you change the implementation of the job activator so that component won't start a AsyncScope?Feune

© 2022 - 2024 — McMap. All rights reserved.