依賴注入 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 解析失敗,很難除錯。