⬆️ ⬇️

Advanced Dependency Injection on the example of Ninject

So, we discovered Dependency Injection, understood all its advantages and undoubted benefits, and began to use it in our projects. Let's see what else you can do with Dependency Injection using the example of the Ninject library.



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



Factories



Consider a fairly frequent situation. The project has several repositories that encapsulate the work with the database. Let it be UserRepository, CustomerRepository, OrderRepository. In addition, in the business layer there is a class Worker, which accesses these repositories. We want to weaken dependencies, select 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 to ring in my head: aren't there too many dependencies being introduced into the Worker class? What happens if the Worker has to contact a couple of repositories? And the future problem is still beginning to emerge: the “littering” of the working classes with a huge number of injections.

At the same time, we notice that our repositories belong to the same layer, one can even say - to one “family” of classes. (depending on the project, perhaps even all repositories are inherited from the same parent class). This is a great opportunity to take advantage of the factories 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<IUserRepository>().To<UserRepository>(); Bind<ICustomerRepository>().To<CustomerRepository>(); Bind<IOrderRepository>().To<OrderRepository>(); Bind<IRepositoryFactory>().ToFactory(); } } 




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



Implementing a 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 see another plus from the use of factories. With classic dependency resolution, Dependency Injection must go through the entire dependency tree and create all instances of all classes that participate in the dependencies. In other words, if in an application of 200 classes they use DI, then when trying to get an instance of a class that is at the top of the dependency tree, 200 instances of the other classes will be created, even if 10 are used in the current scenario 10. The factory supports lazy loading, that is . in the example above, an instance of only a CustomerRepository will be created and only when calling the Test method.



In addition to reducing the number of dependencies, the factory allows you to conveniently work with the parameters of the designers during the injection through the designer. 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 we call 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



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



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




Now we will specify how exactly we will modify the necessary methods. To do this, we need to 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 incomplete log, an exception here, in violation of all canons, is not forwarded further down the stack, but tritely “swallowed”. But for an illustration fit.



The idea here is that the 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 putting an exception (if it happens) to some 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 be inherited from the InterceptAttribute and specify which Intercept to use.



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




Finally, we mark our attribute with the desired virtual method. Naturally, if the method is non-virtual, no Interception will occur, because Ninject uses the banal mechanism of inheritance 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 intercepted and output to the console. At the same time, since we introduced the logger class to our Interception again through dependency injection, our working class doesn’t even “guess” about the existence of some loggers and other auxiliary tools. Everything that the aspect implementation introduces in it is the LogException attribute.

At the same time, in our NinjectModule there is dependency resolution only for the ILogger, since we again specified the resolution for the ExceptionInterceptor in the LogExceptionAttribute:



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

Source: https://habr.com/ru/post/235995/



All Articles