【ASP.NET Core】依賴注入高階玩法——如何注入多個服務實現類
依賴注入在 ASP.NET Core 中起中很重要的作用,也是一種高大上的程式設計思想,它的總體原則就是:俺要啥,你就給俺送啥過來。服務型別的例項轉由容器自動管理,無需我們在程式碼中顯式處理。
因此,有了依賴注入後,你的程式設計思維就得變一變了。在過去,許多功能性的型別(比如一個加密解密的類),我們都喜歡將其定義為靜態(static),而有了依賴注入,你就要避免使用靜態型別,應該交由服務容器幫你管理,只要你用好了,你會發現依賴注入是很方便的。
依賴注入的初級玩法,也是比較標準的玩法,此種玩法有兩種模式:
1、十代單傳模式:一個介面對應一個類,比如先定義介面 IA、IB,隨後,類A實現 IA,類B 實現 IB。一對一。也可以是抽象類(或基類)E,然後 F 繼承 E 類。
2、斷子絕孫模式:直接就寫一個類,不考慮派生,直接就新增到服務容器中。
來,看個例子。
我先定義個介面。
public interface IPlayGame { void Play(); }
然後,寫一個類來實現它。
public class NBPlayGame : IPlayGame { public void Play() { Console.WriteLine("全民打麻藥。"); } }
我們知道,所謂服務類,其實就是普通類,這些類一般用於完成某些功能,比如計算 MD5 值。接著呢,還記得 Startup 類有個ConfigureServices 方法吧,對,就在這廝裡面把我們剛剛那個服務進行註冊(就是新增到 ServiceCollection 集合中)。
public void ConfigureServices(IServiceCollection services) { services.AddTransient<IPlayGame, NBPlayGame>(); }
新增的時候很簡單,型別一對一,IPlayGame 介面與 NBPlayGame 類對應。新增時有三種方法你可以呼叫,實際上對應著,服務類在容器中的生命週期。
AddSingleton:單個例項,這是壽命最長的,與天同壽。整個應用程式中僅用一個例項。
AddTransient:這個是最短命的,可能是天天晚上加班熬夜,死得很快。此種情況下,服務類的例項是用的時候建立,用完後直接銷燬。
AddScoped:這個比較難理解。它的生命週期在單個請求內,包括客戶端與伺服器之間隨後產生的子請求,反正只要請求的會話結束了,就會清理。
然後,你就可以進行注入了,比如在中介軟體,在控制器,或者在其他服務類的建構函式上(中介軟體是在 Invoke / InvokeAsync 方法上)進行例項接收。
現在來用一下,寫一箇中間件。
public class TestMiddleware { public TestMiddleware(RequestDelegate next) { } public Task InvokeAsync(HttpContext context, IPlayGame game) { game.Play(); return Task.CompletedTask; } }
已註冊的服務會注入到 InvokeAsync 方法的引數中。注意第一個引數是 HttpContext,這是必須引數,後面的是注入的引數。
最後,在 Startup 類的 Configure 方法中就可以 use 這個中介軟體了。
public void Configure(IApplicationBuilder app) { app.UseMiddleware<TestMiddleware>(); }
執行後,Play 方法呼叫,在控制檯中輸出以下結果。
“斷子絕孫”模式,不使用介面規範,直接寫功能類。
public class DoSomething { public string GetMessage() => "你好,剛才 Boss 找你。"; }
註冊服務時更簡單。
public void ConfigureServices(IServiceCollection services) { services.AddScoped<DoSomething>(); }
在 Configure 方法中進行注入。
public void Configure(IApplicationBuilder app, DoSomething thing) { Console.WriteLine(thing.GetMessage()); }
執行後,輸出結果如下。
在容器中,使用ServiceDescriptor 類來儲存服務型別相關的資訊。其中,ServiceType 表示的是服務的型別,如果服務是有介面與實現類的,那麼這個屬性指的是介面的型別,實現類的型別資訊由ImplementationType 屬性儲存。如果沒有介面,直接只定義型別,那麼這個型別的資訊就存到ServiceType 屬性上,ImplementationType 屬性不使用。
上面這些例子中,ServiceType 是 IPlayGame 介面相關資訊,ImplementationType 是 NBPlayGame 類的資訊。如果像上面 DoSomething 類的情況,則 ServiceType 為 DoSomething 相關的資訊,ImplementationType 為空。
接下來,咱們看高階玩法。
定義一個介面。
public interface IDemoService { string Version { get; } void Run(); }
然後,有兩個類實現這個介面。
public class DemoService1 : IDemoService { public string Version => "v1"; public void Run() { Console.WriteLine("第一個服務實現類。"); } } public class DemoService2 : IDemoService { public string Version => "v2"; public void Run() { Console.WriteLine("第二個服務實現類。"); } }
然後,我們註冊服務。
public void ConfigureServices(IServiceCollection services) { services.AddTransient<IDemoService, DemoService1>(); services.AddTransient<IDemoService, DemoService2>(); }
然後我們照例,接收注入,咱們依舊使用中介軟體的方法引數接收。
public class DemoMiddleware { public DemoMiddleware(RequestDelegate next) { // 由於程式約定,此建構函式必須提供。 } public async Task InvokeAsync(HttpContext context, IDemoService sv) { await context.Response.WriteAsync(sv.Version); } }
然後,在 Startup.Configure 方法中使用該中介軟體。
public void Configure(IApplicationBuilder app, DoSomething thing) { app.UseMiddleware<DemoMiddleware>(); }
執行之後,你發現問題了,看看輸出。
出事了,引數僅能接收到最後註冊的實現型別例項,也就是DemoService2 類。所以就看到網上有不少朋友發貼問了,.NET Core 是不是不支援多個服務實現類的注入?這難倒了很多人。
實話告訴你,Core Core 兄是支援注入多個實現類的例項的。
下面,老周介紹兩種解決方法(其實有三種,還有一種不太好弄,尤其是你對 Core 兄不熟的時候,所以我說兩種,基本夠用)。
方法一、接收IServiceProvider 型別的注入。
public async Task InvokeAsync(HttpContext context, IServiceProvider provider) { StringBuilder sb = new StringBuilder(); foreach (var sv in provider.GetServices<IDemoService>()) { sb.Append($"{sv.Version}<br/>"); } await context.Response.WriteAsync(sb.ToString()); }
只要能接收到IServiceProvider 所引用的例項,就能通過 GetServices 方法獲取多個服務例項。
方法二,這種方法老周很推薦,更簡單,直接注入IEnumerable<T> 型別,本例中就是IEnumerable<IDemoService>。
public async Task InvokeAsync(HttpContext context, IEnumerable<IDemoService> svs) { StringBuilder sb = new StringBuilder(); foreach (var sv in svs) { sb.Append($"{sv.Version}<br/>"); } await context.Response.WriteAsync(sb.ToString()); }
IEnumerable<T> 的妙處就是可以 foreach ,這樣你也能訪問多個例項,而且必要時還可以聯合 LINQ 一起耍。
執行結果如下。
不要問我是怎麼發現的,反正我告訴你了,你用就是了。