Inject a dependency into a custom model binder and using InRequestScope using Ninject
Asked Answered
K

3

7

I'm using NInject with NInject.Web.Mvc.

To start with, I've created a simple test project in which I want an instance of IPostRepository to be shared between a controller and a custom model binder during the same web request. In my real project, I need this because I'm getting IEntityChangeTracker problems where I effectively have two repositories accessing the same object graph. So to keep my test project simple, I'm just trying to share a dummy repository.

The problem I'm having is that it works on the first request and that's it. The relevant code is below.

NInjectModule:

public class PostRepositoryModule : NinjectModule
{
    public override void Load()
    {
        this.Bind<IPostRepository>().To<PostRepository>().InRequestScope();
    }
}

CustomModelBinder:

public class CustomModelBinder : DefaultModelBinder
{
    [Inject]
    public IPostRepository repository { get; set; }

    public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        repository.Add("Model binder...");

        return base.BindModel(controllerContext, bindingContext);
    }
}

public class HomeController : Controller
{
    private IPostRepository repository;

    public HomeController(IPostRepository repository)
    {
        this.repository = repository;
    }

    public ActionResult Index(string whatever)
    {
        repository.Add("Action...");

        return View(repository.GetList());
    }
}

Global.asax:

protected override void OnApplicationStarted()
{
    AreaRegistration.RegisterAllAreas();

    RegisterGlobalFilters(GlobalFilters.Filters);
    RegisterRoutes(RouteTable.Routes);

    ModelBinders.Binders.Add(typeof(string), kernel.Get<CustomModelBinder>());
}

Doing it this way is actually creating 2 separate instances of IPostRepository rather than the shared instance. There's something here that I'm missing with regards to injecting a dependency into my model binder. My code above is based on the first setup method described in the NInject.Web.Mvc wiki but I have tried both.

When I did use the second method, IPostRepository would be shared only for the very first web request, after which it would default to not sharing the instance. However, when I did get that working, I was using the default DependencyResolver as I couldn't for the life of me figure out how to do the same with NInject (being as the kernel is tucked away in the NInjectMVC3 class). I did that like so:

ModelBinders.Binders.Add(typeof(string),
    DependencyResolver.Current.GetService<CustomModelBinder>());

I suspect the reason this worked the first time only is because this isn't resolving it via NInject, so the lifecycle is really being handled by MVC directly (although that means I have no idea how it's resolving the dependency).

So how do I go about properly registering my model binder and getting NInject to inject the dependency?

Kearse answered 8/2, 2012 at 1:18 Comment(3)
related: More General answer #2900180Tigerish
@RubenBartelink Thanks for the link mate. I'll take that on board and take another look at what I have.Kearse
You're welcome (FYI this tidying and linking is in response to a dup of this question from today)Tigerish
K
4

I eventually managed to solve it with a factory as suggested. However, I just could not figure out how to accomplish this with Ninject.Extensions.Factory which is what I would've preferred. Here is what I ended up with:

The factory interface:

public interface IPostRepositoryFactory
{
    IPostRepository CreatePostRepository();
}

The factory implementation:

public class PostRepositoryFactory : IPostRepositoryFactory
{
    private readonly string key = "PostRepository";

    public IPostRepository CreatePostRepository()
    {
        IPostRepository repository;

        if (HttpContext.Current.Items[key] == null)
        {
            repository = new PostRepository();
            HttpContext.Current.Items.Add(key, repository);
        }
        else
        {
            repository = HttpContext.Current.Items[key] as PostRepository;
        }

        return repository;
    }
}

The Ninject module for the factory:

public class PostRepositoryFactoryModule : NinjectModule
{
    public override void Load()
    {
        this.Bind<IPostRepositoryFactory>().To<PostRepositoryFactory>();
    }
}

The custom model binder:

public class CustomModelBinder : DefaultModelBinder
{
    private IPostRepositoryFactory factory;

    public CustomModelBinder(IPostRepositoryFactory factory)
    {
        this.factory = factory;
    }

    public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        IPostRepository repository = factory.CreatePostRepository();

        repository.Add("Model binder");

        return base.BindModel(controllerContext, bindingContext);
    }
}

The controller:

public class HomeController : Controller
{
    private IPostRepository repository;

    public HomeController(IPostRepositoryFactory factory)
    {
        this.repository = factory.CreatePostRepository();
    }

    public ActionResult Index(string whatever)
    {
        repository.Add("Action method");

        return View(repository.GetList());
    }
}

Global.asax to wire up the custom model binder:

protected override void OnApplicationStarted()
{
    AreaRegistration.RegisterAllAreas();

    RegisterGlobalFilters(GlobalFilters.Filters);
    RegisterRoutes(RouteTable.Routes);

    ModelBinders.Binders.Add(typeof(string), kernel.Get<CustomModelBinder>());
}

Which in my view, gave me the desired output of:

Model binder
Action method

Kearse answered 10/2, 2012 at 17:3 Comment(0)
U
8

The ModelBinders are reused by MVC for multiple requests. This means they have a longer lifecycle than request scope and therefore aren't allowed to depend on objects with the shorter request scope life cycle.

Use a Factory instead to create the IPostRepository for every execution of BindModel

