Util應用框架基礎(一) - 依賴注入

何鎮汐發表於2023-11-02

本節介紹Util應用框架依賴注入的使用和配置擴充套件.

文章分為多個小節,如果對設計原理不感興趣,只需閱讀基礎用法部分即可.

概述

當你想呼叫某個服務的方法完成特定功能時,首先需要得到這個服務的例項.

最簡單的辦法是直接 new 一個服務例項,不過這樣就把服務的實現牢牢綁死了,當你需要更換實現,除了直接修改它沒有別的辦法.

依賴注入是一種獲取服務例項更好的方法.

通常需要先定義服務介面,然後在你的構造方法宣告這些介面引數.

服務例項不是你建立的,而是從外部傳入的.

你只跟服務介面打交道,所以不會被具體的實現類綁死.

依賴注入框架

現在每個服務都在自己的構造方法定義引數接收依賴項,但是最終必須在某處真正建立這些服務例項.

使用new手工建立服務例項是不可行的,因為存在依賴鏈,比如使用 new A() 建立服務A的例項時,服務A可能依賴服務B,需要先建立服務B的例項,而服務B可能還有依賴.

另外,某些服務可能需要特定的生命週期,比如工作單元服務,在單個請求過程,每次注入的工作單元例項必須是同一個.

我們需要一種機制,能夠自動建立具有依賴的服務例項,並管理例項的生命週期.

Asp.Net Core 內建了構造方法依賴注入能力.

透過構造方法注入服務例項,是依賴注入最常見的形式.

一些專門的依賴注入框架,比如 autofac 支援屬性注入等高階功能.

Util應用框架使用Asp.Net Core內建的依賴注入,對於大部分業務場景,構造方法注入已經足夠了.

依賴注入生命週期

依賴注入有三種生命週期.

  • Singleton 單例

    在整個系統只建立一個例項.

    無狀態或不可變的服務才能設定成單例.

  • Scope 每個請求建立一個例項

    對於 Asp.Net Core 環境,每個請求建立一個例項,在整個請求過程,獲取的是同一個例項,在請求結束時銷燬.

    注意: 對於非 Asp.Net Core 環境,Scope 生命週期與 Singleton 相同.

    在Util專案中,與工作單元相關的服務都需要設定成 Scope 生命週期,比如 工作單元,倉儲,領域服務,應用服務等.

  • Transient 每次呼叫建立一個新例項

    每次注入都會建立一個新的服務例項.

依賴注入最佳實踐

一個介面配置一個實現

定義介面的目的是為了方便切換實現.

一個介面可能有多個實現類,但是在同一時間,應該只有一個實現類生效.

舉個例子,倉儲介面有兩個實現類.

/// <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應用框架提供了三個介面,用於自動配置相應生命週期的依賴服務.

  • Util.Dependency.ISingletonDependency
    配置生命週期為 Singleton 的服務.
  • Util.Dependency.IScopeDependency
    配置生命週期為 Scope 的服務.
  • Util.Dependency.ITransientDependency
    配置生命週期為 Transient 的服務.

限制: 必須把 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; }
}

應用服務基類定義了使用者會話和日誌操作等依賴項,但不是從構造方法獲取的,而是呼叫服務提供程式 IServiceProviderGetService 方法.

透過傳遞 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 方便得多.

原始碼解析

DependencyServiceRegistrar 依賴服務註冊器

依賴服務註冊器提供對 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 服務定位器工具類

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();

相關文章