依賴注入在 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 是不是不支持多個服務實現類的注入?這難倒了很多人。
方法一、接收 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()); }
Enumerable<T> 的妙處就是可以 foreach ,這樣你也能訪問多個實例,而且必要時還可以聯合 LINQ 一起耍。
運行結果如下。
接口定義
public interface ITest { string Name { get; } void Do(); } public class TestA : ITest { public string Name => "a"; public void Do() => Console.Write("a"); } public class TestB : ITest { public string Name => "b"; public void Do() => Console.Write("b"); }
依賴注入方法
直接注入實現類的方式
services.AddTransient<TestA>(); services.AddTransient<TestB>(); public HomeController(TestA testA) { ...... } serviceProvider.GetService<TestA>();
使用集合的注入方式
services.AddTransient<ITest, TestA>(); services.AddTransient<ITest, TestB>(); public HomeController(IEnumerable<ITest> tests) { var testa = tests.FirstOrDefault(p => p.Name == "a"); } serviceProvider.GetServices<ITest>().ToList().FirstOrDefault(p => p.Name == "a");
使用Func工廠的注入方式
注意,注冊Func工廠是一定要使用單例模式
services.AddTransient<TestA>(); services.AddTransient<TestB>(); services.AddSingleton(provider => { Func<string, ITest> accesor = key => { switch (key) { case “a”: return provider.GetService<TestA>(); case "b": return provider.GetService<TestB>(); default: throw new NotSupportedException($"Not Support key : {key}"); } }; return accesor; }); public HomeController(Func<string, IOceanOrderHandleStrategy> serviceAccessor) { var testa = serviceAccessor("a"); } var func = serviceProvider.GetServices<Func<string, IOceanOrderHandleStrategy>>(); var testa = func("a");
使用工廠類注入
同樣要注意,這里注冊只能是單例模式
SingletonFactory singletonFactory = new SingletonFactory(); singletonFactory.AddService<ITest>(new TestA(), "a"); singletonFactory.AddService<ITest>(new TestB(), "b"); services.AddSingleton(singletonFactory); public HomeController(SingletonFactory singletonFactory) { //使用標識從SingletonFactory獲取自己想要的服務實現 var testa = singletonFactory.GetService<ITest>("a"); }
SingletonFactory.cs
public class SingletonFactory { Dictionary<Type, Dictionary<string, object>> serviceDic; public SingletonFactory() { serviceDic = new Dictionary<Type, Dictionary<string, object>>(); } public void AddService<TService>(string id, TService service) where TService : class { AddService(typeof(TService), service, id); } public void AddService(string id, Type serviceType, object service) { if (service != null) { if (serviceDict.TryGetValue(serviceType, out Dictionary<string, object> implDict)) { implDict[id] = service; } else { implDict = new Dictionary<string, object>(); implDict[id] = service; serviceDict[serviceType] = implDict; } } } public TService GetService<TService>(string id) where TService : class { var serviceType = typeof(TService); return GetService<TService>(serviceType, id); } public TService GetService<TService>(Type serviceType, string id) where TService : class { if (serviceDic.TryGetValue(serviceType, out Dictionary<string, object> implDic)) { if (implDic.TryGetValue(id, out object service)) { return service as TService; } } return null; } }