Explain on your fingers about the Ninject IoC container - how is it used and in general, why is all this necessary?

I read literature, I just can not understand. It is better to see in practice once.

Closed due to the fact that the issue is too common for the participants pavel , αλεχολυτ , Streletz , user207618, Denis Aug 31 '16 at 6:02 .

Please correct the question so that it describes the specific problem with sufficient detail to determine the appropriate answer. Do not ask a few questions at once. See “How to ask a good question?” For clarification. If the question can be reformulated according to the rules set out in the certificate , edit it .

2 answers 2

On the fingers is unlikely to work, the topic is complex. It arose at about the same time when object-oriented languages ​​were used for developing multi-tier applications. A typical multi-tier application consists of three levels: presentation level, domain level and data access level.

In the classical scheme, the relationship between levels extends from top to bottom: the level of representation depends on the level of the subject area, and that, in turn, depends on the level of data access.

+----------------------------+ | Уровень представления | +----------------------------+ \/ +----------------------------+ | Уровень предметной области | +----------------------------+ \/ +----------------------------+ | Уровень доступа к данным | +----------------------------+ 

Consider a simple example with a web application. Let, for example, we have a list of orders that we want to show to the user:

 public OrderController : Controller { [HttpGet] public ActionResult Index() { var userId = GetCurrentUserId(); var repository = new OrderRepository(); var orders = repository.GetAllByUserId(userId); return View(orders); } } 

For rendering we use simple markup:

 @model IEnumerable<Order> @{ ViewBag.Title = "Список заказов"; } <table> <thead> <tr> <th>Номер</th> <th>Дата</th> <th>Сумма</th> </tr> </thead> <tbody> @foreach (var order in Model) { <tr> <td>@order.Number</td> <td>@order.Date</td> <td>@order.Amount</td> </tr> } </tbody> </table> 

This is the presentation level where the end user sees a list of orders on the site. To get this list, we turn to the order repository (this is a domain level class) and we receive orders (also a subject domain). We send the list of these orders to the HTML rendering engine, which will turn them into a web page.

And this is how the code of the data access level looks like if it accesses the data through the Entity Framework:

 public OrderRepository { public IReadOnlyCollection<Order> GetAllByUserId(int userId) { using (var dbContext = new MyDbContext()) { return dbContext.Orders .AsNoTracking() .Where(x => x.UserId == userId) .AsEnumerable() .Select(x => new Order(x.Id, x.Number, x.Date, x.Amount)) .ToArray(); } } } 

We load the raw data from the database, and create from them domain classes that also have behavior.

The data access level classes are, for example, MyDbContext and OrderData . Everything seems to be good and expandable, but.

What you need to go in such an application from the web interface to the window? Ideally, write one new view level, windowed. The two remaining levels will remain the same. Great.

And what do you need to switch from SQL to either MongoDb or file storage in such an application? Rewrite all levels, since the lower one will still have to be rewritten, and after it you will have to rewrite everything that depends on it. Unhealthy.

What can be done to simplify the transfer of such applications to other stores? Invert the dependence, that is, to make the data access level dependent on the domain level.

 +----------------------------+ | Уровень представления | +----------------------------+ \/ +----------------------------+ | Уровень предметной области | +----------------------------+ /\ +----------------------------+ | Уровень доступа к данным | +----------------------------+ 

If we do this, then it turns out that the subject area will become central. The presentation levels (web, console, desktop) and data access level (SQL, MongoDb, XML files) will depend on it. We will be able to expand the application by adding modules at the top and bottom, since they will depend only on the level of the subject area.

The question arises: but then, then there is a dependence on the central level of the subject area? What if we want to rewrite it?

The answer is unexpected: it is the domain that defines the entire application. If it's Word, then things like documents, paragraphs, formatting, and everything else are described in the subject area. In contrast to the previous cases, the task of replacing the subject area is simply meaningless, you get another application.

