Cú lừa sự tiện lợi
Có bao giờ nhìn vào đoạn code và thấy nó quá bình thường chưa?
builder.Services.AddScoped<IUserService, UserService>(); builder.Services.AddScoped<IUserRepository, UserRepository>();
Sau đó chỉ cần viết:
[ApiController]
[Route("api/users")]
public class UserController : ControllerBase
{
private readonly IUserService _userService;
public UserController(IUserService userService)
{
_userService = userService;
}
}
ASP.NET sẽ tự động inject IUserService vào constructor.
Nhìn qua như quá quen thuộc nhưng nó là "ma thuật framework"
Thử bắt đầu với vài câu hỏi:
- Ai là người tạo
IUserService? - Ai là người tạo
UserRepository? - Object được tạo lúc nào?
- Tại sao
Scopedsống theo request? - Vì sao inject
ScopedvàoSingletonsẽ "nổ" source? - Object được dispose khi nào?
- Framework có dùng Reflection mỗi request không?
Dependency Injection khiến vô tình bỏ qua machinery phía sau. Thực tế, để inject được đúng một dependency, ASP.NET Core phải kéo theo cả một hệ thống build object graph khá phức tạp.
Có thể hình dung như sau:
Request đến
↓
Controller cần dependency
↓
IServiceProvider resolve object
↓
Tạo dependency graph
↓
Cache theo lifetime
↓
Inject vào constructor
↓
Dispose cuối request
"Mổ bụng" từng phần nhé.
AddScoped chưa tạo object nào cả
Nhìn thử đoạn code này
builder.Services.AddScoped<IUserService, UserService>();
Nghĩa là framework đang tạo sẵn một object UserService
Hoàn toàn KHÔNG. Ở thời điểm startup, ASP.NET Core chưa hề tạo object thật. Nó chỉ đang lưu metadata:
- Interface nào map với implement nào
- Lifetime là gì
- Constructor nào cần dùng
- Object nên được cache ra sao
Nói đơn giản hơn:
AddScoped() không tạo object. Nó chỉ đăng ký "công thức chế tạo object" vào DI Container. Giống việc ghi menu vào nhà hàng chứ chưa nấu món ăn.
IServiceProvider - "nhà máy" phía sau ASP.NET Core
Khi request tới Controller, framework mới bắt đầu cần object.
Ví dụ:
public UserController(IUserService service)
{
}
Lúc này ASP.NET Core sẽ chạy tới IServiceProvider và nói:
Tao cần một IUserService
Đây là lúc mà DI Container bắt đầu làm việc.
Nó sẽ:
- Tìm mapping
IUserService->UserService - Đọc constructor của
UserService - Phát hiện
UserServicecần dependency khác - Resolve tiếp Dependency con
- Build toàn bộ object graph
Ví dụ:
public class UserService : IUserService
{
private readonly IUserRepository _userRepository;
private readonly ILogger<UserService> _logger;
public UserService(
IUserRepository userRepository,
ILogger<UserService> logger)
{
}
}
DI Container sẽ tạo:
UserRepositoryILogger<UserService>
Giả sử UserRepository lại cần DbContext, container nó sẽ resolve tiếp.
Đây là lý do Dependency Injection không tạo object. Nó tạo cả cây dependency.
Constructor Injection chỉ là bề nổi
public UserController(IUserService service)
{
}
Ở phía dưới, nó đang làm rất nhiều việc:
UserController
↓
IUserService
↓
IUserRepository
↓
ApplicationDbContext
↓
ILogger
Một constructor có thể kéo theo hàng chục object được tạo ra phía sau.
Đây là một trong những lý do khiến application startup rất chậm hoặc request đầu tiên cực lâu.
Framework nó phải:
- Phân tích constructor
- Resolve dependency
- Build object graph
- Cache delegate
- Quản lý lifetime
Nếu dependency graph quá sâu, việc resolve sẽ tốn chi phí đáng kể.
Lifetime - phần gây "production issue" nhất
ASP.NET Core có 3 lifetime phổ biến:
Transient
builder.Services.AddTransient<IUserService, UserService>();
Mỗi lần resolve -> tạo object mới.
Giải thích đơn giản:
Resolve lần 1 -> object A Resolve lần 2 -> object B Resolve lần 3 -> object C
Transient giống đồ dùng 1 lần, xài xong bỏ.
Scoped
builder.Services.AddScoped<IUserService, UserService>();
Mỗi request (HTTP) sẽ dùng chung một object.
Ví dụ:
Request A -> UserService A Request B -> UserService B
Nhưng bên trong cùng một object:
Controller
↓
Service
↓
Repository
Nếu tất cả dùng cùng DbContext, framework sẽ reuse đúng một instance. Đây là thứ giúp transaction hoạt động nhất quán trong một requestSingleton
builder.Services.AddSingleton<ICacheService, CacheService>();
Object được tạo đúng một lần xuyên suốt ứng dụng.
Nghĩa là:
1000 request
↓
xài chung 1 object
Vì lý do này, rất dễ dính race condition nếu object có mutable state.
Ví dụ:
public class CounterService
{
public int Count = 0;
}
Nếu request cùng sửa Count, dữ liệu có thể sai do chạy song song.
Vì sao inject Scoped vào Singleton lại nổ?
Đây là bug rất phổ biến.
Ví dụ:
public class CacheService
{
public CacheService(ApplicationDbContext context)
{
}
}
Trong khi:
builder.Services.AddSingleton<CacheService>(); builder.Services.AddScoped<ApplicationDbContext>();
Lúc này framework sẽ báo lỗi:
Cannot consume scoped service from singleton
Tại sao?
- Vì
Singletonsống xuyên suốt application - Còn
Scopedchỉ sống trong một request
Nếu giữ Singleton giữ reference tới Scoped object:
Request A kết thúc
↓
DbContext bị dispose
↓
Singleton vẫn giữ reference cũ
Lúc này application có thể giữ object “xác chết” trong bộ nhớ.
DI Container chặn chuyện này ngay từ đầu để tránh bug production cực khó debug.
Bẫy Circular Dependency
Một bug cũng rất “tâm linh”:
UserService cần AuthService AuthService cần TokenService TokenService cần UserService
Container sẽ resolve kiểu:
UserService
↓
AuthService
↓
TokenService
↓
UserService
↓
AuthService
↓
...........
Nó như vòng lặp vô tận.
Và kết quả thường:
- Stack overflow
- Exception lúc startup
- Application fail ngay khi boot
Đây là lý do dependency graph càng phức tạp càng dễ sinh architecture smell.
ASP.NET Core có dùng Reflection mỗi request không?
Có. Nhưng không hoàn toàn theo cách nhiều người nghĩ.
Nếu mỗi request framework đều:
- Scan constructor bằng Reflection
- Parse parameter
- Invoke constructor động
Performance sẽ rất tệ.
ASP.NET Core tối ưu bằng cách:
- Cache constructor metadata
- Compile expression tree
- Build delegate
- Reuse ObjectFactory
Nói cách khác:
Reflection chủ yếu dùng lúc “chuẩn bị”.
Khi request thật sự chạy, framework cố chuyển mọi thứ sang delegate để tăng tốc resolve object. Đây là một trong những lý do DI của ASP.NET Core nhanh hơn nhiều container đời cũ.
Dispose object - phần nhiều người quên mất
DI Container không chỉ tạo object.
Nó còn phải dọn rác đúng thời điểm.
Ví dụ:
public class UserRepository : IDisposable
{
}
Nếu object được tạo theo Scoped, cuối request framework sẽ tự dispose toàn bộ dependency liên quan.
Điều này cực kỳ quan trọng với:
- DbContext
- Database connection
- Stream
- HttpClient handler
- Unmanaged resource
Nếu lifecycle bị quản lý sai:
- Memory leak
- Connection leak
- Socket exhaustion
sẽ xuất hiện rất nhanh ở production.
Kết luận
Nhìn bề ngoài, Dependency Injection chỉ giống:
public UserController(IUserService service)
{
}
Nhưng để inject được đúng object đó, ASP.NET Core phải kéo theo cả một hệ thống:
- Build dependency graph
- Resolve đệ quy
- Cache theo lifetime
- Quản lý scope
- Dispose resource
- Optimize constructor invocation
Thứ trông như “framework tự động new object giúp mình” thực ra là một object factory cực kỳ phức tạp đang vận hành phía dưới mỗi request.
Hiểu được flow này không những giúp code CRUD nhanh hơn. Nó cho thấy thứ quan trọng hơn nhiều:
Khả năng debug bằng tư duy hệ thống
Khi application bắt đầu:
- Inject lỗi
- Memory leak
- Circular dependency
- Object sống sai lifetime
- DbContext bị dispose sớm
- Singleton bị race condition
Lúc này sẽ biết chính xác framework đang “kẹt” ở tầng nào thay vì debug trong tuyệt vọng.
No comments yet. Be the first to comment!