Dependency injection trong ASP.NET Core

6 min read

ASP.NET Core hỗ trợ dependency injection (DI) – một kỹ thuật thiết kế phần mềm giúp cải thiện khả năng bảo trì, mở rộng và kiểm thử của ứng dụng bằng cách tách biệt các thành phần phụ thuộc. ASP.NET Core tích hợp sẵn DI, giúp việc quản lý các phụ thuộc trở nên dễ dàng và hiệu quả.

Tổng quan về Dependency Injection

Một phụ thuộc (dependency) là một đối tượng mà có đối tượng khác phụ thuộc vào. Trong ví dụ dưới đây, lớp SampleService phụ thuộc vào lớp SampleLog.

public class SampleService
{
    private readonly SampleLog _sampleLog = new SampleLog();

    public void Start()
    {
        _sampleLog.Log("SampleService started");
    }
}
public class SampleLog
{
    public void Log(string message)
    {
        Console.WriteLine($"[SAMPLE-LOG] Log message: {message}");
    }
}

Lớp SampleService trực tiếp tạo ra một instance của lớp SampleLog. Như vậy, SampleLog là một phụ thuộc của class SampleService. Việc sử dụng những phụ thuộc tương tự với cách trên có thể gây ra một số vấn đề:

  • Nếu muốn thay SampleLog với một cài đặt khác, ta sẽ phải cập nhật lớp SampleService.
  • Nếu SampleLog cũng có các phụ thuộc, ta cũng phải cài đặt, khởi tạo chúng trong lớp SampleService. Trong trường hợp SampleLog có số lượng phụ thuộc lớn, để sử dụng lại lớp này, việc cấu hình các phụ thuộc của nó sẽ rải rác khắp trong app.
  • Cách cài đặt này sẽ gây khó khăn trong việc unit test.

DI giải quyết những vấn đề này thông qua:

  • Sử dụng interface hoặc lớp base để trừu tượng hóa các cài đặt của phụ thuộc.
  • Đăng ký phụ thuộc trong một service container. ASP.NET Core cung cấp một service container được tích hợp sẵn – IServiceProvider. Các phụ thuộc thường được đăng ký vào service container trong file Program.cs.
  • “Tiêm” (inject) phụ thuộc vào phương thức khởi tạo của lớp sử dụng phụ thuộc này. Framework sẽ chịu trách nhiệm khởi tạo instance cho lớp phụ thuộc và giải phóng chúng khi không còn cần thiết.

Theo ví dụ trước đó, ta có thể định nghĩa interface ISampleLog như sau

public interface ISampleLog
{
    void Log(string message);
}

Cập nhật lớp SampleLog để triển khai interface này

public class SampleLog : ISampleLog
{
    public void Log(string message)
    {
        Console.WriteLine($"[SAMPLE-LOG] Log message: {message}");
    }
}

Sau đó đăng ký service ISampleLog vào service container với cài đặt SampleLog

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
builder.Services.AddScoped<ISampleLog, SampleLog>();

var app = builder.Build();

Khi đó, ta có thể sử dụng ISampleLog trong SampleService bằng cách thêm phụ thuộc này vào phương thức khởi tạo

public class SampleService
{
    private readonly ISampleLog _sampleLog;

    public SampleService(ISampleLog sampleLog)
    {
        _sampleLog = sampleLog;
    }

    public void Start()
    {
        _sampleLog.Log("SampleService started");
    }
}

Bằng cách sử dụng DI pattern:

  • Lớp SampleService không sử dụng lớp cài đặt cụ thể SampleLog, mà chỉ phụ thuộc vào interface ISampleLog. Khi đó ta có thể dễ dàng thay đổi cài đặt của ISampleLog mà không cần phải cập nhật SampleService.
  • Lớp SampleService cũng không tạo ra instance của SampleLog, DI container sẽ lo điều này.

Service lifetimes

Trong ASP.NET Core, khi đăng ký các service vào service container, ta có thể cấu hình thời gian tồn tại (lifetime) của một instance của service đó và khi nào nó được tạo mới hay tái sử dụng. Có ba loại sau:

  • Transient
  • Scoped
  • Singleton

Transient

Các service Transient sẽ có instance được tạo mới mỗi lần nó được request từ service container. Để đăng ký một service dưới dạng Transient, ta sử dụng method AddTransient.

