The IoC Container Lifecycle
Inversion of Control (IoC) is a software design principle responsible for the lifecycle of objects throughout the application. The mission of IoC is to manage the instances used in the application and to reduce dependency. Therefore, the Inversion of Control principle enables the design of loosely coupled classes that make them maintainable, testable, and extensible.
There are diverse libraries to implement IoC. The most widespread ones are:
Castle Windsor, Ninject, Structure Map, Autofac, LightInject, DryIoc
Each library mentioned above is a third-party framework that equips IoC structuring in .NET applications. Especially, Autofac and Ninject are two of the most habituated and used containers for .NET that Microsoft does not develop. However, ASP.NET Core architecture comprises an IoC container module as built-in (Microsoft’s IoC Container in .NET Core) for automatic dependency injection. One of the ways to apply IoC is to implement dependency injection based on the dependency inversion principle. Therefore, discussing these concepts and their brings for this article, which aims to scrutinize IoC and its lifecycles in detail, is essential.
Dependency Inversion
Dependency inversion (DI), a software design principle, is significant for better comprehending the IoC container. The DI design principle advocates that high-level modules should be independent of low-level modules. For instance, domain logic should not depend on how the code that ties to our database works. This section will describe the significance of the relevant design principle based on the scenario where the Dependency Inversion design principle is applied and not applied.
Scenario 1
This drawing illustrates the scenario where the dependency inversion design principle does not apply.
According to the DI design principle, class A is the high-level module, class B is a lower-level module, and class C is an even lower-level module. All these classes directly depend on the class or classes they need. As can be noticed from this image, in such a case, class A is dependent on class B, and class B is dependent on class C. When class B changes, class A also has to change. The same goes for a change to class C, which might affect class B and class A. The typical approach to stemming this case in an object-oriented application is utilizing interfaces.
Scenario 2
This drawing narrates a scenario in which embodies the dependency inversion design principle.
We have class A, whereas this time, it leans on interface B instead of directly on class B. Class B then implements this interface. The same goes for the outgoing dependency of class B. And then interface C, which is implemented in class C. In the case of Scenario 2, the arrows do not consist of continuous lines as in Scenario 1. Because in Scenario 2, the relationship established with interfaces comes into play.
To give a more concrete example of this case, we will need to modify many sections in our code to work with “Dapper” instead of “Entity Framework Core (EF Core)” when the project we are developing requires it. Because if we do not operate dependency injection in the business layer, we should alter all the service classes that created the EF Core class with the “new” operator. This case conflicts with the Open-Closed Principle (OCP), the second SOLID principle. However, when we utilize DI, we will only be able to revise the implementation type of the relevant interface from EF Core to Dapper ORM in the configuration section since we will perform operations over interfaces. This kind of work brings loose coupling relationships.
As a result, establishing a loose coupling (decoupling from concrete types) rather than direct dependency or tight coupling prevents radical changes and makes classes more testable.
Dependency Injection
One of the methods to practically implement dependency inversion, which is the last of the SOLI(D) principles and corresponds to the letter D, is “Dependency Injection (DI).”
· What is Dependency Injection?
Dependency injection (DI) is an understanding that argues that if we require an object of another class at any point in any class, we should avoid creating the object of that class with the “new” operator. For instance, we need class B inside class A. When we create the B object with the “new” operator inside class A, in this case, considering the dependency injection design principle, we should underline that class A has a dependency on class B. We need to inject the relevant object outside to eliminate a class’s dependency on another class. Dependency injection endorses that if we need another class inside a class, we should resolve it from the constructor rather than creating this class by renewing it.
How Does Inversion of Control Work?
Built-in IoC Container
Built-in IoC Container can take objects to be placed in three different behaviors/lifecycles. These lifecycles or lifetimes are singleton, scoped, and transient. This section of the article will examine these three IoC lifecycles and the issue of determining an appropriate lifecycle.
Singleton
A new instance is created once and reused from then onward. Singleton lifecycle is utilized when we always want to return the same instance whenever a type is requested. In a nutshell, the container will instantiate the type the first time it is requested and keep returning the same instance as long as the application is up and running. For instance, there are 500 requests, and IoC Container instantiates the same instance for these 500 requests.
Scoped
A new instance is formed once per request scope and then reused in the scope. The container will create one instance per the request scope. Scoped lifecycle is utilized for generating an object per request and gives that object to all requests during that request scope. For example, if there are 500 requests, the IoC Container will instantiate 500 instances to these 500 requests.
Transient
In the Transient lifecycle, resolving the same type multiple times, a new instance will be returned every time. A new instance is created every time a type is requested. The container will invariably instantiate a new class whenever a type is requested. For example, IoC Container instantiates 500*n instances in response to 500 requests, and the value of “n” depends on how often the class is resolved by the dependency injection.
Determining the Optimum IoC Lifecycle
As can be comprehended from the explanations and samples above, the lifecycle determines whether the IoC Container will generate a new instance. Considering these three lifecycles, the question of which one to use may come to mind. However, giving a precise answer to this challenging question takes work. Because whatever lifecycle can handle the current situation, we should utilize that lifecycle. Thus, it ultimately depends on the necessity and the current case.
Example: Entity Framework Core DbContext vs. CosmosClient
When considering Entity Framework Core DbContext, the lifecycle should correspond to the unit of work (transaction), in practice, the Scope.
CosmosDb does not endorse traditional transactions; it provides a single thread-safe client and comes with recommendations. It is thread-safe and intended for reuse. A single instance is recommended; hence, Singleton.
Working with an IoC Container
We can divide working with an IoC container into phases. We can classify these phases as registration and resolution. In this piece of the article, with sample images from my project, how to implement the IoC Container and its two crucial phases will be scrutinized in depth.
The Registration Phase
· We register specific types in the container. Thus, it knows their presence and when to construct them. This stage is called the registration phase.
· Choosing a lifecycle: We can determine an IoC lifecycle during the registration. Thus, choosing a lifecycle is another step to take at this phase.
Registering Types
When registering types, we should determine the lifetime, the requested service type, and the implementing type. Here, I will explain the registering types with a sample from the project I developed with ASP.NET Core 6.0:
Second, we call the method, which begins with “Add.” Also, we need to choose one of the Transient, Singleton, and Scoped lifecycles. As seen from the image below, in my project, I preferred “Scoped” as the IoC lifecycle for this registration. Thus, we need to call one of the AddSingleton, AddTransient, and AddScoped methods.
Third, once we have determined the lifecycle, we need to add generic parameters (interface and implementation type), because registering a type should include the interface and implementing type.
This line narrates the container: whenever somebody asks for an “IProductReadRepository,” give them a “ProductReadRepository.”
The Resolution Phase
· The container is liable for instantiating types and providing them when requested. We do not need to create objects manually when utilizing the IoC Container. The container does it for us. In a nutshell, when resolving a type, we demand an instance of a service type. The container will find the implementing type, instantiate it as needed, and yield it to us. This stage is called the resolution phase.
· This part of the article examines two different resolution approaches with illustrations from my project to understand the resolution phase better. One is to inject from the constructor, and the other is to resolve with the “GetService” method using the “IServiceProvider” interface.
As seen in the code snippet below, I injected the interfaces from the constructor.
The IServiceProvider interface is implemented by a class or value type that ensures service to other objects. The IServiceProvider is responsible for resolving instances of types at runtime, as required by the application. The implementation of IServiceProvider is designed to act very efficiently so that the resolution of services is fast.
As seen in the code snippet below, we can see that the “GetService” method of IServiceProvider is used to get the service instance.
In conclusion, based on the dependency inversion principle, this article clarifies the IoC Container and lifecycles based on the dependency injection design pattern, which is how to implement the dependency inversion principle in practice. The IoC container provides instances when needed and manages the service lifecycle. When we need the related instances in the container, we can request the instances inside the container. Combining dependency injection and IoC Container, we can build classes decoupled from other classes used through interfaces and hand over control to a framework for connecting interfaces to classes. Thus, the control is inverted. However, when inverting control, we permit a framework to determine which piece of code runs next.
You can visit my GitHub if you want to check out my code snippets.
Link: https://github.com/busratgl
References
Chauhan, S. (2022, August 22). What is IOC container or di container. Live Training, Prepare for Interviews, and Get Hired. Retrieved November 5, 2022, from https://www.dotnettricks.com/learn/dependencyinjection/what-is-ioc-container-or-di-container
Janssen, T. (2020, April 28). Solid design principles explained: Dependency inversion principle with code examples. Stackify. Retrieved November 10, 2022, from https://stackify.com/dependency-inversion-principle/
Rick-Anderson. (n.d.). Dependency injection in ASP.NET Core. Microsoft Learn. Retrieved November 5, 2022, from https://learn.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection?view=aspnetcore-7.0