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ớpSampleService
. - 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ớpSampleService
. Trong trường hợpSampleLog
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 fileProgram.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 interfaceISampleLog
. Khi đó ta có thể dễ dàng thay đổi cài đặt củaISampleLog
mà không cần phải cập nhậtSampleService
. - Lớp
SampleService
cũng không tạo ra instance củaSampleLog
, 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