Computer >> บทช่วยสอนคอมพิวเตอร์ >  >> การเขียนโปรแกรม >> C#

ความแตกต่างระหว่าง AddSingleton vs AddScoped vs AddTransient ใน ASP.NET Core

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>();

ตารางเปรียบเทียบครบถ้วน

คุณสมบัติAddSingletonAddScopedAddTransient
Instanceเดียวกันทั้งแอป (ทุก request/user)1 ต่อ 1 Requestใหม่ทุกครั้งที่ Inject
สร้างเมื่อไหร่Request แรก / App Startupเริ่ม Requestตอน Inject
DisposeApp Shutdownจบ Requestจบ Request
Thread Safetyต้องรับผิดชอบเอง (shared)Per request (ปลอดภัยกว่า)ไม่ต้องกังวล (instance แยก)
Stateful?ได้ (แต่ต้องระวัง concurrency)ได้ (ต่อ request)ไม่แนะนำ (สร้างใหม่ตลอด)
เหมาะสำหรับ
Cache, Configuration, Database Connection Pool, Logger, SignalR HubDbContext, Unit of Work, Repository, User Session, Business Logic ServiceValidator, 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 ไว้ตลอด
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 ที่นี่
        }
    }
}

แนวทางเลือกใช้งาน

  1. Transient - เริ่มต้น (Default choice) สำหรับ stateless, lightweight service
  2. Scoped - เมื่อต้อง share state ภายใน request (DbContext, UnitOfWork, User context)
  3. Singleton - เมื่อต้อง share state ทั่วแอป (Cache, Config, Logger) และ thread-safe