Inject a dependency into a custom model binder and

2019-01-12 06:32发布

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?

3条回答
欢心
2楼-- · 2019-01-12 07:03

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

查看更多
forever°为你锁心
3楼-- · 2019-01-12 07:05

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

查看更多
beautiful°
4楼-- · 2019-01-12 07:13

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.)

查看更多
登录 后发表回答