Ninject ThreadScope Issues

Home / Ninject ThreadScope Issues

Ninject is one of the better dependency injection frameworks, IMHO. However, within the context of .NET Threads and thread pooling, scoping of objects doesn’t work quite as expected.


Today, for example, I was running a little Windows Service that uses Ninject, Entity Framework, and a Repository pattern to perform some work and then persist records of the work performed. It’s a repeating Task using the TaskRepeater code that I previously blogged.

I just happened to notice that if I cleared out some data in the database to force a re-run/reprocess of the data, data was not being updated. After examination, I noticed that the EF DbContext was being reused. This made me wonder what in the world was happening.

It turns out, and I should have considered this before, ThreadScope is just that. Objects are scoped/disposed when a Thread is terminated. But, .NET uses a ThreadPool in which Threads are reused. This complicates things since there is no clear separation of when objects should be released and disposed.

I read up on some Ninject extensions that add other various scoping options, named scopes, call scopes, parent scopes, etc, etc. However, I also saw a mention of the concept of a child scope. Creating a child scope, while maintaining singleton objects across all scopes (via the parent) seemed like a good option.

The path I wound up taking was to create (3) Ninject modules. I kept the Singleton / top-level bindings in what I called my NinjectParentModule. It does things like define my Logger, CommandInterceptors, and such. It looks like this:

public class NinjectParentModule : NinjectModule
{
    public override void Load()
    {
        Bind<IWorkerStatus>().To<WorkerStatus>().InSingletonScope();

        // userservice
        Bind<ICurrentUserService>().To<CurrentUserService>().InThreadScope();

        // Logger
        var logger = LogManager.GetLogger(AppSettings.ServiceName);
        Bind<ILog>().ToConstant(logger);

        Bind<ApiInfo>().ToSelf().InSingletonScope();
    }
}

I then moved all of my bindings for my DbContext, Repositories into a “Common” module:

public class NinjectCommonModule : NinjectModule
{
    public override void Load()
    {
        // contexts
        Bind<MyContext>().ToSelf().InThreadScope()
            .WithConstructorArgument("currentUserService", (ICurrentUserService)this.Kernel.GetService(typeof(ICurrentUserService)))
            .WithPropertyValue("CommandTimeout", 60);

.... etc
}

Finally, I created a NinjectTaskModule. This is the module I will load when I instantiate a “child” kernel. The important thing here is that I pass in the “parent” kernel and the bindings that I want to life (singletons) throughout the lifetime of the service use the reference to the parent kernel to bind against.

public class NinjectTaskModule : NinjectModule
{
    public IKernel ParentKernel { get; set; }

    public NinjectTaskModule(IKernel parent)
    {
        ParentKernel = parent;
    }

    public override void Load()
    {
        Bind<IWorkerStatus>().ToConstant((IWorkerStatus)this.ParentKernel.GetService(typeof(IWorkerStatus)));
        Bind<ILog>().ToConstant((ILog)this.ParentKernel.GetService(typeof(ILog)));
        Bind<ApiInfo>().ToConstant((ApiInfo)this.ParentKernel.GetService(typeof(ApiInfo)));
        Bind<ICurrentUserService>().ToMethod(ctx => (ICurrentUserService)this.ParentKernel.GetService(typeof(ICurrentUserService)));
    }
}

The parent kernel is instantiated when the service starts by loading the parent and common modules:

IKernel parent = new StandardKernel();
parent .Load(new NinjectParentModule(_kernel));
parent .Load(new NinjectCommonModule());

And then, within any Task.Run, a child kernel is instantiated:

IKernel taskKernel = new StandardKernel();
taskKernel.Load(new NinjectTaskModule(parent));
taskKernel.Load(new NinjectCommonModule());

This solves the issue of isolating any Thread/Task to have its own scoped objects. When the Task completes, and the “taskKernel” is disposed, then all of the scoped objects get disposed as well.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.