Undervest answered 8/2, 2012 at 2:13 Comment(7)
Thank you for your answer. After reading your article I upgraded to NInject 3.0.0 rc3 in order to use the factory extension. I managed to fix some installation problems and an issue with debugging symbols, but I can no longer even inject dependencies into my controller. I'm getting the standard parameterless constructor exception. After running some unit tests, I've confirmed that the modules are being loaded correctly, so I'm at a loss as to what the problem is. I hope you don't mind, but my code and the two tests are here: pastebin.com/wvNQBFLqKearse
I setup a new MVC application using the default template, added your code and the application runs perfectly. Are you sure you updated everything to Ninject 3.0.0 using "Install-Package Ninject.MVC3 -pre"?Undervest
Hi Remo, thank you so much for checking that for me. I just recreated my project here (explicitly making sure the packages were installed with install-package Ninject.MVC3 -pre) and now I'm getting an exception directly from NInject: pastebin.com/nHJ3qqyM Would it possible for you to upload the project you have so I can take a look?Kearse
Can it be that you are deriving from NinjectHttpApplication AND use App_Start/NinjectWebCommon.cs at the same time?Undervest
Bah, that was it. I forgot it creates the folder when you install the packages. Sorry about that. That's working now but I still can't seem to share my repository instance. I read your article a second time again and noticed a couple of things. This part especially: "This tells Ninject that it shall automatically implement a factory class for the specified interface and inject a new instance of this factory into each object that request an instance of the factory interface." That means I still don't know how to share the repository between the binder and the controller.Kearse
So far I've tried moving the factory.CreatePostRepository() call to BindModel to create the repository. I then tried changing the controller to accept a factory dependency to also create a repository there and then changing the factory to: ToFactory().InRequestScope(). Finally, I tried to use Lazy<T> but I realised that doesn't make sense as that goes back to what you said about the model binder lifecycle.Kearse
I confirmed using NHProf that the ninject factory does in fact share a single instance in request scope correctly.Unrivaled
U
5

It's actually really simple to get the Ninject factory extension up and running, but that wasn't clear to me from the existing answers.

The factory extensions plugin is a prerequisite, which can be installed via NUGet:

Install-Package Ninject.Extensions.Factory

You just need the factory injected into your model binder somewhere, eg:

private IPostRepositoryFactory _factory;

public CustomModelBinder(IPostRepositoryFactory factory) {
    _factory = factory;
}

Then create an interface for the factory. The name of the factory and the name of the method doesn't actually matter at all, just the return type. (Good to know if you want to inject an NHibernate session but don't want to have to worry about referencing the correct namespace for ISessionFactory, also useful to know if GetCurrentRepository makes what it actually does more clear in context):

public interface IPostRepositoryFactory { 
    IPostRepository CreatePostRepository();
}

Then, assuming your IPostRepository is already being managed by Ninject correctly, the extension will do everything else for you just by calling the .ToFactory() method.

kernel.Bind<IPostRepository().To<PostRepository>();
kernel.Bind<IPostRepositoryFactory>().ToFactory();

Then you just call your factory method in the code where you need it:

var repo = _factory.CreatePostRepository();
repo.DoStuff();

(Update: Apparently naming your factory function GetXXX will actually fail if the service doesn't already exist in the session. So you do actually have to be somewhat careful with what you name the method.)

Unrivaled answered 8/10, 2012 at 19:46 Comment(1)
Thanks for posting this. Although the project is now finished, and deployed, I really appreciate you adding this answer. It's something I'll refer back to in the future.Kearse
K
4

I eventually managed to solve it with a factory as suggested. However, I just could not figure out how to accomplish this with Ninject.Extensions.Factory which is what I would've preferred. Here is what I ended up with:

The factory interface:

public interface IPostRepositoryFactory
{
    IPostRepository CreatePostRepository();
}

The factory implementation:

public class PostRepositoryFactory : IPostRepositoryFactory
{
    private readonly string key = "PostRepository";

    public IPostRepository CreatePostRepository()
    {
        IPostRepository repository;

        if (HttpContext.Current.Items[key] == null)
        {
            repository = new PostRepository();
            HttpContext.Current.Items.Add(key, repository);
        }
        else
        {
            repository = HttpContext.Current.Items[key] as PostRepository;
        }

        return repository;
    }
}

The Ninject module for the factory:

public class PostRepositoryFactoryModule : NinjectModule
{
    public override void Load()
    {
        this.Bind<IPostRepositoryFactory>().To<PostRepositoryFactory>();
    }
}

The custom model binder:

public class CustomModelBinder : DefaultModelBinder
{
    private IPostRepositoryFactory factory;

    public CustomModelBinder(IPostRepositoryFactory factory)
    {
        this.factory = factory;
    }

    public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        IPostRepository repository = factory.CreatePostRepository();

        repository.Add("Model binder");

        return base.BindModel(controllerContext, bindingContext);
    }
}

The controller:

public class HomeController : Controller
{
    private IPostRepository repository;

    public HomeController(IPostRepositoryFactory factory)
    {
        this.repository = factory.CreatePostRepository();
    }

    public ActionResult Index(string whatever)
    {
        repository.Add("Action method");

        return View(repository.GetList());
    }
}

Global.asax to wire up the custom model binder:

protected override void OnApplicationStarted()
{
    AreaRegistration.RegisterAllAreas();

    RegisterGlobalFilters(GlobalFilters.Filters);
    RegisterRoutes(RouteTable.Routes);

    ModelBinders.Binders.Add(typeof(string), kernel.Get<CustomModelBinder>());
}

Which in my view, gave me the desired output of:

Model binder
Action method

Kearse answered 10/2, 2012 at 17:3 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.