builder.Services.AddTransient<ITransientService, TransientService>();

Trong các ứng dụng xử lý request, các service transient sẽ được giải phóng (disposed) khi kết thúc yêu cầu. Thời gian tồn tại này gây ra việc phân bổ tài nguyên cho mỗi yêu cầu, vì các service được tìm ra và khởi tạo mỗi lần.

Scoped

Đối với các ứng dụng web, thời gian tồn tại scoped chỉ ra rằng các service được tạo một lần cho mỗi request từ client. Ta có thể đăng ký các service scoped bằng phương thức AddScoped.

builder.Services.AddScoped<IScopedService, ScopedService>();

Trong các ứng dụng xử lý request, các scoped service sẽ được giải phóng (disposed) khi kết thúc yêu cầu.

Khi sử dụng Entity Framework Core, phương thức mở rộng AddDbContext đăng ký các kiểu DbContext với thời gian tồn tại scoped theo mặc định.

Singleton

Instance của các singleton service được tạo ra vào:

  • Lần đầu tiên service đó được yêu cầu hoặc
  • Bởi developer, bằng cách đưa ra một cài đặt trực tiếp vào service container. Trường hợp này thường ít xảy ra.

Mỗi request tiếp theo với triển khai của service từ service container sẽ sử dụng chung một instance. Để đăng ký một singleton service, ta sử dụng phương thức AddSingleton.

builder.Services.AddSingleton<ISingletonService, SingletonService>();

Các singleton service thường phải được thiết kế để đảm bảo an toàn với luồng (thread safe) và thường được sử dụng trong các stateless service.

Trong các ứng dụng xử lý request, các singleton service sẽ được giải phóng khi ServiceProvider được giải phóng khi ứng dụng tắt. Vì bộ nhớ không được giải phóng cho đến khi ứng dụng tắt, hãy cân nhắc việc sử dụng bộ nhớ với service singleton.

Thiết kế service cho DI

Khi thiết kế các service có thể sử dụng cho DI:

  • Tránh các class, member static, hoặc có lưu giữ trạng thái. Tránh việc tạo trạng thái toàn cục, thay vào đó hãy sử dụng các singleton service.
  • Tránh việc khởi tạo trực tiếp các phụ thuộc trong các service. Việc khởi tạo trực tiếp làm cho mã code của service bị phụ thuộc vào một triển khai cụ thể.
  • Làm cho các lớp service nhỏ gọn, có cấu trúc tốt và dễ kiểm thử.

Nếu một lớp có nhiều phụ thuộc được tiêm vào, đó có thể là dấu hiệu cho thấy lớp đó có quá nhiều trách nhiệm và vi phạm Nguyên tắc Trách nhiệm Đơn (Single Responsibility Principle – SRP). Hãy cố gắng tái cấu trúc lớp bằng cách chuyển một số trách nhiệm của nó vào các lớp mới.

Giải phóng các service

Service container sẽ gọi phương thức Dispose của các kiểu IDisposable mà nó khởi tạo. Các service được quản lý bởi service container  không bao giờ nên được giải phóng bởi developer. Nếu một kiểu hoặc factory được đăng ký là singleton, container sẽ tự động giải phóng singleton.

Kết luận

Dependency Injection (DI) là một phần không thể thiếu trong ASP.NET Core, giúp bạn quản lý các phụ thuộc một cách hiệu quả và linh hoạt. Bằng cách sử dụng DI, bạn có thể tạo ra các ứng dụng dễ bảo trì, dễ kiểm thử và mở rộng. Việc hiểu rõ các loại service lifetimes (Transient, Scoped, Singleton) và cách sử dụng chúng đúng cách sẽ giúp bạn tối ưu hóa hiệu suất và tài nguyên của ứng dụng.

Hy vọng bài viết này đã cung cấp cho bạn cái nhìn tổng quan về Dependency Injection trong ASP.NET Core và cách sử dụng chúng một cách hiệu quả!

Code sample

https://github.com/silver311/aspnet-core-di-sample

Nguồn tham khảo

https://learn.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection?view=aspnetcore-8.0

Avatar photo

Leave a Reply

Your email address will not be published. Required fields are marked *