Dependency Injection With .NET Core
What happens without dependency Injections
Without dependency injection, the UserService
class is responsible for creating its dependencies, specifically an instance of the UserRepository
class. This approach is known as tight coupling, as the UserService
class directly depends on the concrete implementation of UserRepository
.
Here’s an explanation of the code:
// Without Dependency Injection
public class UserService
{
private UserRepository _userRepository;
public UserService()
{
// creates a new instance of UserRepository.
this._userRepository = new UserRepository();
}
public void AddUser(User user)
{
_userRepository.Save(user);
}
}
Part of the UserRepository
class:
public class UserRepository
{
// Method to save a user to the repository.
public void Save(User user)
{
// Perform the actual save operation
// For simplicity, let's print a message
Console.WriteLine($"User '{user.Name}' saved successfully.");
}
}
The UserService
is tightly coupled to the UserRepository
class. If you decide to change or extend the data access logic, you would need to modify the UserService
class and it becomes challenging to unit test the AddUser
method in isolation. Since the UserService
class creates its own instance of UserRepository
, it's difficult to substitute a mock or test-specific implementation during testing.
As the application grows, managing dependencies manually becomes complex. Dependency Injection helps in managing these dependencies more effectively and promotes a cleaner design.
What is Dependency Injection?
Dependency Injection (DI) is indeed a design pattern that promotes loose coupling instead of tightly coupled ones. It achieves this by allowing them to be injected from external sources rather than being created within the class itself. There are three types of dependency injection such as constructor injection, method injection, and setter injection.
Here are some key advantages related to Dependency Injection:
- Reusability: Because dependencies can be shared across classes, dependency injection allows developers to reuse a code block.
- Simple Testing: Dependency injection allows developers to utilize mock objects to test the application during unit testing methods. Because it delivers accurate results, .NET Core code maintenance can be improved and vulnerabilities can be patched.
- Loose Coupling: DI assists in breaking a class’s dependency on another class. Even if you change one class, it won’t affect the dependent classes and will still produce accurate output.
- Extensibility: Dependency injection makes the program more scalable and adaptable, enabling it to eliminate dependencies without compromising uptime.
What is Inversion of Control (IoC)?
The inversion of Control (IoC) design principle encourages coding against interfaces instead of implementation by inverting the control flow in an application. High-level modules or components of an application should never rely on low-level modules or components. They should rely on abstractions, according to the IoC principle. In this instance, the control flow is reversed or “controlled” by a framework or container rather than a component using a component to control the program flow.
An object shouldn’t create instances of objects it depends on, according to the IoC design principle. Rather, a framework or container should build the dependent items. A technique for achieving Inversion of control (IoC) is called dependency injection (DI), in which a class receives its dependencies from outside sources rather than creating them itself. It eliminates internal dependencies from the implementation by allowing you to inject the dependencies externally. Dependency injection makes it possible for components of an application to be loosely coupled, which makes source code more modular and manageable.
How we can Implement the Dependency Injection
To provide Inversion of Control (IoC) between classes and their dependents, the Dependency Injection (DI) software design pattern is supported by .NET. In the .NET framework, dependencies injection is included as standard, just like the option pattern, logging, and configuration. Let’s see the implementation.
Dependency injection is achieved by following these:
- The use of an interface or base class to abstract the dependency implementation. So, let’s create the
IUserRepository
interface andUserRepository
class which is implemented byIUserRepository
.
public interface IUserRepository
{
void Save(User user);
IEnumerable<User> GetAllUsers();
// Implement the rest of the method here
}
public class UserRepository : IUserRepository
{
// Implementation of Save method from IUserRepository interface.
public void Save(User user)
{
// Perform the actual save operation
// For simplicity, let's print a message
Console.WriteLine($"User '{user.Name}' saved successfully.");
}
}
- Registration of the dependency in a service container. .NET provides a built-in service container, IServiceProvider. Services are typically registered at the app’s start-up and appended to an IServiceCollection. Once all services are added, you use BuildServiceProvider to create the service container.
- Injection of the service into the constructor of the class where it’s used. The framework takes on the responsibility of creating an instance of the dependency and disposing of it when it’s no longer needed.
By using the DI pattern, the UserService
:
- Doesn’t use the concrete type
UserRepository
, only theIUserRepository
interface that implements it. That makes it easy to change the implementation that theUserService
uses without modifying theUserService
. - Doesn’t create an instance of
UserRepository
. The instance is created by the DI container.
This is how the registration of the dependency in a service container happens inside the startup.cs
file.
using DependencyInjection.Example;
HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
builder.Services.AddHostedService<Worker>();
builder.Services.AddSingleton<IUserRepository, UserRepository>();
using IHost host = builder.Build();
host.Run();
Dependency Injection Lifetimes in ASP.NET Core
Services can be registered with one of the following lifetimes:
- Transient
- Scoped
- Singleton
The following sections describe each of the preceding lifetimes. Choose an appropriate lifetime for each registered service.
Transient
Transient lifetime services will be created for each Http request. So, when you register a service with the container using a transient lifetime, a new instance of the service will be created each time you inject the instance. This lifetime works best for lightweight, stateless services. Register transient services with AddTransient.
builder.Services.AddTransient<IUserRepository, UserRepository>();
In apps that process requests, transient services are disposed of at the end of the request.
Scoped
This specifies that only one instance for the entire application will be created. Register scoped services with AddScoped.
In apps that process requests, scoped services are disposed at the end of the request.
builder.Services.AddScoped<IUserRepository, UserRepository>();
When using Entity Framework Core, the AddDbContext extension method registers DbContext
types with a scoped lifetime by default.
Singleton
Singleton lifetime services are created either:
- The first time they’re requested.
- By the developer, when providing an implementation instance directly to the container. This approach is rarely needed.
Every subsequent request of the service implementation from the dependency injection container uses the same instance. If the app requires singleton behavior, allow the service container to manage the service’s lifetime. Don’t implement the singleton design pattern and provide code to dispose of the singleton. Services should never be disposed of by code that resolved the service from the container. If a type or factory is registered as a singleton, the container disposes of the singleton automatically.
Register singleton services with AddSingleton. Singleton services must be thread-safe and are often used in stateless services.
builder.Services.AddSingleton<IUserRepository, UserRepository>();
In apps that process requests, singleton services are disposed of when the ServiceProvider is disposed of on application shutdown. Because memory is not released until the app is shut down, consider memory use with a singleton service.
Dependency Injection and Inversion of Control both make it easier to create loosely linked, adaptable, and maintainable applications. Though DI can make it easier to construct classes with distinct roles, it comes with a lot of complexity and requires some learning before someone can begin utilizing IoC with DI. Furthermore, DI adds a runtime penalty that can be problematic in systems that are sensitive to performance even if it is little and can be ignored in the majority of applications.
See you in the next blog post. Bye Bye🍻🍸❤️❤️