Dependency Injection Lifetime ใน ASP.NET Core
ใน ASP.NET Core มี 3 แบบของ Service Lifetime สำหรับลงทะเบียน Dependency ใน Startup.cs (หรือ Program.cs ใน .NET 6+)
1. AddSingleton - Instance เดียวตลอดชีวิตแอป
- สร้าง instance ครั้งเดียว เมื่อ request ครั้งแรก
- Instance เดียวนี้ใช้ร่วมกัน ทุก request, ทุก user, ทุก controller
- คล้ายกับ Static object
- Dispose เมื่อแอปพลิเคชัน shutdown
services.AddSingleton<ILog, Logger>();
2. AddScoped - Instance ต่อ 1 Request
- สร้าง instance ใหม่ทุก HTTP Request
- ภายใน request เดียว ใช้ instance เดียวกัน (controller, service, middleware ใช้ร่วมกัน)
- Request ใหม่ → Instance ใหม่
- Dispose ที่สิ้นสุด Request
services.AddScoped<ILog, Logger>();
3. AddTransient - Instance ใหม่ทุกครั้งที่ขอ
- สร้าง instance ใหม่ทุกครั้ง ที่มีการ request (inject)
- แม้ใน request เดียว ก็อาจได้ instance ต่างกันหาก inject หลายที่
- เหมาะกับ service ที่ Lightweight, Stateless
- Dispose ที่สิ้นสุด Request (ถ้า implement IDisposable)
services.AddTransient<ILog, Logger>();
ตารางเปรียบเทียบครบถ้วน
| คุณสมบัติ | AddSingleton | AddScoped | AddTransient |
|---|---|---|---|
| Instance | เดียวกันทั้งแอป (ทุก request/user) | 1 ต่อ 1 Request | ใหม่ทุกครั้งที่ Inject |
| สร้างเมื่อไหร่ | Request แรก / App Startup | เริ่ม Request | ตอน Inject |
| Dispose | App Shutdown | จบ Request | จบ Request |
| Thread Safety | ต้องรับผิดชอบเอง (shared) | Per request (ปลอดภัยกว่า) | ไม่ต้องกังวล (instance แยก) |
| Stateful? | ได้ (แต่ต้องระวัง concurrency) | ได้ (ต่อ request) | ไม่แนะนำ (สร้างใหม่ตลอด) |
| เหมาะสำหรับ | |||
| Cache, Configuration, Database Connection Pool, Logger, SignalR Hub | DbContext, Unit of Work, Repository, User Session, Business Logic Service | Validator, Helper, Email Sender, Calculator, Stateless Service | |
ตัวอย่างแสดงความแตกต่างจริง
using System;
using Microsoft.Extensions.DependencyInjection;
public interface IOperation
{
Guid OperationId { get; }
}
public class TransientOperation : IOperation
{
public Guid OperationId { get; } = Guid.NewGuid();
}
public class ScopedOperation : IOperation
{
public Guid OperationId { get; } = Guid.NewGuid();
}
public class SingletonOperation : IOperation
{
public Guid OperationId { get; } = Guid.NewGuid();
}
class Program
{
static void Main()
{
var services = new ServiceCollection();
services.AddTransient<IOperation, TransientOperation>();
services.AddScoped<IOperation, ScopedOperation>();
services.AddSingleton<IOperation, SingletonOperation>();
var provider = services.BuildServiceProvider();
// จำลอง Request 1
using (var scope1 = provider.CreateScope())
{
var sp = scope1.ServiceProvider;
Console.WriteLine("=== Request 1 ===");
PrintIds(sp);
PrintIds(sp); // ใน scope เดียว
}
// จำลอง Request 2
using (var scope2 = provider.CreateScope())
{
var sp = scope2.ServiceProvider;
Console.WriteLine("\n=== Request 2 ===");
PrintIds(sp);
}
}
static void PrintIds(IServiceProvider sp)
{
Console.WriteLine($"Transient: {sp.GetService<IOperation>().OperationId}");
Console.WriteLine($"Scoped: {sp.GetService<IOperation>().OperationId}");
Console.WriteLine($"Singleton: {sp.GetService<IOperation>().OperationId}");
}
}
ผลลัพธ์:
=== Request 1 === Transient: aaa-bbb-ccc-111 Transient: ddd-eee-fff-222 <-- ต่างกัน! Scoped: 111-222-333-444 Scoped: 111-222-333-444 <-- เดียวกันใน scope Singleton: 999-888-777-666 Singleton: 999-888-777-666 <-- เดียวกันตลอด === Request 2 === Transient: ggg-hhh-iii-333 Scoped: 555-666-777-888 <-- ใหม่! Singleton: 999-888-777-666 <-- เดียวกัน!
ข้อผิดพลาดที่พบบ่อย (Captive Dependency)
⚠️ ไม่ควร Inject Scoped/Transient เข้า Singleton
Singleton มีอายุยาวกว่า → จะ "จับ" Scoped/Transient instance ไว้ตลอดชีวิต → Memory leak, Stale data, Concurrency bug
Singleton มีอายุยาวกว่า → จะ "จับ" Scoped/Transient instance ไว้ตลอดชีวิต → Memory leak, Stale data, Concurrency bug
// ❌ ผิด - Singleton จับ Scoped ไว้ตลอด
services.AddSingleton<MySingleton>();
services.AddScoped<MyScoped>();
class MySingleton
{
public MySingleton(MyScoped scoped) { } // Captive dependency!
}
// ✅ ถูกต้อง - ใช้ IServiceScopeFactory
class MySingleton
{
private readonly IServiceScopeFactory _factory;
public MySingleton(IServiceScopeFactory factory) { _factory = factory; }
public void DoWork()
{
using (var scope = _factory.CreateScope())
{
var scoped = scope.ServiceProvider.GetService<MyScoped>();
// ใช้ scoped ที่นี่
}
}
}
แนวทางเลือกใช้งาน
- Transient - เริ่มต้น (Default choice) สำหรับ stateless, lightweight service
- Scoped - เมื่อต้อง share state ภายใน request (DbContext, UnitOfWork, User context)
- Singleton - เมื่อต้อง share state ทั่วแอป (Cache, Config, Logger) และ thread-safe