Dependency Injection
3 Loại Lifecycle
Singleton
// Một instance duy nhất cho toàn bộ application lifetime
builder.Services.AddSingleton<IEmailService, EmailService>();
// Ví dụ sử dụng
public class ProductService
{
private readonly IEmailService _emailService;
public ProductService(IEmailService emailService)
{
_emailService = emailService; // Cùng instance xuyên suốt
}
}
Khi nào dùng:
- Configuration services
- Logger (ILogger
) - Caching services
- Services không có state hoặc share state toàn cục
Scoped
// Một instance per HTTP request
builder.Services.AddScoped<IProductRepository, ProductRepository>();
// Ví dụ sử dụng
public class OrderService
{
private readonly IProductRepository _productRepo;
public OrderService(IProductRepository productRepo)
{
_productRepo = productRepo; // Cùng instance trong 1 request
}
}
Khi nào dùng:
- DbContext (EF Core)
- Services cần per-request state
- Business logic services
Transient
// Instance mới mỗi lần được yêu cầu
builder.Services.AddTransient<IReportGenerator, ReportGenerator>();
// Ví dụ sử dụng
public class DashboardService
{
private readonly IReportGenerator _reportGenerator;
public DashboardService(IReportGenerator reportGenerator)
{
_reportGenerator = reportGenerator; // Instance mới mỗi lần
}
}
Khi nào dùng:
- Lightweight, stateless services
- Services với expensive initialization
- Khi cần instance riêng biệt cho mỗi lần sử dụng
Ví dụ thực tế
// Program.cs
var builder = WebApplication.CreateBuilder(args);
// Singleton - Configuration, Logger
builder.Services.AddSingleton<IConfiguration>(builder.Configuration);
builder.Services.AddSingleton<ILogger<Program>, Logger<Program>>();
// Scoped - DbContext, Repositories
builder.Services.AddScoped<AppDbContext>();
builder.Services.AddScoped<IProductRepository, ProductRepository>();
builder.Services.AddScoped<IOrderRepository, OrderRepository>();
// Transient - Small, stateless services
builder.Services.AddTransient<IEmailSender, EmailSender>();
builder.Services.AddTransient<IDateTimeProvider, DateTimeProvider>();
var app = builder.Build();
Lifecycle Diagram
┌─────────────────────────────────────────────────────────────────┐
│ APPLICATION LIFETIME │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ SINGLETON │ │
│ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │
│ │ │ Request │ │ Request │ │ Request │ │ Request │ │ │
│ │ │ 1 │ │ 2 │ │ 3 │ │ 4 │ │ │
│ │ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │ │
│ │ │ │ │ │ │ │
│ │ └────────────┴────────────┴────────────┘ │ │
│ │ (Cùng một instance cho tất cả requests) │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ SCOPED │ │
│ │ ┌─────────┐ ┌─────────┐ │ │
│ │ │ Request │ │ Request │ │ │
│ │ │ 1 │ │ 2 │ │ │
│ │ │ ┌─────┐ │ │ ┌─────┐ │ │ │
│ │ │ │ Svc │ │ │ │ Svc │ │ │ │
│ │ │ └─────┘ │ │ └─────┘ │ │ │
│ │ └─────────┘ └─────────┘ │ │
│ │ (Instance mới cho mỗi request, reuse trong request) │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ TRANSIENT │ │
│ │ ┌────┐ ┌────┐ ┌────┐ ┌────┐ ┌────┐ ┌────┐ │ │
│ │ │ S1 │ │ S2 │ │ S3 │ │ S4 │ │ S5 │ │ S6 │ │ │
│ │ └────┘ └────┘ └────┘ └────┘ └────┘ └────┘ │ │
│ │ (Instance mới mỗi lần được request) │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
Anti-Pattern: Scoped vào Singleton
Tại sao KHÔNG NÊN?
// ❌ BAD - Injecting Scoped service vào Singleton
builder.Services.AddSingleton<BadSingletonService>();
public class BadSingletonService
{
private readonly AppDbContext _context;
public BadSingletonService(AppDbContext context) // Scoped!
{
_context = context; // Sẽ gây lỗi!
}
}
Vấn đề: Singleton tồn tại xuyên suốt application lifetime, nhưng Scoped service (DbContext) chỉ valid trong một HTTP request. Khi request kết thúc, DbContext bị disposed nhưng Singleton vẫn giữ reference, gây ObjectDisposedException.
Giải pháp
// ✅ GOOD - Inject IServiceProvider vào Singleton
builder.Services.AddSingleton<GoodSingletonService>();
public class GoodSingletonService
{
private readonly IServiceProvider _serviceProvider;
public GoodSingletonService(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public void DoWork()
{
// Tạo scope mới để resolve scoped services
using var scope = _serviceProvider.CreateScope();
var context = scope.ServiceProvider.GetRequiredService<AppDbContext>();
// Sử dụng context trong scope
var data = context.Products.ToList();
}
}
Best Practices
-
Tuân thủ Service Lifetime Hierarchy:
- ✅ Singleton → Singleton
- ✅ Singleton → Scoped (thông qua IServiceProvider)
- ✅ Scoped → Scoped
- ✅ Scoped → Transient
- ✅ Transient → Transient
- ❌ Scoped → Singleton (không có vấn đề về mặt kỹ thuật)
- ❌ Singleton → Scoped (không nên)
- ❌ Singleton → Transient (có thể accept)
-
Register services với interface:
// ✅ Tốt builder.Services.AddScoped<IProductRepository, ProductRepository>(); // ❌ Tránh builder.Services.AddScoped<ProductRepository>(); -
Constructor Injection thay vì Property Injection:
// ✅ Tốt public class Service { private readonly IOtherService _other; public Service(IOtherService other) => _other = other; } // ❌ Tránh public class Service { public IOtherService Other { get; set; } }