IoC(控制反轉)和 DI(依賴注入)常被混用,但兩者的層次不同:IoC 是設計原則,DI 是實現手段之一。搞清楚這個關係,才不會看到「容器」兩字就覺得是 IoC 的全部。
控制反轉 (IoC) 是什麼
傳統寫法裡,程式碼自己決定何時建立依賴、用哪個實作——控制權在你的類別手上。IoC 把這個控制權交出去:由外部(框架、容器、或呼叫方)決定要注入什麼。
IoC 並不等於 DI 容器。Martin Fowler 最早定義 IoC 時,涵蓋了 Service Locator、Template Method、以及依賴注入等多種形式。DI 容器只是實現 IoC 最常見的一種工具。
IoC 帶來的實際好處:
- 解耦:類別依賴介面而非具體實作,替換實作時不需動到使用方。
- 可測試性:測試時可把真實依賴換成 mock,不需啟動整個應用程式。
- 可擴展性:新增實作、切換策略,不需修改現有程式碼。
這三點都源自同一個機制——類別不再自己 new 依賴,而是被動接受注入。
依賴注入 (DI) 的注入方式
DI 的核心想法:不在類別內部建立依賴物件,改由外部傳入。
注入方式有三種,但在 .NET 內建 DI 容器的情境下,實際可用的只有建構函式注入:
建構函式注入(.NET 內建容器的唯一原生支援方式):
1 | public class OrderService |
屬性注入和方法注入在 .NET 內建 DI 容器中並非原生支援。若要使用,需要改用 Autofac 或 Ninject 等第三方容器。直接在屬性上加 [Inject] 在內建容器下不會有任何效果,這是新手常踩的坑。
.NET Core 內建 DI 容器
註冊服務
.NET 6+ 的 Program.cs:
1 | var builder = WebApplication.CreateBuilder(args); |
.NET 5 以前的 Startup.cs:
1 | public class Startup |
控制器使用:
1 | public class MyController : Controller |
生命週期管理
.NET DI 容器提供三種生命週期:
- Transient:每次從容器解析(resolve)時建立新實例。注意「每次解析」不等於「每次 HTTP 請求」——同一個請求裡如果有兩個類別都依賴同一個 Transient 服務,兩者拿到的是不同實例。
- Scoped:同一個請求(或範圍)內共用同一實例,跨請求則各自獨立。
- Singleton:整個應用程式生命週期共用同一實例,只建立一次。
進階用法
工廠方法(Transient 的進階寫法):
當建立服務需要額外參數或執行時期邏輯時,用 AddTransient 的 factory 多載:
1 | builder.Services.AddTransient<IEmailSender>(provider => |
同一介面的多個實作:
1 | builder.Services.AddTransient<INotification, EmailNotification>(); |
取出所有實作用 GetServices<T>()(注意是複數):
1 | public class NotificationDispatcher |
條件註冊(依環境切換實作):
1 | if (builder.Environment.IsDevelopment()) |
最佳實踐
優先建構函式注入。 依賴清單一目了然,且強迫你在類別被建立時就完成初始化,不存在「物件存在但依賴還沒設定」的中間狀態。
依賴抽象,不依賴具體實作。 IUserRepository 而非 SqlUserRepository,這樣日後換成 Redis 或 Cosmos DB 不需改動使用方。
避免 Service Locator 模式。 直接注入 IServiceProvider 再手動 GetService<T>() 是反模式——依賴關係藏在方法體裡,測試時無法從建構子看出類別需要什麼。
注意生命週期陷阱(Captive Dependency)。 Singleton 服務若持有 Scoped 服務,Scoped 服務會被「捕獲」成事實上的 Singleton——它在整個應用程式生命週期只建立一次,不會隨請求重置,導致資料隔離失效。.NET 在開發模式下預設開啟 scope validation,會在啟動時直接丟例外,但正式環境預設關閉,要特別留意。
1 | // 危險:Singleton 持有 Scoped,會在容器建立時被鎖定 |




