ASP.NET Core Essential: Dependency Injection

ASP.NET Core Essential: Dependency Injection

Dependency Injection (DI) is a software design pattern that helps in creating loosely coupled systems by reducing the dependency of an object on its external services. In ASP.NET Core, DI is an integral part of the framework, making it easier to manage object lifetimes, improve code reusability, and support testability by injecting dependencies through constructor injection, property injection, or method injection.

In this article, we'll dive deep into the concept of Dependency Injection in ASP.NET Core, explaining how it works, what the DI container is, and the different service lifetimes in DI.


How Dependency Injection Works in ASP.NET Core

1. Understanding Dependencies and Services

A dependency is any object that another object requires. For instance, if class A requires an instance of class B to function, B is a dependency of A.

In ASP.NET Core, dependencies are typically referred to as services. These services are injected into classes (or consumers) that need them, instead of being instantiated within those classes. This approach decouples the object creation logic from the business logic, adhering to the Single Responsibility Principle.

2. Dependency Injection Workflow

The workflow of Dependency Injection in ASP.NET Core consists of the following steps:

1. Service Registration: In the Startup.cs file, services are registered into Dependency Injection Container (or DI Container) using the ConfigureServices method and. ASP.NET Core uses a ServiceCollection to store these service registrations. For example, you might register a service like this:

public void ConfigureServices(IServiceCollection services)
{
    services.AddScoped<IMyService, MyService>();
}        

Here, IMyService is the interface, and MyService is the implementation. The framework will inject MyService wherever IMyService is needed.

2. Service Resolution: When a service is required by a class (for example, a controller), the framework uses the registered services from the DI container to resolve the required dependencies. This is done via constructor injection

public class MyController : Controller
{
    private readonly IMyService _myService;

    public MyController(IMyService myService)
    {
        _myService = myService;
    }

    public IActionResult Index()
    {
        // Use the injected service
        _myService.DoSomething();
        return View();
    }
}        

In this example, the IMyService interface is injected into the constructor of MyController, and ASP.NET Core ensures that an instance of MyService is provided when the controller is created.

3. Service Disposal: ASP.NET Core also manages the lifetime of the services. It disposes of services when they are no longer needed via the IDispose Interface. For example, a scoped service is disposed of when the HTTP request ends. This is especially useful for managing resources like database connections.


What is a DI Container?

A Dependency Injection Container (or DI container) is a framework component that manages the life cycles of dependencies and facilitates the process of injecting dependencies into the consumers. In ASP.NET Core, the DI container is responsible for:

  1. Registering Services: Services and their corresponding implementations are registered during the application startup in the ConfigureServices method.
  2. Resolving Dependencies: When a service is needed (in a controller, for example), the container automatically provides the appropriate instance of the service, ensuring all required dependencies are injected.
  3. Managing Lifetimes: The DI container manages the lifetime of services, ensuring that services are created and disposed of appropriately based on their registration type (transient, scoped, or singleton).

The ASP.NET Core DI container is built-in and follows the convention over configuration principle, meaning it automatically resolves dependencies based on their registered type and interface.


Service Lifetimes in ASP.NET Core

Along with different types of injection, the DI container supports different service lifetimes to control the lifecycle of services:


1. Transient: For Lightweight and Stateless Operations

Transient objects are always different; a new instance is provided to every controller and every service. Each service instance is short-lived and stateless, with no need to retain any data or context from previous uses. Also, services are lightweight, so creating multiple instances does not introduce significant overhead.

Transient services can only be injected into other Transient services.

Scenarios:

  • Logging Service: A service that logs events or actions without holding any state between requests. Each time a log action occurs, a new instance of the logging service is created, ensuring no state is carried over between operations.
  • Form Validation Service: Used to validate user input or forms. Since form validation logic is stateless and does not need to remember previous interactions, it is recreated for every request.
  • Email Sending Service: A service that generates and sends emails. Every email is an independent action, so creating a new instance each time ensures separation between email operations.

2. Scoped: For Per-Request Operations

Scoped objects are the same within a request, but different across different requests. We use Scoped services when Services should be shared across components handling a single request but should not persist beyond the request. Scoped services ensure that all parts of the system working on a single HTTP request share the same instance.

Scoped Services can be injected into Scoped and Transient services.

Scenarios:

  • Database Context (DbContext): In applications using Entity Framework Core, the DbContext is typically registered as a scoped service. It needs to share the same instance within the same HTTP request to ensure consistent database transactions, but a new instance is created for each new request.
  • Unit of Work Pattern: A service managing a set of operations (like data access) in a single transaction. It should have a single scope per request so that changes made to entities are tracked and can be committed as a single transaction at the end of the request.
  • Shopping Cart Service: A service that tracks the items a user adds to their shopping cart during an HTTP request. The state needs to be maintained for the duration of the request but not beyond it.

3. Singleton: For Shared Global State

Singleton objects are the same for every object and every request. We use Singleton when The service needs to maintain a global state or resource that can be shared across the entire application. These are efficient when initialization is expensive or the service holds resources (e.g., cache, configuration) that should be reused globally and thread-safe, long-lived operations where the service should not be recreated unnecessarily are things we need to consider.

Singleton services can be injected into all 3 types.

Scenarios:

  • Configuration Service: A service that loads and holds application configuration settings in memory. These settings are typically needed throughout the application's lifetime, and reloading them on every request would be inefficient.
  • Caching Service: A service that caches frequently accessed data. Since caching is application-wide, having a single instance of the cache available globally ensures data is stored and retrieved consistently across the application.
  • Third-party API Client: A client that communicates with a third-party service (e.g., an external payment gateway). Reusing a single instance of the API client across the application's lifetime avoids overhead related to repeatedly setting up connections.


If you want to read more, check out these links!

https://learn.microsoft.com/dotnet/core/extensions/dependency-injection-usage

https://stackoverflow.com/questions/38138100/addtransient-addscoped-and-addsingleton-services-differences

That is all for today, thanks for reading!

要查看或添加评论,请登录

Tran Anh的更多文章

社区洞察

其他会员也浏览了