Search code examples
asp.net-mvcdependency-injectionninjectmodel-bindingcustom-model-binder

Inject a dependency into a custom model binder and using InRequestScope using Ninject


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?


Solution

  • 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