在 .NET Core 中注入多個實作有幾種方式,各自有不同的優缺點與試用情境,相關範例程式碼會放在 這個 GitHub 專案 上。
Implementation Factory
Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions 提供的多載,以 AddScoped 為例:
public static IServiceCollection AddScoped<TService>(this IServiceCollection services, Func<IServiceProvider, TService> implementationFactory) where TService : class
第二個參數提供一個工廠方法來決定要使用哪種實作。
優點
缺點與限制
- 只適用於在注入時就知道要用什麼規則決定使用哪個實作。
- 彈性低,雖然簡單但是適用情境狹窄。
注入 IEnumerable
註冊多個實作,使用時注入 IEnumerable<I> 後再決定要使用哪個實作,可以搭配一個欄位用來標示該實作的名字 (要用類別名稱來標示也可以)。
註冊服務:
1 2 3 4 5 6 7
| public void ConfigureServices(IServiceCollection services) { services.AddControllers();
services.AddSingleton<IFoo, Foo1>(); services.AddSingleton<IFoo, Foo2>(); }
|
注入與使用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| [ApiController] [Route("[controller]")] public class DemoController : ControllerBase { private IEnumerable<IFoo> _allFoo;
public DemoController(IEnumerable<IFoo> allFoo) { _allFoo = allFoo; }
[HttpGet] public ActionResult<string> Get(string name) { var sb = new StringBuilder();
var foo = _allFoo.FirstOrDefault(r => r.Name == name); sb.Append(foo?.Hi());
return Ok(sb.ToString()); } }
|
優點
缺點與限制
- 因為是注入所有的實作後再挑出要使用的那一個,其他的都用不到,會有浪費資源的疑慮,所以通常搭配 AddSingleton 使用。
- 每次搜尋 (
_allFoo.FirstOrDefault(r => r.Name == name) )的時間複雜度都是 O(n),需要多次搜尋時這個缺點會更明顯。
Dictionary
註冊成一個 Dictionary 或類似的物件。
註冊服務:
1 2 3 4 5 6 7 8 9 10 11 12
| public void ConfigureServices(IServiceCollection services) { services.AddControllers();
services.AddScoped<IBar, Bar1>(); services.AddScoped<IBar, Bar2>(); services.AddScoped<IReadOnlyDictionary<string, IBar>>(provider => { var allBar = provider.GetService<IEnumerable<IBar>>(); return allBar.ToDictionary(bar => bar.Name, bar => bar); }); }
|
注入與使用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| [ApiController] [Route("[controller]")] public class DemoController : ControllerBase { private IReadOnlyDictionary<string, IBar> _allBar;
public DemoController(IReadOnlyDictionary<string, IBar> allBar) { _allBar = allBar; }
[HttpGet] public ActionResult<string> Get(string name) { var sb = new StringBuilder();
_allBar.TryGetValue(name, out IBar bar); sb.Append(bar?.Hello());
return Ok(sb.ToString()); } }
|
優點
- 簡單。
- 彈性高。
- 可擴充性高,如果是作為一個套件,即使沒提供適合的實作,使用者仍然可以自己實作
IBar 後注入。
- 只有第一次初始化的時候將
IEnumerable<T> 轉成 Dictionary 時比較耗時,之後每次取用的時間複雜度都是 O(1),在生命週期範圍內多次取用的效能優於 IEnumerable<T>。
缺點與限制
- 沒有那麼直覺,對於使用者來說要記得注入時要注入為
IReadOnlyDictionary<string, IBar>。
透過另一個類別來轉接 (橋接模式)
這種做法的概念在於提供一個橋接用的類別來使用那些實作。
註冊服務:
1 2 3 4 5 6 7 8
| public void ConfigureServices(IServiceCollection services) { services.AddControllers();
services.AddScoped<IBaz, Baz1>(); services.AddScoped<IBaz, Baz2>(); services.AddScoped<IBazBridge, BazBridge>(); }
|
注入與使用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| [ApiController] [Route("[controller]")] public class DemoController : ControllerBase { private IBazBridge _bazBridge;
public DemoController(IBazBridge bazBridge) { _bazBridge = bazBridge; }
[HttpGet] public ActionResult<string> Get(string name) { var sb = new StringBuilder();
sb.Append(_bazBridge.Hey(name));
return Ok(sb.ToString()); } }
|
Bridge 相關類別:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| public interface IBazBridge { string Hey(string name); }
internal class BazBridge : IBazBridge { private IReadOnlyDictionary<string, IBaz> _allBazDictionary;
public BazBridge(IEnumerable<IBaz> allBaz) { _allBazDictionary = allBaz.ToDictionary(baz => baz.Name, baz => baz); }
public string Hey(string name) { _allBazDictionary.TryGetValue(name, out IBaz baz); return baz?.Hey(); } }
|
如果有多個情境要使用 Bridge,可以將建構子中轉換 IEnumerable<T> 為 Dictionary 的部分抽成 BridgeBase,提供給所有 Bridge 繼承。
優點
- 彈性極高,Brige 中甚至可以做各種轉接與變化。
- 可擴充性高,如果是作為一個套件,即使沒提供適合的實作,使用者仍然可以自己實作
IBaz 後注入。
- 只有第一次初始化的時候將
IEnumerable<T> 轉成 Dictionary 時比較耗時,之後每次取用的時間複雜度都是 O(1),在生命週期範圍內多次取用的效能優於 IEnumerable<T>。
- 相較於之前的注入 Dictionary 的解法,這種高彈性/擴充性的作法更適合放在套件中,這種設計相似於
HttpClient 與 IHttpClientFactory 之間的關係,所以相對於之前 Dictionary 的解法來說,使用上不直覺的問題相對低了一些。
缺點與限制
- 複雜很多。
- 多個介面需要套用時會需要很多組 Bridge。
- 註冊較不友善,
services.AddScoped<IBazBridge, BazBridge>(); 這邊使用者要認識具體實作 BazBridge。
透過另一個類別來管理 (Provider)
這種做法的概念在於提供一個管理用的類別來提供那些實作。
註冊服務:
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 26 27 28 29 30 31 32 33 34
| public void ConfigureServices(IServiceCollection services) { services.AddControllers();
services.AddScoped<IQux, Qux1>(); services.AddScoped<IQux, Qux2>(); services.AddScoped<IProvider<IQux>, Provider<IQux>>(); } , ``
注入與使用: ``` csharp [ApiController] [Route("[controller]")] public class DemoController : ControllerBase { private IProvider<IQux> _quxProvider;
public DemoController(IProvider<IQux> quxProvider) { _quxProvider = quxProvider; }
[HttpGet] public ActionResult<string> Get(string name) { var sb = new StringBuilder();
sb.Append(_quxProvider.Get(name).Yo());
return Ok(sb.ToString()); } }
|
Provider 相關類別:
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 IProvider<T> where T : INameable { T Get(string name); }
public class Provider<T> : IProvider<T> where T : INameable { private IReadOnlyDictionary<string, T> _dictionary;
public Provider(IEnumerable<T> nameable) { _dictionary = nameable.ToDictionary(n => n.Name, n => n); }
public T Get(string name) { _dictionary.TryGetValue(name, out T imp); return imp; } }
|
優點
- 有幾乎等同
Bridge 解法的優點 (除了彈性略低)。
- 泛用性極高,可重複套用在任何單介面多實作的情境。
缺點與限制
- 因為不需要實作各種
Bridge,彈性較 Bridge 解法低一點。
- 註冊較不友善,
services.AddScoped<IProvider<IQux>, Provider<IQux>>(); 這邊使用者要認識具體實作 Provider<T>,且兩層泛型參數比較不好閱讀。
綜合以上最佳化使用者體驗
這是我用在一個套件 - MoreNet.DependencyInjection 上的作法,介紹直接看 GitHub。
優點
- 泛用性極高,可重複套用在任何單介面多實作的情境。
- 註冊簡易,對使用者比較友善。
缺點與限制
結論
方法很多種,大致可以分為兩個面向
- 注入全部後再挑選。
- 提供管理用的類別 (不論是工廠/橋接或是其他模式)。
上面的範例只是為了展示這兩個大方向,實際運用時需要視情境變化調整,例如這篇文章提到幾種做法,都和上面範例不太一樣但大方向是一致的。
參考
Service registration methods
.NET6 Dependency Injection — One Interface, Multiple Implementations