So, dependency inversion is a useful thing. But how to implement it practically? In practice, we have to describe the abstraction (interface) of access to data. Instead of a specific class OrderRepository , we have an interface:

 public interface IOrderRepository { IReadOnlyCollection<Order> GetByUserId(int userId); } 

This is the domain interface. It is used in the OrderController controller of our MVC application (that is, it is available from the presentation level).

 public OrderController : Controller { private readonly IOrderRepository _orderRepository; public OrderController(IOrderReposiotory orderRepository) { _orderRepository = orderRepository; } [HttpGet] public ActionResult GetOrders() { var userId = GetCurrentUserId(); var orders = _orderRepository.GetAllByUserId(userId); return View(model); } } 

The code differs from the previous one in that it can no longer create an object of the OrderRepository class directly; moreover, this class is completely inaccessible from the controller. We have access only to the abstraction (interface) and expect to get the implementation through the controller constructor.

Someone outside must create an OrderRepository object and pass its instance to our constructor. For now, let's postpone consideration of the question of who this “someone” is and see how the repository is implemented at the data access level.

 public OrderRepository : IOrderRepository { public IReadOnlyCollection<Order> GetAllByUserId(int userId) { using (var dbContext = new MyDbContext()) { return dbContext.Orders .AsNoTracking() .Where(x => x.UserId == userId) .AsEnumerable() .Select(x => new Order(x.Id, x.Number, x.Amount)) .ToArray(); } } } 

Here the interface of one level is implemented on another level. Physically, dependency means that from the project where the OrderRepository implemented should be a reference to the project where the IOrderRepository described and in this case we see that the dependency is inverted: the level of data access depends on the presentation level.

Now, if we want to change the view, we don’t need to change the OrderRepository , it’s enough to implement another, for example, console interface instead of the web interface. If we want to change the implementation from EF to NHibernate, we only need to rewrite the repositories, without touching the rest of the project, for example, controllers.

The question remains: who is linking interfaces and implementations with each other? The same IoC container, in particular, NInject.

In the project where this container is created, all the dependencies converge, so it is called the composition root . What should be done? You need to replace the standard IDependencyResolver from ASP.NET MVC with your implementation based on NInject and register your dependencies in the container. About the implementation is written, for example, here .

Registration is done in the private AddBindings method:

 public class NinjectDependecyResolver : IDependencyResolver { private readonly IKernel kernel; public NinjectDependecyResolver() { kernel = new StandardKernel(); AddBindings(); } public object GetService(Type serviceType) { return kernel.TryGet(serviceType); } public IEnumerable<object> GetServices(Type serviceType) { return kernel.GetAll(serviceType); } void AddBindings() { kernel.Bind<IOrderResult>().To<OrderResult>(); . . . } } 

Next, in Global.asax, we change the object to resolve dependencies:

 protected void Application_Start() { DependencyResolver.SetResolver(new NinjectDepedencyResolver()); } 

After that, when trying to create an OrderController engine will access the NinjectDependencyResolver , which will find that an object is expected in the constructor that implements the IOrderRepository interface. The NInject engine will go through the column of registered objects and find out that this interface implements the OrderRepository class which can be created directly (that is, it has an empty constructor). NInject will create this class and pass the reference to it to the OrderController constructor. This pattern is called embedding through the constructor . The created controller will be transferred to the ASP.NET MVC engine and will process the Index request.

In addition to NInject, there are a large number of IoC containers: Castle Windsor, Autofac, Unity. If desired, you can even handle dependencies manually, which is sometimes done in small projects.

