本節介紹Util應用框架依賴注入的使用和設定擴充套件.
文章分為多個小節,如果對設計原理不感興趣,只需閱讀基礎用法部分即可.
當你想呼叫某個服務的方法完成特定功能時,首先需要得到這個服務的範例.
最簡單的辦法是直接 new 一個服務範例,不過這樣就把服務的實現牢牢綁死了,當你需要更換實現,除了直接修改它沒有別的辦法.
依賴注入是一種獲取服務範例更好的方法.
通常需要先定義服務介面,然後在你的構造方法宣告這些介面引數.
服務範例不是你建立的,而是從外部傳入的.
你只跟服務介面打交道,所以不會被具體的實現類綁死.
現在每個服務都在自己的構造方法定義引數接收依賴項,但是最終必須在某處真正建立這些服務範例.
使用new手工建立服務範例是不可行的,因為存在依賴鏈,比如使用 new A() 建立服務A的範例時,服務A可能依賴服務B,需要先建立服務B的範例,而服務B可能還有依賴.
另外,某些服務可能需要特定的生命週期,比如工作單元服務,在單個請求過程,每次注入的工作單元範例必須是同一個.
我們需要一種機制,能夠自動建立具有依賴的服務範例,並管理範例的生命週期.
Asp.Net Core 內建了構造方法依賴注入能力.
通過構造方法注入服務範例,是依賴注入最常見的形式.
一些專門的依賴注入框架,比如 autofac 支援屬性注入等高階功能.
Util應用框架使用Asp.Net Core內建的依賴注入,對於大部分業務場景,構造方法注入已經足夠了.
依賴注入有三種生命週期.
在整個系統只建立一個範例.
無狀態或不可變的服務才能設定成單例.
對於 Asp.Net Core 環境,每個請求建立一個範例,在整個請求過程,獲取的是同一個範例,在請求結束時銷燬.
注意: 對於非 Asp.Net Core 環境,Scope 生命週期與 Singleton 相同.
在Util專案中,與工作單元相關的服務都需要設定成 Scope 生命週期,比如 工作單元,倉儲,領域服務,應用服務等.
每次注入都會建立一個新的服務範例.
定義介面的目的是為了方便切換實現.
一個介面可能有多個實現類,但是在同一時間,應該只有一個實現類生效.
舉個例子,倉儲介面有兩個實現類.
/// <summary>
/// 倉儲
/// </summary>
public interface IRepository {
}
/// <summary>
/// 倉儲1
/// </summary>
public class Repository1 : IRepository {
}
/// <summary>
/// 倉儲2
/// </summary>
public class Repository2 : IRepository {
}
有兩個應用服務,服務1需要倉儲1的範例,服務2需要倉儲2的範例.
/// <summary>
/// 服務1
/// </summary>
public class Service1 {
public Service1( IRepository repository ) {
}
}
/// <summary>
/// 服務2
/// </summary>
public class Service2 {
public Service2( IRepository repository ) {
}
}
現在, IRepository有兩個範例,並且這兩個範例都處於使用狀態.
兩個服務都注入了 IRepository 介面, 如何把正確的倉儲範例注入到指定的服務中?
一些依賴注入框架可以為特定實現類命名,然後為服務傳遞特定命名的依賴項,不過這種方法複雜且容易出錯.
一種簡單有效的方法是建立更具體的介面,從而讓每種生效的實現類只有一個.
/// <summary>
/// 倉儲
/// </summary>
public interface IRepository {
}
/// <summary>
/// 倉儲1
/// </summary>
public interface IRepository1 : IRepository {
}
/// <summary>
/// 倉儲2
/// </summary>
public interface IRepository2 : IRepository {
}
/// <summary>
/// 倉儲1
/// </summary>
public class Repository1 : IRepository1 {
}
/// <summary>
/// 倉儲2
/// </summary>
public class Repository2 : IRepository2 {
}
/// <summary>
/// 服務1
/// </summary>
public class Service1 {
public Service1( IRepository1 repository ) {
}
}
/// <summary>
/// 服務2
/// </summary>
public class Service2 {
public Service2( IRepository2 repository ) {
}
}
由於注入了更具體的介面,所以不需要特定的依賴設定方法.
不要奇怪,雖然現在每個介面只有一個實現,但你在任何時候都可以增加實現類進行切換.
唯一需要記住的是,任何時候,生效的實現類應該只有一個.
通常對服務型別使用依賴注入,比如控制器,應用服務,領域服務,倉儲等.
實體可能也包含某些依賴項,但不能使用依賴注入框架建立實體.
簡單實體使用 new 建立,更復雜的實體建立過程使用工廠進行封裝.
只需在構造方法定義需要的服務引數即可.
範例:
/// <summary>
/// 測試服務
/// </summary>
public class TestService {
public TestService( ITestRepository repository ) {
}
}
Asp.Net Core 標準的依賴設定方法是呼叫 IServiceCollection 擴充套件方法.
範例:
設定 ITestService 介面的實現類為 TestService,生命週期為 Scope.
var builder = WebApplication.CreateBuilder( args );
builder.Services.AddScoped<ITestService, TestService>();
不過,大部分時候,你都不需要手工設定依賴服務,它由Util應用框架自動掃描設定.
Util應用框架提供了三個介面,用於自動設定相應生命週期的依賴服務.
限制: 必須把 ISingletonDependency 這三個介面放在需要設定的介面上,不能放在實現類上.
範例:
服務基介面 IService 繼承了 IScopeDependency 介面.
所有繼承了 IService 的服務介面,在啟動時自動查詢相應的實現類,並設定為 Scope 服務.
/// <summary>
/// 服務
/// </summary>
public interface IService : IScopeDependency {
}
當使用 ISingletonDependency 等介面自動設定依賴關係時,如果服務介面有多個實現類,究竟哪個生效?
Util應用框架提供了 Util.Dependency.IocAttribute 特性,用於更改依賴優先順序,從而精確指定實現類.
範例:
服務 Service1 實現了服務介面 IService, IService 從 IScopeDependency 繼承.
實現類的預設優先順序為 0.
IocAttribute 特性接收一個表示優先順序的整數,值越大,表示優先順序越高.
服務 Service2 的依賴優先順序設定為 1,比 Service1 大,所以注入 IService 介面的實現類是 Service2.
/// <summary>
/// 服務1
/// </summary>
public class Service1 : IService {
}
/// <summary>
/// 服務2
/// </summary>
[Ioc(1)]
public class Service2 : IService {
}
構造方法依賴注入簡單清晰,只需檢視構造方法就能瞭解依賴的服務.
不過它也帶來了一些問題.
如果服務基礎類別使用了構造方法依賴注入,每當依賴服務發生變化,都需要修改所有子類的構造方法,這會導致架構的脆弱性.
另一個問題是無法通過依賴注入為靜態方法提供依賴項.
在業務場景使用靜態方法是一種陋習,需要堅決抵制.
但是某些工具類使用靜態方法可能更方便.
服務定位器從物件容器中主動拉取依賴服務.
依賴注入和服務定位器都從物件容器獲取依賴項,但依賴注入的依賴項是從外部被動推入的.
服務定位器比依賴注入的耦合度高,也更難測試,不過它能解決之前提到的問題.
為了讓服務基礎類別穩定,可以在基礎類別構造方法獲取 IServiceProvider 引數.
IServiceProvider 是 .Net 服務提供程式,可以呼叫它獲取依賴服務.
下面來看看Util應用服務基礎類別.
/// <summary>
/// 應用服務
/// </summary>
public abstract class ServiceBase : IService {
/// <summary>
/// 初始化應用服務
/// </summary>
/// <param name="serviceProvider">服務提供器</param>
protected ServiceBase( IServiceProvider serviceProvider ) {
ServiceProvider = serviceProvider ?? throw new ArgumentNullException( nameof( serviceProvider ) );
Session = serviceProvider.GetService<ISession>() ?? NullSession.Instance;
IntegrationEventBus = serviceProvider.GetService<IIntegrationEventBus>() ?? NullIntegrationEventBus.Instance;
var logFactory = serviceProvider.GetService<ILogFactory>();
Log = logFactory?.CreateLog( GetType() ) ?? NullLog.Instance;
}
/// <summary>
/// 服務提供器
/// </summary>
protected IServiceProvider ServiceProvider { get; }
/// <summary>
/// 使用者對談
/// </summary>
protected ISession Session { get; }
/// <summary>
/// 整合事件匯流排
/// </summary>
protected IIntegrationEventBus IntegrationEventBus { get; }
/// <summary>
/// 紀錄檔操作
/// </summary>
protected ILog Log { get; }
}
應用服務基礎類別定義了使用者對談和紀錄檔操作等依賴項,但不是從構造方法獲取的,而是呼叫服務提供程式 IServiceProvider 的 GetService 方法.
通過傳遞 IServiceProvider 引數,服務子類不需要在構造方法宣告使用者對談等其它依賴項,減輕了負擔.
當依賴項發生變化時,不需要修改基礎類別的構造方法引數,直接通過服務提供程式獲取依賴.
構造方法獲取 IServiceProvider 引數解決了服務基礎類別的問題,但 IServiceProvider 引數本身還是通過依賴注入方式提供的.
無法通過依賴注入為靜態工具類傳遞引數,在靜態工具方法中傳遞 IServiceProvider 引數又會導致API難用.
一個常見的需求是在靜態工具方法中獲取當前 HttpContext 範例,並存取它的某些功能.
在更早的 Asp.Net 中, 我們可以通過 HttpContext.Current 靜態屬性來獲取當前Http上下文.
但 Asp.Net Core 已經拋棄這種用法,現在需要先依賴注入 IHttpContextAccessor 範例,並使用它獲取當前Http上下文.
Util提供了一個服務定位器工具類 Util.Helpers.Ioc .
通過呼叫 Ioc 靜態方法 Create 就能獲取依賴服務.
範例:
下面的例子演示瞭如何在靜態方法中獲取遠端IP地址.
先通過 Ioc.Create 獲取Http上下文存取器, 然後得到當前Http上下文,呼叫它的 Connection.RemoteIpAddress 獲取遠端IP地址.
public static class Tool {
/// <summary>
/// 獲取使用者端Ip地址
/// </summary>
public static string GetIp() {
var httpContext = Ioc.Create<IHttpContextAccessor>()?.HttpContext;
return httpContext?.Connection.RemoteIpAddress?.ToString();
}
}
使用 Ioc.Create 方法獲取依賴項要小心,只有在 Asp.Net Core 環境中才能安全使用.
在後臺任務等其它環境中, Ioc.Create 與依賴注入使用的物件容器可能不同.
由於它具有副作用, Util靜態工具方法已經很少使用它.
Util.Helpers.Ioc 現在用在不太重要的一些場景,業務開發中應嚴格使用依賴注入獲取依賴.
Util應用框架提供了另一個工具類 Util.Helpers.Web 來支援 Asp.Net Core 靜態工具方法.
使用 Util.Helpers.Web 改造上面的例子.
public static class Tool {
/// <summary>
/// 獲取使用者端Ip地址
/// </summary>
public static string GetIp() {
return Web.HttpContext?.Connection.RemoteIpAddress?.ToString();
}
}
你可以通過 Web.HttpContext 獲取當前Http上下文,比使用 Ioc.Create 方便得多.
依賴服務註冊器提供對 Util.Dependency.ISingletonDependency 等介面的依賴設定擴充套件支援.
通過型別查詢器分別查詢實現了 ISingletonDependency,IScopeDependency,ITransientDependency 三個介面的所有class.
對每個class類,查詢它們的介面,並註冊相應生命週期的依賴關係.
/// <summary>
/// 依賴服務註冊器 - 用於掃描註冊ISingletonDependency,IScopeDependency,ITransientDependency
/// </summary>
public class DependencyServiceRegistrar : IServiceRegistrar {
/// <summary>
/// 獲取服務名
/// </summary>
public static string ServiceName => "Util.Infrastructure.DependencyServiceRegistrar";
/// <summary>
/// 排序號
/// </summary>
public int OrderId => 100;
/// <summary>
/// 是否啟用
/// </summary>
public bool Enabled => ServiceRegistrarConfig.IsEnabled( ServiceName );
/// <summary>
/// 註冊服務
/// </summary>
/// <param name="serviceContext">服務上下文</param>
public Action Register( ServiceContext serviceContext ) {
return () => {
serviceContext.HostBuilder.ConfigureServices( ( context, services ) => {
RegisterDependency<ISingletonDependency>( services, serviceContext.TypeFinder, ServiceLifetime.Singleton );
RegisterDependency<IScopeDependency>( services, serviceContext.TypeFinder, ServiceLifetime.Scoped );
RegisterDependency<ITransientDependency>( services, serviceContext.TypeFinder, ServiceLifetime.Transient );
} );
};
}
/// <summary>
/// 註冊依賴
/// </summary>
private void RegisterDependency<TDependencyInterface>( IServiceCollection services, ITypeFinder finder, ServiceLifetime lifetime ) {
var types = GetTypes<TDependencyInterface>( finder );
var result = FilterTypes( types );
foreach ( var item in result )
RegisterType( services, item.Item1, item.Item2, lifetime );
}
/// <summary>
/// 獲取介面型別和實現型別列表
/// </summary>
private List<(Type, Type)> GetTypes<TDependencyInterface>( ITypeFinder finder ) {
var result = new List<(Type, Type)>();
var classTypes = finder.Find<TDependencyInterface>();
foreach ( var classType in classTypes ) {
var interfaceTypes = Util.Helpers.Reflection.GetInterfaceTypes( classType, typeof( TDependencyInterface ) );
interfaceTypes.ForEach( interfaceType => result.Add( (interfaceType, classType) ) );
}
return result;
}
/// <summary>
/// 過濾型別
/// </summary>
private List<(Type, Type)> FilterTypes( List<(Type, Type)> types ) {
var result = new List<(Type, Type)>();
foreach ( var group in types.GroupBy( t => t.Item1 ) ) {
if ( group.Count() == 1 ) {
result.Add( group.First() );
continue;
}
result.Add( GetTypesByPriority( group ) );
}
return result;
}
/// <summary>
/// 獲取優先順序型別
/// </summary>
private (Type, Type) GetTypesByPriority( IGrouping<Type, (Type, Type)> group ) {
int? currentPriority = null;
Type classType = null;
foreach ( var item in group ) {
var priority = GetPriority( item.Item2 );
if ( currentPriority == null || priority > currentPriority ) {
currentPriority = priority;
classType = item.Item2;
}
}
return ( group.Key, classType );
}
/// <summary>
/// 獲取優先順序
/// </summary>
private int GetPriority( Type type ) {
var attribute = type.GetCustomAttribute<IocAttribute>();
if ( attribute == null )
return 0;
return attribute.Priority;
}
/// <summary>
/// 註冊型別
/// </summary>
private void RegisterType( IServiceCollection services, Type interfaceType, Type classType, ServiceLifetime lifetime ) {
services.TryAdd( new ServiceDescriptor( interfaceType, classType, lifetime ) );
}
}
Ioc 工具類內建了一個物件容器,如果沒有為它設定服務提供器,它將從內建物件容器獲取依賴,這是導致副作用的根源.
/// <summary>
/// 容器操作
/// </summary>
public static class Ioc {
/// <summary>
/// 容器
/// </summary>
private static readonly Util.Dependency.Container _container = Util.Dependency.Container.Instance;
/// <summary>
/// 獲取服務提供器操作
/// </summary>
private static Func<IServiceProvider> _getServiceProviderAction;
/// <summary>
/// 服務範圍工廠
/// </summary>
public static IServiceScopeFactory ServiceScopeFactory { get; set; }
/// <summary>
/// 建立新容器
/// </summary>
public static Util.Dependency.Container CreateContainer() {
return new Util.Dependency.Container();
}
/// <summary>
/// 獲取服務集合
/// </summary>
public static IServiceCollection GetServices() {
return _container.GetServices();
}
/// <summary>
/// 設定獲取服務提供器操作
/// </summary>
/// <param name="action">獲取服務提供器操作</param>
public static void SetServiceProviderAction( Func<IServiceProvider> action ) {
_getServiceProviderAction = action;
}
/// <summary>
/// 獲取
/// </summary>
public static IServiceProvider GetServiceProvider() {
var provider = _getServiceProviderAction?.Invoke();
if ( provider != null )
return provider;
return _container.GetServiceProvider();
}
/// <summary>
/// 建立物件
/// </summary>
/// <typeparam name="T">物件型別</typeparam>
public static T Create<T>() {
return Create<T>( typeof( T ) );
}
/// <summary>
/// 建立物件
/// </summary>
/// <typeparam name="T">返回物件型別</typeparam>
/// <param name="type">物件型別</param>
public static T Create<T>( Type type ) {
var service = Create( type );
if( service == null )
return default;
return (T)service;
}
/// <summary>
/// 建立物件
/// </summary>
/// <param name="type">物件型別</param>
public static object Create( Type type ) {
if( type == null )
return null;
var provider = GetServiceProvider();
return provider.GetService( type );
}
/// <summary>
/// 建立物件集合
/// </summary>
/// <typeparam name="T">返回型別</typeparam>
public static List<T> CreateList<T>() {
return CreateList<T>( typeof( T ) );
}
/// <summary>
/// 建立物件集合
/// </summary>
/// <typeparam name="T">返回型別</typeparam>
/// <param name="type">物件型別</param>
public static List<T> CreateList<T>( Type type ) {
Type serviceType = typeof( IEnumerable<> ).MakeGenericType( type );
var result = Create( serviceType );
if( result == null )
return new List<T>();
return ( (IEnumerable<T>)result ).ToList();
}
/// <summary>
/// 建立服務範圍
/// </summary>
public static IServiceScope CreateScope() {
var provider = GetServiceProvider();
return provider.CreateScope();
}
/// <summary>
/// 清理
/// </summary>
public static void Clear() {
_container.Clear();
}
}
Ioc 工具類需要獲取正確的服務提供器,可以通過 SetServiceProviderAction 方法進行設定.
對於 Asp.Net Core 環境, AspNetCoreServiceRegistrar 服務註冊器已經正確設定Ioc工具類的服務提供器.
但對於非 Asp.Net Core 環境, 設定正確的服務提供器可能非常困難.
/// <summary>
/// AspNetCore服務註冊器
/// </summary>
public class AspNetCoreServiceRegistrar : IServiceRegistrar {
/// <summary>
/// 獲取服務名
/// </summary>
public static string ServiceName => "Util.Infrastructure.AspNetCoreServiceRegistrar";
/// <summary>
/// 排序號
/// </summary>
public int OrderId => 200;
/// <summary>
/// 是否啟用
/// </summary>
public bool Enabled => ServiceRegistrarConfig.IsEnabled( ServiceName );
/// <summary>
/// 註冊服務
/// </summary>
/// <param name="serviceContext">服務上下文</param>
public Action Register( ServiceContext serviceContext ) {
serviceContext.HostBuilder.ConfigureServices( ( context, services ) => {
RegisterHttpContextAccessor( services );
RegisterServiceLocator();
} );
return null;
}
/// <summary>
/// 註冊Http上下文存取器
/// </summary>
private void RegisterHttpContextAccessor( IServiceCollection services ) {
var httpContextAccessor = new HttpContextAccessor();
services.TryAddSingleton<IHttpContextAccessor>( httpContextAccessor );
Web.HttpContextAccessor = httpContextAccessor;
}
/// <summary>
/// 註冊服務定位器
/// </summary>
private void RegisterServiceLocator() {
Ioc.SetServiceProviderAction( () => Web.ServiceProvider );
}
}
如果你不想自動掃描註冊 ISingletonDependency,IScopeDependency,ITransientDependency 相關依賴,可以禁用它.
ServiceRegistrarConfig.Instance.DisableDependencyServiceRegistrar();
builder.AsBuild().AddUtil();