☕ NEW! 完成新手任務即可參加抽獎!LINE 星巴克禮券等你拿,名額有限!        🎉 推廣活動:邀請好友註冊 DevLearn,累積推薦抽 LINE 星巴克禮券! 活動詳情 →        🔥 活動期間 2026/4/1 - 5/31 |已有 0 人參加       
ASP.NET Core 中級

依賴注入 DI(Dependency Injection)

什麼是依賴注入?

想像你去餐廳點餐

  • 沒有 DI:你自己走進廚房、找食材、自己煮(在程式碼裡自己 new 物件)
  • 有 DI:你跟服務生說你要什麼,餐廳幫你準備好送來(框架幫你建立物件)
// ❌ 沒有 DI:Controller 自己建立服務
public class OrderController : Controller
{
    public IActionResult Index()
    {
        var service = new OrderService();        // 自己 new(緊耦合!)
        var db = new AppDbContext();              // 每次都要自己建
        return View(service.GetOrders());        // 取得訂單
    }
}

// ✅ 有 DI:框架自動注入
public class OrderController : Controller
{
    private readonly IOrderService _service;     // 只宣告介面

    public OrderController(IOrderService service) // 建構子注入
    {
        _service = service;                      // 框架會自動提供實例
    }

    public IActionResult Index()
    {
        return View(_service.GetOrders());       // 直接使用(鬆耦合!)
    }
}

三種生命週期

// Program.cs - 註冊服務
var builder = WebApplication.CreateBuilder(args);

// Transient:每次注入都建立新實例(像即溶咖啡,每杯都新泡)
builder.Services.AddTransient<IEmailService, EmailService>();

// Scoped:每個 HTTP 請求共用一個實例(像餐廳一桌一壺茶)
builder.Services.AddScoped<IOrderService, OrderService>();

// Singleton:整個應用程式只有一個實例(像飲水機,大家共用)
builder.Services.AddSingleton<ICacheService, CacheService>();

什麼時候用哪個?

生命週期 說明 適合場景 比喻
Transient 每次都新建 輕量、無狀態的服務 即溶咖啡
Scoped 每個請求一個 DbContext、購物車 一桌一壺茶
Singleton 全域唯一 快取、設定檔、Logger 飲水機

建構子注入

// 定義介面
public interface IProductService
{
    List<Product> GetAll();                       // 取得所有商品
    Product? GetById(int id);                     // 用 ID 取得商品
}

// 實作介面
public class ProductService : IProductService
{
    private readonly AppDbContext _db;             // 資料庫 Context

    public ProductService(AppDbContext db)         // 注入 DbContext
    {
        _db = db;                                 // 保存參考
    }

    public List<Product> GetAll()
    {
        return _db.Products.ToList();             // 從資料庫取得所有商品
    }

    public Product? GetById(int id)
    {
        return _db.Products.Find(id);             // 用主鍵查詢
    }
}
// Program.cs - 註冊服務
builder.Services.AddScoped<IProductService, ProductService>(); // 註冊介面與實作
builder.Services.AddDbContext<AppDbContext>(options =>          // 註冊 DbContext
    options.UseSqlServer(connectionString));                    // 使用 SQL Server
// Controller 中使用
public class ProductsController : Controller
{
    private readonly IProductService _productService; // 介面欄位

    // 建構子注入:框架會自動提供 IProductService 實例
    public ProductsController(IProductService productService)
    {
        _productService = productService;             // 保存注入的服務
    }

    public IActionResult Index()
    {
        var products = _productService.GetAll();      // 使用服務取得資料
        return View(products);                        // 傳給 View
    }
}

IServiceCollection 常用方法

// Program.cs - 各種註冊方式
var builder = WebApplication.CreateBuilder(args);

// 1. 基本註冊
builder.Services.AddTransient<IMyService, MyService>();    // 介面 → 實作

// 2. 註冊自己(沒有介面)
builder.Services.AddScoped<MyService>();                   // 直接註冊類別