Summarize the main points:

  1. We introduce interfaces between the domain level and the lower levels (classically, this is the data access level, in modern applications there may be more than one).

  2. Objects of the subject area and the higher levels for work use only interfaces, getting them through the constructor. For example, if we need a repository in the controller, we describe the parameter in the constructor and store the passed value in a private variable of the object.

  3. We implement all the interfaces used at the lower levels.

  4. We bring together all the interfaces and their implementations (in classical IoC terminology - register (register) interfaces in the container, in the terminology of NInject - assign (bind) the implementation). All registered objects form a graph of objects .

  5. We use an IoC container to create top-level classes (in the MVC application, these are controllers). To do this, MVC provides the IDependencyResolver interface and the static DependencyResolver.SetResolver method.

  6. The container resolves all intermediate dependencies using the object graph. At the bottom of the graph are primitive classes with empty constructors that are created directly.

  7. All this allows you to transparently implement dependency inversion. Our code gets easier. Unit tests are easier to develop. If necessary, it is also easier to switch from one DBMS to another DBMS or even not a DBMS at all.

    On the fingers (a simple stupid example with a picture and almost no code). About errors and clarifications write - correct.

    To better understand, simply create a test project that will read the same data from files of different formats (csv, json, xml, txt, whatever you like). Or something similar.

    Let me want to write a site. It will display a catalog of some products. Production of only 30 items and it is expected that in the coming year it will be a maximum of 33 items.

    I decided that 30 items can be easily saved in a JSON file. It can be easily edited by hand and there is little in it - no big deal.

    I create a new ASP.Net application. It will CatalogController controller, a CatalogController display, a ProductItem class for a single product, a class for reading the ProductFileReader file ProductFileReader and a class for parsing (it will create a list from the ProductItem) ProductParser .

    Picture:

    Illustration

    And then the boss said that he wants to drag a list of 1C .... We will have to rewrite the code that uses ProductParser and ProductFileReader , because they are no longer suitable. Here, fortunately, all this is used only in the CatalogController . You can, of course, rewrite everything once with the thought that nothing will change exactly (yeah, never).

    Here comes the thought that the controller does not need to call all this. Here you need to get only a list of products and where it will come from for us anyway. That is, you can enter a dependency here. From what? Create an IProductProvider interface and use it in the controller. The interface (more precisely, the class that implements it) will give us a list of products and no matter how it turned out and where it came from - the main thing is that it is in the right place.

    For example, NInject will take care of creating the necessary object for the required interface. And this dependence will be only in one place (in the NInject settings).

     private static void RegisterServices(IKernel kernel) { kernel.Bind<IProductProvider>() .To<ProductProviderFromFile>(); } 

    ProductProviderFromFile will read the file and parse. We can then change to ProductProviderFromDB , which will receive the list from the database.

    Then the block with the controller will look like this:

    Illustration

    Thus, it is not necessary to rewrite the controller if we want to switch to storage in the database.

    The same with unit testing. We need to test only the controller separately from everything, and classes for reading files and parsing are nailed to it and it’s easy not to get rid of them. Here, with the help of NInject, or without it at all, you can simply slip the controller to any other class that implements the IProductProvider interface (it can simply return a manually IProductProvider list of three test products — what difference IProductProvider it make, though empty).

    • So, isn't it easier then to make the IProductProvider interface and implement it in classes? And then just replace the providers? - Ildarik07
    • You do that in DI. Match the interface with the implementation you currently need. If you mean to use an interface without DI, then you will have to write IProductProvider p = new ProductProviderFromFile() everywhere (there will be many such places). To achieve only one such place, you will eventually have to create your own analogue DI - Andrew B
    • I apologize, can I comment on the code in detail? private static void RegisterServices (IKernel kernel) {kernel.Bind <IProductProvider> () .To <ProductProviderFromFile> (); } ProductProviderFromFile is the class to implement from the file? - Ildarik07
    • When a container is connected to a DI project (let it be an NInject), a class is created that configures your application to work with NInject. In this class there is a RegisterServices method for registering services (dependencies). Initially this method is empty. In the example, I register ( Bind ) in it the IProductProvider service and the service provider ( To ) ProductProviderFromFile (read from file). - Andrew B
    • So we don't create the RegisterServices class, are we? Is he already reserved? I'm sorry if I ask too stupid questions? - Ildarik07