Before we begin with .NET Core Dependency Injection, it is important to understand the significance of Dependency Injection and why it is necessary.
Click on this link to learn about the DI: Introduction to Dependency Injection (DI).
Now, we will explore the Dependency Inversion Principle (DIP). DIP enables the decoupling of tightly coupled classes, leading to improved reusability and maintainability.
DIP states two fundamental principles:
High-level modules should not depend on low-level modules. Instead, both should depend on abstractions.
Abstractions should not depend on details. Instead, details should depend on abstractions.
For the purpose of this discussion, let's focus on the first principle and delve deeper with an example:
class Foo {
Foo(Car _car){
// something
}
}
In the above code snippet, the class Foo has a direct dependency on the Car class. This tight coupling between the two classes gives rise to two significant issues:
Foo cannot be instantiated with a different type of Car. In other words, if a new car class, such as Sedan, is introduced, Foo cannot be reused for it.
Any changes to the contract of the Car class will directly impact Foo, increasing the burden of maintenance.
To overcome these problems, DIP suggests that the higher-level module, Foo, should not have a direct dependency on the lower-level module, Car. Instead, both modules should depend on an abstraction, such as an interface:
class Foo {
Foo(ICar _car){
// something
}
}
interface ICar {
// contract definition
}
class Car : ICar {
// implementation
}
class Sedan : ICar {
// implementation
}
By introducing a simple abstraction, ICar, Foo becomes compatible with any class that adheres to the contract defined by the abstraction.
This abstraction, achieved through DI, allows for loose coupling and provides the flexibility to substitute different implementations of the ICar interface, thereby enhancing reusability and minimizing the impact of changes in the lower-level modules.
Code Reusability and Flexibility with Dependency Inversion Principle and Dependency Injection
The Dependency Inversion Principle (DIP) is used to improve the code reusability and mitigate the ripple effect when making changes to lower-level classes. However, even when DIP is implemented correctly, the interface only decouples the usage of the lower-level class in the higher-level class and not its instantiation. Therefore, there is still a need to instantiate an implementation of the interface somewhere in the code. This limitation prevents the seamless replacement of the interface implementation with a different one on the fly.
This is where Dependency Injection (DI) comes into play, offering a solution to separate the usage of an instance from its creation. In simple terms, when the DI framework encounters a dependency of a registered service in a class, it takes responsibility for providing a concrete instantiation of that dependency.
Let's consider an example where ICar is registered in the DI framework to provide an instance of Car. In this scenario, the constructor of Foo will always receive a concrete instance of Car during the instantiation of each Foo object. The DI framework automatically handles the creation and injection of the appropriate dependencies, eliminating the need for manual instantiation.
This decoupling of dependencies through DI enables greater flexibility and maintainability. It allows you to easily swap implementations of interfaces without modifying the dependent classes. By relying on the DI framework, you gain the ability to introduce new implementations, extend functionality, and manage dependencies more efficiently.
.Net core Dependency Injection:
.NET Core Dependency Injection has simplified the process of managing dependencies compared to previous versions of the .NET framework. In the past, developers had to configure third-party DI frameworks like Castle Windsor or Autofac. However, in .NET Core, DI is built-in and readily available.
The DI in .NET Core lies within the "Startup" class. Inside this class, there is a method called "ConfigureServices" where developers can register their services and classes with the DI container.
public class Startup {
// ...
public void ConfigureServices(IServiceCollection services)
{
services.AddTransient<ICar, Car>();
}
// ...
}
In the above code, the "ConfigureServices" method is configuring the DI container to register the interface ICar with its corresponding implementation Car as a transient service. This means that every time the DI container resolves a dependency for ICar, it will create a new instance of Car.
By using this built-in DI mechanism in .NET Core, the controller's dependencies can be automatically resolved from the container for each request. This eliminates the need for complex configuration and manual instantiation of dependencies.
Dependency Lifetimes
When registering services in a dependency injection (DI) container, it is important to specify the lifetime of the service. The lifetime definition determines when a new instance of the service is created.
In .NET Core, there are three different lifetime options available:
Transient: With transient lifetime, a new instance of the service is created every time it is requested from the DI container. This means that each consumer class that depends on the service will receive a separate instance. Transient lifetime is suitable for lightweight and stateless services that can be safely instantiated multiple times.
Scoped: Scoped lifetime creates a new instance of the service for each new scope. In the context of a web application, a scope usually corresponds to a new web request. This means that all dependencies resolved within the same scope will receive the same instance of the service. Once the scope is completed, the instances are disposed of. Scoped lifetime is useful for services that require per-request or per-operation state.
Singleton: Singleton lifetime creates a single instance of the service throughout the lifetime of the application. The same instance is shared among all consumer classes that depend on the service. Singleton services are instantiated only once, typically upon the first request, and subsequent requests will receive the same instance. Singleton lifetime is suitable for stateful services that maintain shared state or expensive resources.
When choosing the appropriate lifetime, you should consider the nature of the service, its usage, and the desired behavior. Transient lifetime is ideal for services with no shared state, scoped lifetime for per-request or per-operation state, and singleton lifetime for shared state or expensive resources.
Best Practices
Here are some good practices to keep in mind when working with dependency lifetimes in .NET Core:
Scoped services should be used within a single web request or thread: Scoped services are designed to be used within a specific scope, usually corresponding to a web request. Sharing service scopes across threads can lead to unexpected behavior and should be avoided.
Be cautious with singleton services to avoid memory leaks: Singleton services are created only once and shared throughout the lifetime of the application. However, if a singleton service is not properly disposed of, it can cause memory leaks. It's important to ensure that any resources held by singleton services are released appropriately when they are no longer needed.
Dispose of singleton services when they are no longer used: Since singleton services persist throughout the application's lifespan, it's crucial to release them when they are no longer required. Failing to do so can result in memory usage that accumulates over time.
Consider the implications of transient services: Transient services have a shorter lifespan as they are created each time they are requested. In general, you may not need to worry as much about multi-threading and memory leaks when using transient services. However, it's still important to be mindful of any potential thread-safety issues.
Avoid depending on a transient or scoped service within a singleton service: It's not recommended to have a singleton service depend on a transient or scoped service. When a singleton service injects a transient service, the transient service effectively becomes a singleton instance within that context. This can lead to unexpected behavior and potential issues if the transient service is not designed to support such scenarios. The default DI container in ASP.NET Core will throw exceptions in such cases to help identify and prevent these issues.
Conclusion
DIP insists on creating an abstraction (interface) between a higher-level class and its dependencies. This helps in decoupling the higher-level class from its dependencies so that any change to the lower-level class will not affect the higher-level class. The only piece of code that uses a dependency directly is the one that is responsible for the instantiation an object of the class that implements the interface.
Comments