Util应用框架基础(一) - 依赖注入

发布时间 2023-11-02 11:58:55作者: 何镇汐

本节介绍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();