Hầu hết .NET dev đều biết add service vào DI container. Nhưng rất ít người thực sự hiểu: lifetime nghĩa là gì trong runtime, captive dependency là gì, và khi nào DI container lại trở thành vấn đề.
Mục tiêu
- Hiểu bản chất DI (không phải chỉ cú pháp)
- Nắm vững lifetime: Transient, Scoped, Singleton - thực sự hoạt động thế nào
- Phát hiện Captive Dependency bug (nguy hiểm, khó debug)
- Biết khi nào DI là overkill
- Anti-patterns cần tránh
1) DI là gì - Bản chất 30 giây
DI KHÔNG phải .NET feature. DI là design principle:
"Object không tự tạo dependency, mà nhận từ bên ngoài."
// ❌ Không DI: class tự tạo dependency
class OrderService
{
private readonly EmailService _email = new EmailService(); // tight coupling!
}
// ✅ DI: nhận dependency từ bên ngoài
class OrderService
{
private readonly IEmailService _email;
public OrderService(IEmailService email) => _email = email; // loose coupling
}
.NET DI Container (Microsoft.Extensions.DependencyInjection) chỉ là công cụ tự động hóa việc tạo và inject dependencies. Bạn có thể làm DI hoàn toàn bằng tay (poor man's DI), container chỉ giúp bạn quản lý.
2) Lifetime - Chuyện Thực Sự Xảy Ra Trong Runtime
Transient: Mỗi lần inject = instance MỚI
csharpbuilder.Services.AddTransient<IEmailService, EmailService>();
// Mỗi lần ai đó inject IEmailService → new EmailService()
// OrderService inject → instance A
// NotificationService inject → instance B
// Cùng 1 request → KHÁC instance!
Khi nào dùng: Stateless services, lightweight objects, services cần isolate mỗi consumer.
Cẩn thận: Nếu Transient service tốn resource (DB connection, HTTP client) → tạo/hủy liên tục = chậm + leak!
Scoped: 1 instance PER SCOPE (= per HTTP request trong ASP.NET)
csharpbuilder.Services.AddScoped<IUnitOfWork, UnitOfWork>();
// Request 1: inject IUnitOfWork → instance A
// - OrderController inject → instance A
// - OrderService inject → instance A (CÙNG instance!)
// - SaveChanges() → 1 transaction cho cả request
// Request 2: inject IUnitOfWork → instance B (KHÁC request 1!)
Bản chất: ASP.NET tạo 1 IServiceScope cho mỗi HTTP request. Mọi Scoped service trong request đó share cùng instance.
Khi nào dùng: DbContext (bắt buộc!), Unit of Work, bất cứ gì cần share state trong 1 request.
Singleton: 1 instance DUY NHẤT cho TOÀN BỘ app lifetime
csharpbuilder.Services.AddSingleton<ICacheService, MemoryCacheService>();
// App start → tạo 1 instance
// Mọi request, mọi thread → dùng CÙNG instance đó
// → PHẢI thread-safe!
Khi nào dùng: Caching, configuration, HttpClientFactory, stateless utilities.
Cẩn thận: Singleton + mutable state → race condition, memory leak (state sống mãi).
3) Captive Dependency - Bug Nguy Hiểm Nhất
Captive Dependency: Singleton giữ reference đến Scoped/Transient service → Scoped service "bị giam" trong Singleton, KHÔNG được dispose theo request.
csharp// ❌ BUG: Singleton capture Scoped service
builder.Services.AddSingleton<ICacheService, CacheService>();
builder.Services.AddScoped<AppDbContext>();
public class CacheService : ICacheService
{
private readonly AppDbContext _db; // 💀 Scoped bị giam trong Singleton!
public CacheService(AppDbContext db)
{
_db = db;
// _db này KHÔNG BAO GIỜ được dispose!
// _db này dùng chung cho MỌI request!
// → Data cũ, connection leak, crash!
}
}
Tại sao nguy hiểm?
Timeline:
App start → CacheService (Singleton) được tạo
→ AppDbContext (Scoped) được inject VÀO Singleton
→ "Root scope" DbContext → KHÔNG bao giờ dispose
Request 1: dùng CacheService → DbContext trong CacheService
Request ends → DbContext LẼ RA phải dispose, nhưng Singleton giữ reference → LEAK!
Request 2: cùng DbContext cũ → stale data, connection pool exhaustion
...
Request N: 💀 SqlException: "Timeout expired. Pool exhausted."
Quy tắc: Lifetime phải "tương thích"
Singleton CÓ THỂ inject: Singleton
Singleton KHÔNG được inject: Scoped, Transient (captive dependency!)
Scoped CÓ THỂ inject: Singleton, Scoped
Scoped KHÔNG được inject: Transient có state (captured & shared unexpectedly)
Transient CÓ THỂ inject: Singleton, Scoped, Transient
Phát hiện tự động
csharp// .NET tự detect trong Development mode
builder.Services.AddDbContext<AppDbContext>();
var app = builder.Build();
// ✅ Enable scope validation (mặc định BẬT trong Development)
// Nếu Singleton inject Scoped → EXCEPTION ngay khi resolve!
// appsettings.Development.json:
// "DetailedErrors": true
⚠️ Scope validation chỉ bật trong Development.
Production sẽ KHÔNG throw exception → bug âm thầm!
→ Luôn test DI registration trong Development mode.
Fix: Inject IServiceScopeFactory thay vì service trực tiếp
csharp// ✅ Singleton cần Scoped service? → Tạo scope manually
public class CacheService(IServiceScopeFactory scopeFactory) : ICacheService
{
public async Task<Product?> GetProductAsync(int id)
{
using var scope = scopeFactory.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
return await db.Products.FindAsync(id);
// scope dispose → DbContext dispose → connection trả về pool ✅
}
}
4) Service Locator Anti-Pattern
csharp// ❌ SERVICE LOCATOR: resolve services từ IServiceProvider trực tiếp
public class OrderService
{
private readonly IServiceProvider _sp;
public OrderService(IServiceProvider sp) => _sp = sp;
public void Process()
{
// Dependencies ẩn, không rõ class cần gì
var email = _sp.GetRequiredService<IEmailService>();
var db = _sp.GetRequiredService<AppDbContext>();
}
}
// Vấn đề:
// 1. Dependencies KHÔNG rõ ràng (phải đọc code mới biết)
// 2. Constructor không thể hiện đầy đủ dependencies
// 3. Test khó: phải mock IServiceProvider thay vì từng service
// 4. Runtime error thay vì compile-time error
// ✅ CONSTRUCTOR INJECTION: dependencies rõ ràng
public class OrderService(
IEmailService email,
AppDbContext db)
{
// Constructor nói RÕ RÀNG: "Tôi cần email và db"
// Thiếu → compile error
// Test → mock 2 interfaces
}
Ngoại lệ hợp lý: IServiceScopeFactory trong Singleton (như ở trên), hoặc factory pattern khi cần tạo service dynamically.
5) Khi Nào DI Là Overkill?
csharp// ❌ Over-DI: register MỌI THỨ vào container
builder.Services.AddTransient<IStringHelper, StringHelper>();
builder.Services.AddTransient<IDateTimeHelper, DateTimeHelper>();
builder.Services.AddTransient<IMathHelper, MathHelper>();
// Sai vì: utility class stateless, không có external dependency
// → Dùng static class hoặc extension method
// ✅ Better
public static class StringHelper
{
public static string Slugify(string input) => /* ... */;
}
DI Decision Tree
Class có external dependency (DB, HTTP, file system)?
├── YES → Register vào DI ✅
└── NO
├── Class có state cần manage lifetime?
│ ├── YES → Register vào DI ✅
│ └── NO
│ ├── Class cần mock trong tests?
│ │ ├── YES → Register vào DI ✅ (interface)
│ │ └── NO → Static class / Extension method 👍
│ └──
└──
6) Keyed Services (.NET 8+) - Giải Quyết "Multiple Implementations"
csharp// Vấn đề: 2 implementations cho 1 interface
builder.Services.AddKeyedScoped<INotificationService, EmailNotification>("email");
builder.Services.AddKeyedScoped<INotificationService, SmsNotification>("sms");
// Inject by key
public class OrderService(
[FromKeyedServices("email")] INotificationService emailNotifier,
[FromKeyedServices("sms")] INotificationService smsNotifier)
{
public async Task NotifyAsync(Order order)
{
await emailNotifier.SendAsync(order);
if (order.IsUrgent)
await smsNotifier.SendAsync(order);
}
}
// Trước .NET 8: phải dùng factory pattern hoặc IEnumerable<T>
Master Checklist ✅
□ DbContext/UnitOfWork luôn Scoped?
□ Singleton KHÔNG inject Scoped service trực tiếp?
□ Scope validation bật trong Development?
□ Không dùng Service Locator (inject IServiceProvider rồi GetService)?
□ Constructor dependencies rõ ràng, không quá nhiều (>5 → tách class)?
□ Static/utility class KHÔNG register vào DI?
□ IDisposable services được dispose đúng cách bởi container?
□ HttpClient dùng IHttpClientFactory (không new HttpClient)?
Common Mistakes Recap
| Mistake | Hậu quả | Fix |
|---|---|---|
| Singleton inject Scoped | Connection leak, stale data | IServiceScopeFactory |
| Scoped trong Background Service | Scoped = app lifetime = Singleton | Tạo scope manual |
new HttpClient() thay DI | Socket exhaustion | IHttpClientFactory |
| Quá nhiều Transient | GC pressure, memory churn | Scoped nếu share trong request |
| Service Locator | Hidden deps, untestable | Constructor Injection |
Tư duy master: Khi add service vào DI, hỏi: "Instance này sống bao lâu? Ai share nó? Thread-safe không?" - Trả lời được 3 câu này = hiểu DI.