One of the most important aspects to consider when developing software is how you will manage dependencies. A dependency is an external component that a piece of software relies on to function properly. For example, a .NET web application may depend on a database server, a message queue, or a third-party Application Programming Interface (API).
A software system can become complicated to maintain and scale without proper dependency management. If you do not manage dependencies properly, they can become tightly coupled to the code that uses them. This coupling can make it difficult to change or update a dependency without breaking the code.
That's where dependency injection (DI) comes in.
What Is Dependency Injection (DI)?
DI is a software development technique for achieving loosely coupled code dependencies between objects and their collaborators, or “dependencies.” When using DI, the responsibility of locating and providing dependencies is removed from an object and given to a third party. Doing so results in greater flexibility and maintainability in software development.
In .NET Core, a built-in DI container manages dependencies between objects. The DI container implements the Inversion of Control (IoC) principle, which states that an object should not be responsible for locating or managing its dependencies. Instead, this responsibility is inverted and given to a third party.
The use of DI containers can result in more modular and testable code. Furthermore, because the container manages the dependencies between objects, it can provide greater flexibility in how dependencies are created and injected into objects.
In addition to the built-in DI container, many other third-party IoC containers are available for .NET Core. These containers can provide additional features and configuration options.
The Occurrence Of Dependency Problems
Dependency problems can occur in any codebase, but they are especially prevalent in tightly coupled code. When code is tightly coupled, the dependencies between objects are very strong.
This tight coupling can make it difficult to change or update one object without affecting other objects in the system.
An Example Of A Dependency Problem
Consider a hypothetical .NET web application that uses a third-party API to fetch data. Suppose the code for this application is tightly coupled to the API. As a result, if the API changes, the code will also need to be updated.
If the API is frequently updated by its developer, this can cause significant problems as it therefore requires constant changes to the code of your .Net web application as well.
Essentially, the need to change your code will depend on whether the API changes, which is outside your control since a third party develops it.
Dependency Inversion Principle In Dependency Injection
The Dependency Inversion Principle (DIP) is a software design principle that states that dependencies should be inverted and that an object should not be responsible for locating or managing its dependencies. Instead, this responsibility should be inverted and given to a third party. Two concepts help to achieve this inversion of control:
High-Level Modules Should Depend on Abstractions
The first concept of the DIP is that high-level modules shouldn’t depend on the low-level modules. A module is high-level if it is responsible for a significant piece of functionality in the application. Conversely, a low-level module is any module that is not high-level, such as a utility class.
Both high-level and low-level modules should depend on abstractions. An abstraction is a supertype that defines the contract for a group of types. In other words, it is an interface or base class that defines what functionality must be present for a type to be considered a member of the group.
By depending on abstractions, high-level modules are not tied to any particular implementation of the low-level modules. This makes it easy to change the implementation of the low-level modules without affecting the high-level modules.
Abstractions Should Not Depend On Details
The DIP states that abstractions should not depend on details. A detail is any concrete implementation of an abstraction. For example, if an abstraction is an interface, then a detail would be a class that implements that interface. Instead, details should be based on abstractions.
This principle is closely related to the previous one in that it ensures that high-level modules are not dependent on low-level modules. However, it takes things one step further by stating that even abstractions should not be dependent on concrete details. By depending on abstractions instead of details, you can make your code more flexible and extensible.
By depending on details, high-level modules become tied to the particular implementation of the low-level modules. As a result, it makes it difficult to change the implementation of the low-level modules without affecting the high-level modules.
By following the Dependency Inversion Principle, dependencies become inverted and are managed by a third party, making it much easier to change the implementation of dependencies without affecting the code that depends on them. Additionally, it makes it possible to unit test modules in isolation from their dependencies.
Why Use Dependency Injection
You should strongly consider using a DI to manage dependencies when developing software. The following are the primary reasons why you should do this:
When dependencies are injected into your code, the code becomes more flexible. This flexibility is the result of being able to swap out the dependencies with different implementations easily.
For example, if you are using a DI container, you can change the configuration to use a different implementation of a dependency. Doing so is much easier than changing the code itself.
Ease In Testing
Another reason to use DI is that it makes unit testing much more manageable because dependencies can be easily mocked or stubbed, which refers to providing fake implementations of the dependencies.
As a result, you can test the code in isolation from the dependencies. Doing so is impossible if the code directly depends on the real implementation of the dependencies.
Easier To Maintain
Additionally, code that uses DI is generally easier to maintain because the dependencies are clearly defined and can be easily changed if necessary. The code is also less likely to break if a dependency changes since the code does not directly use the dependencies. As a result, you'll spend less time fixing broken code.
Code that uses DI is more readable because the dependencies are clearly defined and can be easily understood. It’s easier to understand because the dependencies are injected into the code, rather than being defined within the code, thereby making it clearer what each dependency is for.
Additionally, the code is less likely to be cluttered with unnecessary code.
More Extendable Class Structure
When using DI, the class structure is more extendable. The class structure refers to the organization of classes in the code. For example, if a class depends on another class, the dependent class is said to be a “child class.”
When using DI, you can inject the dependencies into the child classes, making them more extendable. Being more extendable makes adding new functionality to the child classes easier. Doing so is impossible if the dependencies are defined within the child classes.
Facilitates Team Development
DI can also facilitate team development since each team member can work on a different module in isolation. As a result, team members are less likely to get in each other’s way. Additionally, it is easier to integrate the work of different team members since the dependencies are clearly defined.
.NET Core And The Dependency Injection
.NET Core has a built-in IoC container that you can use for DI. This container is based on the Microsoft Common Service Locator library. The Common Service Locator library is a lightweight library that provides a consistent way to locate services.
You can use the IoC container in .NET Core to resolve dependencies in various ways. For example, the container can resolve dependencies by type, name, or convention. Additionally, the container can resolve dependencies lazily, which means the dependency will not be instantiated until it is needed.
The Role Of IoC Containers In Implementation
IoC is a programming principle that states that a class should not depend on another class, thereby allowing them to be loosely coupled. Instead, the dependency should be injected into the class. IoC is also known as DI.
IoC containers are used to implement IoC. A container is simply a class that stores objects and manages their life cycles. The container is responsible for creating objects, injecting dependencies, and disposing of objects when they are no longer needed.
To use an IoC container, you must first register your types with the container, which means you must specify which types the container should instantiate. You can register types by specifying the type of the object to be created, the name of the object, or the convention to be used. There are two ways to register types with the container.
The first way is to use attributes. Attributes decorate your types with metadata. The container uses this metadata to determine how to instantiate and resolve dependencies for your types.
The second way to register types with the container is to use a fluent API. You can use fluent APIs to configure the container using a fluent interface, which is done either in code or in configuration.
Once you register your types with the container, you can use it to resolve dependencies by calling the “GetService” or “GetInstance” methods on the container. These methods return an instance of the requested type. The container is responsible for creating the object and injecting any required dependencies.
The container can dispose of an object when it is no longer needed. You can do this by calling the “Release” method on the container, thereby causing the container to call the “Dispose” method on the object, if it implements IDisposable.
IDisposable is an interface that defines a dispose method. By doing this, you can ensure that your objects are properly disposed of and that any unmanaged resources they are using are properly released.
Creating An IoC Container Of Your Own
If you want to create your own IoC container, you need to start by introducing the dependencies that the container will manage. These dependencies are called services. Once you introduce the services, you can start writing the code for your container. The following are the two types of services that you can introduce:
The first type of service is framework services. These are dependencies that the framework itself requires. For example, the ASP.NET Core framework requires a service to resolve dependencies by type. This service is called the “TypeActivatorCache.”
The second type of service is application services. These are dependencies that your application requires.
For example, your application may require a service to resolve dependencies by name. This service is called the “NameResolver.” Another example of an application service is the “DataAccessor.” This service provides a way to access data from a database.
Service Lifetimes For A Dependency
Each registered dependency can have a different lifetime. The lifetime refers to the amount of time that the container will keep a reference to the object. Once the lifetime expires, the container will dispose of the object.
You can specify three different lifetimes: Transient, Singleton, and Scoped. Choosing which lifetime to use will depend on your application's needs. For example, if your app only needs one instance of a service, then you can use the Singleton lifetime. Doing so will cause the container to create only one instance of the service and keep it for the lifetime of the app.
With that in mind, the following are the three different lifetimes that you can specify for a dependency:
The Transient lifetime will cause the container to create a new instance of the service every time it is resolved. As a result, each time you call GetService or GetInstance, it will create a new object.
For example, if your app has a service that accesses data from a database, you may want to use the Transient lifetime. Doing so will ensure that each time the service is used, it will get the latest data from the database. The Transient lifetime is the default lifetime if one is not specified.
The Singleton lifetime will cause the container to create a single instance of the service and reuse it for every resolution.
As a result, the same object will be returned each time you call GetService or GetInstance. For example, if your app has a service that stores user data, such as preferences, you may want to use the Singleton lifetime. Doing so will ensure that all code uses the same instance of the service and, therefore, the same data.
The Scoped lifetime will cause the container to create a new instance of the service for each request. Suppose your app receives two requests simultaneously. It will then create two instances of the service. However, if your app receives two requests in quick succession, it will only create one instance of the service.
For example, if your app has a service that accesses data from a database, you may want to use the Scoped lifetime. Doing so will ensure that each request gets its own instance of the service. However, if two requests come in quick succession, only one instance of the service will be created, thereby helping improve performance by the reuse of services.
Other Common Approaches To Dependency Injection
There are several other approaches to DI that are worth mentioning. You can use these approaches in addition to or instead of the ones discussed above. Keeping that in mind, the following are three other common approaches to DI:
In object-oriented programming, a constructor is a special type of subroutine called to create an object. Constructor injection is a DI where the dependencies are injected into the constructor of the class, which is then responsible for instantiating the object.
This approach is often used in conjunction with the Singleton and Scoped lifetime services, as these are when the constructor is called.
A method is a function that is associated with a class. A method defines the behavior of an object. When a method is called, it is executed on an object. Method injection is a form of DI where the dependencies are injected into the method of the class, which is then responsible for instantiating the object.
A property is a value that is associated with a class. Property injection is a form of DI where the dependencies are injected into the properties of the class, which is then responsible for instantiating the object. This can be seen as a more "lightweight" form of DI because it does not require the class to have a constructor or method.
Learn How To Use Dependency Injection In .NET Core
DI is a powerful technique that can help you decouple the code in your applications. As a result, it can make your code more maintainable and easier to test. In addition, it can improve the performance of your applications by reusing services.
.NET Core provides good support for DI, and many DI containers are available. As such, .Net Core is an excellent option when choosing a platform for your applications.