// 3. 用工廠方法註冊
builder.Services.AddTransient<IMyService>(sp =>            // sp = ServiceProvider
{
    var config = sp.GetRequiredService<IConfiguration>();   // 取得其他服務
    return new MyService(config["ApiKey"]!);                // 用設定值建立
});

// 4. 註冊多個實作
builder.Services.AddTransient<INotifier, EmailNotifier>(); // 第一個實作
builder.Services.AddTransient<INotifier, SmsNotifier>();   // 第二個實作
// 注入 IEnumerable<INotifier> 可以拿到所有實作

🤔 我這樣寫為什麼會錯?

❌ 錯誤 1:Captive Dependency(Singleton 持有 Scoped)

// ❌ Singleton 服務注入 Scoped 服務(被困的依賴)
builder.Services.AddSingleton<ICacheService, CacheService>(); // Singleton
builder.Services.AddScoped<IDbContext, AppDbContext>();        // Scoped

public class CacheService : ICacheService
{
    private readonly IDbContext _db;               // ❌ Singleton 持有 Scoped!
    public CacheService(IDbContext db)             // DbContext 永遠不會被釋放
    {
        _db = db;                                  // 記憶體洩漏!
    }
}
// ✅ 用 IServiceScopeFactory 手動建立 Scope
public class CacheService : ICacheService
{
    private readonly IServiceScopeFactory _factory; // 注入 Scope 工廠

    public CacheService(IServiceScopeFactory factory)
    {
        _factory = factory;                        // 保存工廠
    }

    public List<Product> GetCachedProducts()
    {
        using var scope = _factory.CreateScope();  // 建立新的 Scope
        var db = scope.ServiceProvider
            .GetRequiredService<IDbContext>();       // 從 Scope 取得 DbContext
        return db.Products.ToList();               // 使用後 Scope 會自動 Dispose
    }
}

為什麼? Singleton 存活整個應用程式生命週期,但 Scoped 應該每個請求結束就釋放。Singleton 持有 Scoped 會導致 Scoped 永遠不被釋放,造成記憶體洩漏。

❌ 錯誤 2:忘記註冊服務

// ❌ 忘記在 Program.cs 註冊 IOrderService
public class OrderController : Controller
{
    public OrderController(IOrderService service) { } // 執行時會報錯!
}
// 錯誤:InvalidOperationException: Unable to resolve service for type 'IOrderService'
// ✅ 記得在 Program.cs 註冊
builder.Services.AddScoped<IOrderService, OrderService>(); // 註冊服務

為什麼? DI 容器只認識你註冊過的服務,沒註冊就不知道怎麼建立實例,會在執行時丟出例外。

❌ 錯誤 3:在建構子裡做太多事

// ❌ 建構子裡做複雜初始化(萬一失敗整個服務就壞了)
public class ReportService : IReportService
{
    private readonly List<Report> _reports;         // 報表快取

    public ReportService(IDbContext db)
    {
        _reports = db.Reports.ToList();             // ❌ 建構子裡查資料庫!
    }
}
// ✅ 建構子只存參考,方法裡才做邏輯
public class ReportService : IReportService
{
    private readonly IDbContext _db;                 // 只存參考

    public ReportService(IDbContext db)
    {
        _db = db;                                   // 建構子只做簡單賦值
    }

    public List<Report> GetReports()
    {
        return _db.Reports.ToList();                // 方法裡才查詢
    }
}

為什麼? 建構子應該只做欄位賦值,不該有商業邏輯或 I/O 操作。建構子失敗會導致整個 DI 解析失敗,很難除錯。

💡 大家的想法 · 0

載入中...
💬 即時聊天室 🟢 0 人在線
😀 😎 🤓 💻 🎮 🎸 🔥
➕ 新問題
📋 我的工單
💬 LINE 社群
🔒
需要註冊才能使用此功能
註冊帳號即可解鎖測驗、遊戲、簽到、筆記下載等所有功能,完全免費!
免費註冊