.Net Core DI 使用注意事項
1.一個接口多個Service實現
builder.Services.AddTransient<Service1>(); builder.Services.AddTransient<Service2>(); builder.Services.AddTransient(serviceProvider => { Func<Type, IService> accesor = key => { if (key == typeof(Service1)) return serviceProvider.GetService<Service1>(); else if (key == typeof(Service2)) return serviceProvider.GetService<Service2>(); else throw new ArgumentException($"不支持的DI Key: {key}"); }; return accesor; });
調用:
public class ValuesController : ControllerBase { readonly IService _Service1; readonly IService _Service2; public ValuesController(Func<Type, IService> func) { _Service1 = func(typeof(Service1)); _Service2 = func(typeof(Service2)); } xxx... }
知識點:
當一個interface被多個Service實現時,在正常調用時內存會load全部的Service實現類(如果Service過多,則會占用大量的內存,常見的以CAP Event通信為主),
如果不指定則默認為最后一個注入的Service(管道:request進入是有順序的),這種情況往往所以我們通過以上方式來定向獲取,還有一種比較普遍的的用法,
Service Locator模式,這個下文介紹,這是一種設計模式,根本不是"依賴注入"。
上面這段話,我們會發現一個問題,為什么在調用時需要加載全部的實現類呢,通常我們進行構造函數注入時往往要求聲明的是interface類型,這其實不是一種硬性要求,這是一種設計思想,降低程序的耦合度,在使用處實例化!!! 然后 規則?規則是用來打破滴~
如果在構造函數中直接注入的是實現類,就可以避免加載全部的service導致內存溢出的問題了,當然,這又增加了代碼的耦合度,
在實際的開發中,很多時候都是需要做一個取舍,但最好還是拆分Service功能,在根源處降低耦合。
2.一個Service具有多個構造函數
一個Service有多個構造函數,雖然它們的參數均能夠由ServiceProvider來提供,但是並沒有一個構造函數的參數類型集合能夠成為所有有效構造函數參數類型集合的超集,所以ServiceProvider無法選擇出一個最佳的構造函數。如果我們運行這個程序,一個System.AggregateException異常會被拋出來,控制台上將呈現出如下所示的錯誤消息。
InvalidOperationException: Unable to activate type 'DIDemo.Services.Service.MethodA'. The following constructors are ambiguous:
不同的DI容器可能有不同的策略,因為只接觸過.net Core,所以不懂的俺不能亂說,在網上查 可以在構造函數上標注一個InjectionAttribute特性,來指定構造函數,
1: public class Foo 2: { 3: public IBar Bar{get; private set;} 4: public IBaz Baz {get; private set;} 5: 6: [Injection] 7: public Foo(IBar bar) 8: { 9: this.Bar = bar; 10: } 11: 12: public Foo(IBar bar, IBaz):this(bar) 13: { 14: this.Baz = baz; 15: } 16: }
類似這種,這個Inject是需要根據業務自己實現的!
.net core默認的則是,需要有一個是所有構造函數參數合集的這么一個構造函數!
3.Service Locator模式
上文我們提到過,這哥們根本就不是"依賴注入",那么怎么來區分呢,以下來自 互聯網,,,可以直接跳到后面
從使用者的角度,在一個采用依賴注入的應用中,我們只需要采用標准的注入形式將服務類型定義好,並在應用啟動之前完成相應的服務注冊就可以了,框架自身的引擎在運行過程中會利用依賴注入容器來提供當前所需的服務實例。換句話說,依賴注入容器的使用者應該是框架而不是應用程序。Service Locator模式顯然不是這樣,很明顯是應用程序在利用它來提供所需的服務實例,所以它的使用者是應用程序。
本着“松耦合、高內聚”的設計原則,我們既然將一組相關的操作定義在一個能夠復用的服務中,就應該盡量要求服務自身不但具有獨立和自治的特性,也要求服務之間的應該具有明確的界限,服務之間的依賴關系應該是明確的而不是模糊的。不論是采用屬性注入或者方法注入,還是使用Service Locator來提供當前依賴的服務,這無疑為當前的服務增添了一個新的依賴,即針對依賴注入容器或者Service Locator的依賴。
當前服務針對另一個服務的依賴與針對依賴注入容器或者Service Locator的依賴具有本質的不同,前者是一種基於類型的依賴,不論是基於服務的接口還是實現類型,這是一種基於“契約”的依賴。這種依賴不僅是明確的,也是有保障的。但是依賴注入容器或者Service Locator本質上是一個黑盒,它能夠提供所需服務的前提是相應的服務注冊已經預先添加了容器之中,但是這種依賴不僅是模糊的也是不可靠的。
我自己的理解是,DI是將所需Service推送給程序,而Service Locator則是在使用時自己拉取Service,前者可以確定有且明確,不然沒東西推啊,后者,不清楚拉的東西是不是存在的,所以在會用到模糊和不可靠來形容,不說廢話,直接上代碼。
public class ServiceLocator { public static IServiceProvider Instance { get; set; } }
定義一個全局變量,然后在program.cs的注冊Service的最后!最后!最后!賦值,如不在最后,可能會有遺漏的Service沒有被添加到ServiceProvider,使用的時候獲取不到。
ServiceLocator.Instance = builder.Services.BuildServiceProvider();
...
//在使用處拉去Service即可。
ServiceLocator.Instance.GetService<IMethodA>().A();
如果是Controller中,還可以使用HttpContext獲取,HttpContext只能在Controller層獲取,所以局限還挺大的。
HttpContext.RequestServices.GetService<T>();
細心的小伙伴們,可能會發現一個問題,我定義的全局變量是個靜態字段,好處從名字可見,而缺點會是什么呢?問的好~
as you know,每次請求都會經過管道,為該字段賦值,且只有一次,當遭遇多線程時,生命周期為Transient(即用即銷)的Service,將會被固定使用一個,線程安全問題顯而易見,不定時的會出現實例被Dispose掉了,於是我們要剔除Static,那就只剩下ServiceProvider了。
_serviceProvider.GetService<IMethodA>().A(); var services = _serviceProvider.GetServices<IMethodA>();
這是思考的過程哈,如果使用,請直接使用IServciePovider來拉取,注入方式和其他正常Service一樣!
private readonly ILogger<HomeController> _logger; private readonly IMethodA _methodA; private readonly IService _Service1; private readonly IService _Service2; private readonly IServiceProvider _serviceProvider; public HomeController(ILogger<HomeController> logger, IMethodA methodA, Func<Type, IService> func, IServiceProvider serviceProvider) { _logger = logger; _methodA = methodA; _Service1 = func(typeof(Service1)); _Service2 = func(typeof(Service2)); _serviceProvider = serviceProvider; }
最后我們介紹一下ServiceProvider與ServiceDescriptor,解釋一下上文出現的各種現象。
ASP.NET Core中的DI容器最終體現為一個IServiceProvider接口,我們將所有實現了該接口的類型及其實例統稱為ServiceProvider。如下面的代碼片段所示,該接口簡單至極,它僅僅提供了唯一個GetService方法,該方法根據提供的服務類型為你提供對應的服務實例。
public interface IServiceProvider { object GetService(Type serviceType); }
ASP.NET Core內部真正使用的是一個實現了IServiceProvider接口的內部類型(該類型的名稱為“ServiceProvider”),我們不能直接創建該對象,只能間接地通過調用IServiceCollection接口的擴展方法BuildServiceProvider得到它。IServiceCollection接口定義在“Microsoft.Extensions.DependencyInjection”命名空間下。 如下面的代碼片段所示,IServiceCollection接口實際上代表一個元素為ServiceDescriptor對象的集合,它直接繼承了另一個接口IList<ServiceDescriptor>,而ServiceCollection類實現了該接口。
public static class ServiceCollectionExtensions { public static IServiceProvider BuildServiceProvider(this IServiceCollection services); } public interface IServiceCollection : IList<ServiceDescriptor> {} Public class ServiceCollection: IServiceCollection { //省略成員 }
體現為DI容器的ServiceProvider之所以能夠根據我們給定的服務類型(一般是一個接口類型)提供一個能夠開箱即用的服務實例,是因為我們預先注冊了相應的服務描述信息,這些指導ServiceProvider正確實施服務提供操作的服務描述體現為如下一個ServiceDescriptor類型。
public class ServiceDescriptor { public ServiceDescriptor(Type serviceType, object instance); public ServiceDescriptor(Type serviceType, Func<IServiceProvider, object> factory, ServiceLifetime lifetime); public ServiceDescriptor(Type serviceType, Type implementationType, ServiceLifetime lifetime); public Type ServiceType { get; } public ServiceLifetime Lifetime { get; } public Type ImplementationType { get; } public object ImplementationInstance { get; } public Func<IServiceProvider, object> ImplementationFactory { get; } }
ServiceDescriptor的ServiceType屬性代表提供服務的生命類型,由於標准化的服務一般會定義成接口,所以在絕大部分情況下體現為一個接口類型。類型為ServiceLifetime的屬性Lifetime體現了ServiceProvider針對服務實例生命周期的控制方式。如下面的代碼片段所示,ServiceLifetime是一個枚舉類型,定義其中的三個選項(Singleton、Scoped和Transient)體現三種對服務對象生命周期的控制形式,這個我們就As you know了。
對於ServiceDescriptor的其他三個屬性來說,它們實際上是輔助ServiceProvider完成具體的服務實例提供操作。ImplementationType屬性代表被提供服務實例的真實類型,屬性ImplementationInstance則直接代表被提供的服務實例,ImplementationFactory則提供了一個創建服務實例的委托對象。
由於ASP.NET Core中的ServiceProvider是根據一個代表ServiceDescriptor集合的IServiceCollection對象創建的,當我們調用其GetService方法的時候,它會根據我們提供的服務類型找到對應的ServiceDecriptor對象。如果該ServiceDecriptor對象的ImplementationInstance屬性返回一個具體的對象,該對象將直接用作被提供的服務實例。如果ServiceDecriptor對象的ImplementationFactory返回一個具體的委托,該委托對象將直接用作創建服務實例的工廠。
如果這兩個屬性均為Null,ServiceProvider才會根據ImplementationType屬性返回的類型調用相應的構造函數創建被提供的服務實例。至於我們在上面一節中提到的三種依賴注入方式,ServiceProvider僅僅支持構造器注入,屬性注入和方法注入的支持並未提供。
代碼已同步到GitHub:https://github.com/Seth-Song/DIDemo