有一天,我夢見我死了。
地獄門口,閻羅王拿出一本厚厚的註冊清單,一邊唸:「根據你的 ServiceCollection,這輩子你註冊了以下幾項依賴…」
1 2 3 4 5 6 7 8 IConfidenceService → FakeItTillYouMakeItService ILoveService → UnhealthyAttachmentService ILogger<YourLife> → Mom'sConstantNagging ICareerPlanner → JustFollowWhatEveryoneElseDidService ISelfWorthEvaluator → DependsOnLikesAndRetweetsService IPurposeOfLifeResolver → null
我試圖抗議:「等等!我沒有註冊這些啊!」 閻羅王翻了翻後台的 StackTrace,淡淡地說:「人走著走著、就註冊成這樣了。」
「每一次討好別人,你就呼叫了 services.AddTransient(); 每一次壓抑情緒,你就將 Logger 的 level 設成了 Silent,你以為退一步海闊天空,其實 ConfigureLifetime(Singleton),永遠揮之不去。」
我低頭看著那本厚厚的註冊清單,每一筆都不是誰替我填的, 而是我用選擇、用習慣,親手寫上去的。 有些甚至沒有 interface,只有實作,就這樣默默被綁死一輩子。
也許我們都該時不時打開人生的 ServiceCollection,看看自己到底註冊了些什麼。不然有一天注入進來的,不是服務,是報應。
👼 「註冊」到底是在註冊什麼?
當你寫這樣的程式碼:
1 2 3 services.AddScoped<IMailService, GmailMailService>();
系統實際在一個「對照表(ServiceCollection)」裡,加入一筆資料,這筆資料記錄了:
服務的類型(IMailService)
實作的類型(GmailMailService)
生命週期(Scope)
要怎麼產生這個實體(Constructor Info)
會是像這樣的資料結構
1 2 3 4 5 6 7 8 9 class ServiceDescriptor { public Type ServiceType; public Type ImplementationType; public Lifetime Lifetime; public Func<IServiceProvider, object > Factory; }
註冊的本質:就是建立「類型對應表」和「建構方式」的設定檔。
註冊介面或直接註冊實作 1 2 3 4 5 6 7 8 9 services.AddTransient<IDataService, DataService>(); services.AddTransient<DataService>(); services.AddSingleton<GlobalService>(); services.AddScoped<MyService>();
在 Controller 中使用它:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public class HomeController : Controller { private readonly DataService _dataService; public HomeController (DataService dataService ) { _dataService = dataService; } public IActionResult Index () { var data = _dataService.GetData(); return Content(data); } }
小型、簡單、不需要替換的服務,可以直接用實作類別,不一定要 interface。 大型、複雜、可能會變動或測試的服務,有多個實作版本、建議一定要寫 interface。
註冊時也可以選擇自行處理建立物件的細節 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 builder.Services.AddSingleton<IClient>(sp => { var logger = sp.GetRequiredService<ILogger<Client>>(); var serviceToken = builder.Configuration["SECRETS:ServiceToken:Client" ]; var market = builder.Configuration["MARKET" ]; var env = builder.Configuration["ENVIRONMENT" ]; if (market == "TW" ) { env = "Prod" ; } if (env?.StartsWith("QA" ) == true ) { env = "QA" ; } return new Client(logger, serviceToken, market, env); });
註冊「同一個實體」 1 2 3 4 5 services.AddSingleton<IDataService, DataService>(); services.AddSingleton<ISomeInterface, DataService>();
雖然看起來都是 DataService,但其實你是告訴系統:
建立一個 DataService 實體,當作 IDataService
再建立一個新的 DataService 實體,當作 ISomeInterface
➡️ 結果:兩個不同實體、記憶體不同
問題風險在,如果你以為「是同一個實體」,卻不是,那會導致狀態不一致,修改一個的值,另一個沒跟著改,這是多份資源浪費(像是開了兩個 DB 連線)而且一些事件、註冊、記憶體、thread safety 問題難以追蹤
推薦用「工廠註冊法」:
1 2 3 4 5 services.AddSingleton<DataService>(); services.AddSingleton<IDataService>(provider => provider.GetRequiredService<DataService>()); services.AddSingleton<ISomeInterface>(provider => provider.GetRequiredService<DataService>());
我先註冊 DataService,由系統 new 它(只會 new 一次)
然後把 IDataService 和 ISomeInterface 都指向同一個實體(透過 GetRequiredService)
➡️ 這樣最安全、最乾淨,也不用手動 new。
「共用同一個實體」 1 2 3 4 5 var dataService = new DataService();services.AddSingleton<IDataService>(dataService); services.AddSingleton<ISomeInterface>(dataService);
你先 new 一個物件,然後把這個同一個實體分別註冊成不同介面 ➡️ 結果:兩個介面都指向 同一個物件實體
👼 注入 .NET 背後的 DI Container(容器) 會做以下幾件事:
📦 Step 1:尋找註冊的服務描述
透過 ServiceType(這裡是 IMailService)去你註冊時的清單找有沒有對應的項目。
🏗️ Step 2:建立物件
如果有找到對應的 GmailMailService,就會根據註冊時的「建構方式」:
檢查 GmailMailService 的建構子。
如果建構子有需要其他參數(例如 ILogger),就遞迴再去找這些參數的實作。
組裝整個依賴鏈(Dependency Graph)。
呼叫 Activator.CreateInstance(…) 或自己產生 instance(透過 Reflection 或 IL emit)。
這其實稱為:「依賴樹的遞迴解析(Recursive Resolution)」。本質上就像在砌積木,每個積木可能還需要別的積木,組合好才能建出你要的服務。
主要分為三種方式
面向
建構式注入
Invoke 參數注入
RequestServices
依賴清晰度
🌟🌟🌟🌟🌟 最清楚,依賴寫在建構式
🌟🌟🌟 有點清楚(寫在方法參數)
🌟 混亂,不容易知道用了哪些服務
可測試性
🌟🌟🌟🌟🌟 最好測試(直接模擬建構式)
🌟🌟🌟 好測試(方法參數可控制)
🌟 難測試(要模擬整個 DI 容器)
耦合程度
低(高內聚)
中(方法與 DI 框架綁在一起)
高(強烈依賴系統物件,如 HttpContext)
彈性程度
中(要在建構時就決定依賴)
高(延後注入時間)
很高(可隨時取用)
維護性
高
中
低
建構式注入 一開始創建角色(物件)時,就把所有會需要的服務裝備好。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 public interface IAttackCalculator { int CalculateDamage (int baseDamage ) ; } public class Warrior { private readonly IAttackCalculator _calculator; private readonly ILogger<Warrior> _logger; public Warrior (IAttackCalculator calculator, ILogger<Warrior> logger ) { _calculator = calculator; _logger = logger; } public void Attack () { int damage = _calculator.CalculateDamage(10 ); _logger.LogInformation($"戰士攻擊造成了 {damage} 點傷害!" ); } }
角色一出生就配好劍、護甲和能力,隨時可以戰鬥。
Invoke 參數注入 把需要的工具「在執行某個方法時才交進來」,不是在一開始建立物件時就給。 攻擊行為是在遊戲事件中發生(例如 Controller 的 Action),用參數注入獲得幫手。
1 2 3 4 5 6 7 8 9 10 11 12 13 public class BattleController : ControllerBase { [HttpPost("attack" ) ] public IActionResult Attack ([FromServices] IAttackCalculator calculator, [FromServices] ILogger<BattleController> logger ) { int damage = calculator.CalculateDamage(20 ); logger.LogInformation($"玩家進行攻擊,造成了 {damage} 點傷害!" ); return Ok(new { damage }); } }
或是在 Middleware
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public class MyMiddleware { private readonly RequestDelegate _next; public MyMiddleware (RequestDelegate next ) { _next = next; } public async Task InvokeAsync (HttpContext context, ILogger<MyMiddleware> logger ) { logger.LogInformation("Middleware 被呼叫" ); await _next(context); } }
RequestServices 這是「臨時需要某個服務時自己去問系統要」,不像前面兩種那樣自動注入。有點像你做早餐做到一半突然發現缺牛奶,就去冰箱(系統)裡找牛奶。
✅ 用 HttpContext.RequestServices
1 2 3 4 5 6 7 8 9 10 11 12 public class MagicController : ControllerBase { [HttpGet("cast" ) ] public IActionResult CastMagic () { var magicService = HttpContext.RequestServices.GetRequiredService<IMagicService>(); var result = magicService.Cast(); return Ok(result); } }
✅ 用全域的 IServiceProvider
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public class GameEngine { private readonly IServiceProvider _globalProvider; public GameEngine (IServiceProvider provider ) { _globalProvider = provider; } public void Execute () { var magicService = _globalProvider.GetRequiredService<IMagicService>(); magicService.Cast(); } }
比較項目
IServiceProvider
HttpContext.RequestServices
範圍
可以是全域、單例、任何你創的容器
限定在這一個 HTTP 請求內
建立時間
可在應用程式啟動時建立
ASP.NET Core 為每個 Request 自動建立
目的
用來解析任何註冊過的服務
用來取得「這次請求範圍內」的服務實體
常見用法
在 Console App、背景工作中
在 Controller、Middleware、Filter、Razor Page
你要解決的問題
應該用誰?
Controller 或 Middleware 中臨時需要服務
HttpContext.RequestServices
非 Request 範圍(如背景工作、Console App)需要服務
IServiceProvider
註冊服務
IServiceCollection
從 provider 抓服務
GetRequiredService<T>()
☘️ 結語 有些人會問:「這些服務註冊這麼細,真的有差嗎?反正能跑就好啊。」
就像你從不記得自己哪天開始變得愛解釋、怕尷尬、總是說「沒關係」;哪次告白沒說出口,最後竟默默註冊了一個 INoOneWillEverKnowService。
其實差別就在這裡:註冊,決定了注入;選擇,鋪好了路徑。
你現在寫下的每一行 services.AddXXX(…),未來都有可能在 Controller、Middleware,甚至某個深夜 2 點的 Exception Stack 裡出現。就像我們的性格、行為、價值觀,終將在某個人生的切點被呼叫、被注入、被執行。
所以下次看到註冊服務這件事,別再想著只是個小設定。因為當你不主動定義,預設值就會替你做決定 —— 而預設值,通常不是你想要的版本。
1 2 3 services.AddScoped<IFuture, WhatYouReallyWantService>();
你註冊了嗎?