Search code examples
c#asp.net-mvcdependency-injectionninject

How to inject a generic interface into MVC controller using Ninject


I have the following situation:

  • an interface:

    public interface ITest<T> where T:class
     {
        void Delete(T item);
     }
    
  • the abstract implementation:

     public abstract class Test<T>:ITest<T> where T:class 
        {
            private readonly ApplicationDbContext _context;
    
            protected Test(ApplicationDbContext context){
            _context=context;
            }
            public void Delete(T item) { }
        }
    
  • final class:

    public class RepoTest:Test<FirstEntity>
    {
       public void DoSomething() { }
    }
    

I have a MVC Controller, which looks like this:

 public abstract class MyController<T>:Controller where T:class 
    {
        private readonly ITest<T> _test;
        protected MyController(ITest<T> test)
        {
            _test = test;
        }
    }

For each entity, I create a controller, inherited from MyController, and base on Entity I want ninject to inject the specific class.

For this I try to use this bindings:

kernel.Bind(typeof(ITest<>)).To(typeof(Test<>)).InRequestScope();

           kernel.Bind(x=>x.FromAssemblyContaining(typeof(Test<>))
           .SelectAllClasses()
           .InheritedFrom(typeof(Test<>))
           .BindToSelf());

Unfortunatly I alwasys got this kind of errors:

Error activating ITest{Tool} No matching bindings are available, and the type is not self-bindable. Activation path: 2) Injection of dependency ITest{Tool} into parameter test of constructor of type ToolsController 1) Request for ToolsController

Suggestions: 1) Ensure that you have defined a binding for ITest{Tool}. 2) If the binding was defined in a module, ensure that the module has been loaded into the kernel. 3) Ensure you have not accidentally created more than one kernel. 4) If you are using constructor arguments, ensure that the parameter name matches the constructors parameter name. 5) If you are using automatic module loading, ensure the search path and filters are correct.

How can I tell to Ninject, to inject the class base on the Entity type?


Solution

  • The code as it is written currently won't work.

    You have two options:

    1. Use generic:

    Because your controller is expecting ITest<T> which is bound to an abstract class Test<T> which can't be instantiated.

    You have to make a concrete but Generic class Test<T> and add a binding for ApplicationDbContext which will automatically work.

    1. Use Reflection to find the right type at binding, e.g.:

    Important!!! remove both of your kernel.Bind() calls.

     // this will find classes which, like RepoTest, are derived from Test<>
    var allDerivedTypes = typeof(Test<>).Assembly.GetExportedTypes().Where(x => x.BaseType.IsGenericType && x.BaseType.GetGenericTypeDefinition() == typeof(Test<>)).ToList();
    
    // ideally, you'd find some way to constrain all your models.
    // what you need for this foreach is all of the entities that can be present in things like RepoTest
    foreach(var t in typeof(Tool).Assembly.GetExportedTypes())
    {
        // For each entity, get a runtime representation of Test<Entity>
        var targetType = typeof(Test<>).MakeGenericType(t);
    
        // Check if there is a class derived from Test<Entity>
        var potentiallyPresentImplementation = allDerivedTypes.FirstOrDefault(x => targetType == x.BaseType); // here you might want to decide how to handle multiple instances of the same generic base
    
        // Found one, so bind it
        if(potentiallyPresentImplementation != null)
        {
            kernel.Bind(targetType ).To(potentiallyPresentImplementation ).InRequestScope();
        }
    }
    

    Note: method 2 is currently assuming that all models and Test<> derivatives are in one assmebly, respecitvely. You'd need to add a little more reflection magic to inspect all referenced assemblies if this is not the case.

    After this, the controller will get RepoTest injected. Although to be honest with you, approach 1. is better :)