在 .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
7public 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[ ]
[ ]
public class DemoController : ControllerBase
{
private IEnumerable<IFoo> _allFoo;
public DemoController(IEnumerable<IFoo> allFoo)
{
_allFoo = allFoo;
}
[ ]
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
12public 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[ ]
[ ]
public class DemoController : ControllerBase
{
private IReadOnlyDictionary<string, IBar> _allBar;
public DemoController(IReadOnlyDictionary<string, IBar> allBar)
{
_allBar = allBar;
}
[ ]
public ActionResult<string> Get(string name)
{
var sb = new StringBuilder();
// bar
_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
8public 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[ ]
[ ]
public class DemoController : ControllerBase
{
private IBazBridge _bazBridge;
public DemoController(IBazBridge bazBridge)
{
_bazBridge = bazBridge;
}
[ ]
public ActionResult<string> Get(string name)
{
var sb = new StringBuilder();
// baz
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
34public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddScoped<IQux, Qux1>();
services.AddScoped<IQux, Qux2>();
services.AddScoped<IProvider<IQux>, Provider<IQux>>();
}
, ``
注入與使用:
``` csharp
[ ]
[ ]
public class DemoController : ControllerBase
{
private IProvider<IQux> _quxProvider;
public DemoController(IProvider<IQux> quxProvider)
{
_quxProvider = quxProvider;
}
[ ]
public ActionResult<string> Get(string name)
{
var sb = new StringBuilder();
// baz
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
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。
優點
- 泛用性極高,可重複套用在任何單介面多實作的情境。
- 註冊簡易,對使用者比較友善。
缺點與限制
- 實作很複雜。
結論
方法很多種,大致可以分為兩個面向
- 注入全部後再挑選。
- 提供管理用的類別 (不論是工廠/橋接或是其他模式)。
上面的範例只是為了展示這兩個大方向,實際運用時需要視情境變化調整,例如這篇文章提到幾種做法,都和上面範例不太一樣但大方向是一致的。
參考
.NET6 Dependency Injection — One Interface, Multiple Implementations