Advanced Dependency Injection with Ninject as an Example

So, we discovered Dependency Injection for ourselves, understood all its advantages and undoubted benefits and began to use it with might and main in our projects. Let's see what else can be done with Dependency Injection using the Ninject library as an example.

For the code to work, we will need to install, in addition to Ninject itself, three more extensions: Ninject.Extensions.Factory, Ninject.Extensions.Interception and Ninject.Extensions.Interception.DynamicProxy. These extensions are available on NuGet with the corresponding identifiers.

Factories


Consider a fairly common situation. There are several repositories in the project that encapsulate working with the database. Let it be UserRepository, CustomerRepository, OrderRepository. In addition, there is a Worker class in the business layer that accesses these repositories. We want to ease dependencies, extract interfaces from repositories and resolve dependencies through a DI container:

    public class Worker
    {
        public Worker(IUserRepository userRepository, ICustomerRepository customerRepository, IOrderRepository orderRepository)
        {
        }
    }


Already at this stage, an alarming bell starts ringing in your head: aren't too many dependencies being implemented in the Worker class? What happens if the Worker has to turn to another couple of repositories? And a still future problem gradually begins to emerge: "littering" the working classes with a huge number of injections.
At the same time, we notice that our repositories belong to one layer, one might even say - to one “family” of classes. (depending on the project, even all repositories may be inherited from one parent class). This is a great opportunity to take advantage of the factory mechanism that Ninject provides.

So, we create the factory interface:

    public interface IRepositoryFactory
    {
        IUserRepository CreateUserRepository();
        ICustomerRepository CreateCustomerRepository();
        IOrderRepository CreateOrderRepository();
    }


and write the implementation of this interface in our NinjectModule:

    public class CommonModule : NinjectModule
    {
        public override void Load()
        {
            Bind().To();
            Bind().To();
            Bind().To();
            Bind().ToFactory();
        }
    }


Please note: we did not create a class that implements IRepositoryFactory! Yes, we don’t need it - Ninject will create it, guided by the following logic: each method of our interface should return a new object of the specified type. If this type can be resolved through the dependencies specified in the NinjectModule, then it will be resolved and created.

The introduction of the factory allows you to replace several dependencies with one:

    public class Worker
    {
        private readonly IRepositoryFactory _repositoryFactory;
        public Worker(IRepositoryFactory repositoryFactory)
        {
            _repositoryFactory = repositoryFactory;
        }
        public void Test()
        {
            var customerRepository = _repositoryFactory.CreateCustomerRepository();
        }
    }


Here you can notice another plus from the use of factories. In the classical resolution of dependencies, the Dependency Injection engine must go through the entire dependency tree and create all instances of all classes that participate in the dependencies. In other words, if the application uses 200 DI classes, then when you try to get an instance of the class that is at the top of the dependency tree, 200 instances of the remaining classes will be created, even if you use 10. In the current scenario, the factory supports lazy loading, . in the above example, only the CustomerRepository will be instantiated and only when the Test method is called.

In addition to reducing the number of dependencies, the factory allows you to conveniently work with the parameters of designers when injecting through the constructor. Add the userName parameter to the UserRepository constructor:

    public class UserRepository : IUserRepository
    {
        public UserRepository(string userName)
        {
        }
    }


and modify the factory interface:

    public interface IRepositoryFactory
    {
        IUserRepository CreateUserRepository(string userName);
        ICustomerRepository CreateCustomerRepository();
        IOrderRepository CreateOrderRepository();
    }


Now, when calling the repository, we can easily pass the parameter to the constructor:

    public class Worker
    {
        private readonly IRepositoryFactory _repositoryFactory;
        public Worker(IRepositoryFactory repositoryFactory)
        {
            _repositoryFactory = repositoryFactory;
        }
        public void TestUser()
        {
            var userRepository = _repositoryFactory.CreateUserRepository("testUser");
        }
    }


Aspects of


Ninject allows you to implement not only injections into data types, but also add additional functionality to methods, that is, introduce aspects. Consider this, again, a fairly common example. Suppose we want to enable automatic logging for some of our methods. Create a log class and select the interface:

    public interface ILogger
    {
        void Log(Exception ex);
    }
    public class Logger : ILogger
    {
        public void Log(Exception ex)
        {
            Console.WriteLine(ex.Message);
        }
    }


Now we indicate how exactly we will modify the necessary methods. To do this, we must implement the IInterceptor interface:

    public class ExceptionInterceptor : IInterceptor
    {
        private readonly ILogger _logger;
        public ExceptionInterceptor(ILogger logger)
        {
            _logger = logger;
        }
        public void Intercept(IInvocation invocation)
        {
            try
            {
                invocation.Proceed();
            }
            catch (Exception ex)
            {
                _logger.Log(ex);
            }
        }
    }


Of course, this is an inferior log, the exception here, in violation of all the canons, does not push further along the stack, but is simply “swallowed”. But for illustration it will do.

The idea here is that a direct method call occurs during invocation.Processed. So, we can add any functionality before and after calling this method. What we are doing is framing the method call in try / catch and throwing an exception (should it happen) in a certain log.

You can enable Intercept for the desired method / methods in several ways, the simplest and most elegant of which is to mark the method with a special attribute. Let's create this attribute. It should inherit from InterceptAttribute and indicate which Intercept to use.

    public class LogExceptionAttribute : InterceptAttribute
    {
        public override IInterceptor CreateInterceptor(IProxyRequest request)
        {
            return request.Context.Kernel.Get();
        }
    }


Finally, we mark with our attribute the desired virtual method. Naturally, if the method is non-virtual, no Interception will occur, because Ninject uses a banal mechanism for inheriting and creating a proxy class with overridden methods:

    public class Worker
    {
        [LogException]
        public virtual void Test()
        {
            throw new Exception("test exception");
        }
    }


In our example, the exception will be caught and displayed on the console. At the same time, since we introduced the logger class into our Interception again through dependency injection, our working class does not even “suspect” the existence of any loggers and other auxiliary tools. All that the aspect implementation introduces in it is the LogException attribute.
At the same time, in our NinjectModule there is dependency resolution only for ILogger, since we again specified permission for ExceptionInterceptor in LogExceptionAttribute:

    public class CommonModule : NinjectModule
    {
        public override void Load()
        {
            Bind().To();
        }
    }

Also popular now: