Search code examples
c#asp.net-mvcdependency-injectionsimple-injectorfluentscheduler

How to properly configure Simple Injector with FluentScheduler


I have following configuration for Simple Injector.

public class SimpleInjectorIntegrator
{
    private static Container container;

    public static Container Setup()
    {
        container = new Container();
        container.Options.DefaultScopedLifestyle = Lifestyle.CreateHybrid(
            defaultLifestyle: new WebRequestLifestyle(),
            fallbackLifestyle: new ThreadScopedLifestyle());

        container.Register<IUserService, UserService>(Lifestyle.Scoped);
        container.Register<IJob, BackgroundScheduler>(Lifestyle.Scoped);

        JobManager.JobFactory = new SimpleInjectorJobFactory(container);
        JobManager.Initialize(new RegisterScheduler());
    }
}

public class SimpleInjectorJobFactory : IJobFactory
{
    Container Container;

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

    public IJob GetJobInstance<T>() where T : IJob
    {
        return Container.GetInstance<IJob>();
    }
}

The RegisterScheduler initializes and Schedules the job.

BackgroundScheduler looks like as below:

public class BackgroundScheduler : IJob, IRegisteredObject
    {
        IUserService _userService;

        public BackgroundScheduler(IUserService userService)
        {
            _userService = userService;
        }

        public void Execute() 
        {
            _userService.GetAll();
        }
    }

The BackgroundScheduler depends on IUserService. When I try to inject IUserService in Background scheduler I got following exception:

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

Stack trace:

SimpleInjector.ActivationException was unhandled by user code
  HResult=-2146233088
  Message=The BackgroundScheduler is registered as 'Hybrid Web Request / Thread Scoped' lifestyle, but the instance is requested outside the context of an active (Hybrid Web Request / Thread Scoped) scope.
  Source=SimpleInjector
  StackTrace:
       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.BuildAndReplaceInstanceCreatorAndCreateFirstInstance()
       at SimpleInjector.InstanceProducer.GetInstance()
       at SimpleInjector.Container.GetInstanceFromProducer(InstanceProducer instanceProducer, Type serviceType)
       at SimpleInjector.Container.GetInstanceForRootType[TService]()
       at SimpleInjector.Container.GetInstance[TService]()
       at FluentScheduler.JobManager.<>c__12`1.<GetJobAction>b__12_0() in __the_file_path_omitted__:line 76
       at System.Threading.Tasks.Task.InnerInvoke()
       at System.Threading.Tasks.Task.Execute()
  InnerException: 

I am not sure why this is happening?


Solution

  • FleuntScheduler's IJobFactory is deprecated, while not being replaced with another extension point. Although the official documentation seems to lack any description on how to effectively resolve your jobs from your DI Container, the maintainer's point of view seems that you register your jobs as a closure.

    Since the use of a closure means resolving the job, wrapping it in a scope, and registering your job in Simple Injector, the most practical solution would be to move this logic into an extension method. This could look like this:

    public static void AddFluentSchedulerJob<TJob>(
        this Container container, Action<Schedule> schedule)
        where TJob : class, IMyJob
    {
        container.Register<TJob>();
    
        JobManager.AddJob(() =>
            {
                using (AsyncScopedLifestyle.BeginScope(container))
                {
                    container.GetInstance<TJob>().Run();
                }
            },
            schedule);
    }
    

    In this example, IMyJob is an application-specified abstraction. When doing this, you prevent your application code from requiring to depend on FluentScheduler.

    This extension method can be used as follows:

    container.AddFluentSchedulerJob<MyAwesomeJob>(s => s.ToRunEvery(5).Seconds());
    

    The other options is to use the (now deprecated) IJobFactory. This requires your jobs to implement FluentScheduler's IJob interface. The difficulty in implementing a job factory is that you need to find a way to wrap the operation in a scope.

    The trick is to wrap the resolve and execution of your job with a scope. As there seem limited interception points in FluentScheduler, the only way I figured you can do this, is by altering your job factory in such way that it returns a decorator that postpones the creation of the real job until the decorator's Execute method is called. The decorator's Execute can begin the scope, resolve the real job, execute that job, and dispose the scope internally.

    Here's the factory that uses a scope:

    public class SimpleInjectorJobFactory : IJobFactory
    {
        private readonly Container container;
    
        public SimpleInjectorJobFactory(Container container) => this.container = container;
    
        public IJob GetJobInstance<T>() where T : IJob
        {
            return new AsyncScopedJobDecorator(
                this.container,
                () => (IJob)this.container.GetInstance(typeof(T)));
        }
    
        private sealed class AsyncScopedJobDecorator : IJob
        {
            private readonly Container container;
            private readonly Func<IJob> decorateeFactory;
    
            public AsyncScopedJobDecorator(Container container, Func<IJob> decorateeFactory)
            {
                this.container = container;
                this.decorateeFactory = decorateeFactory;
            }
    
            public void Execute()
            {
                using (AsyncScopedLifestyle.BeginScope(this.container))
                {
                    this.decorateeFactory().Execute();
                }
            }
        }
    }
    

    You can use that factory by setting the JobManager.JobFactory, as you are already doing:

    JobManager.JobFactory = new SimpleInjectorJobFactory(container);