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

本节介绍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 每次调用创建一个新实例

每次注入都会创建一个新的服务实例.

依赖注入最佳实践

一个接口配置一个实现

定义接口的目的是为了方便切换实现.

一个接口可能有多个实现类,但是在同一时间,应该只有一个实现类生效.

举个例子,仓储接口有两个实现类.

c# 复制代码
/// <summary>
/// 仓储
/// </summary>
public interface IRepository {
}

/// <summary>
/// 仓储1
/// </summary>
public class Repository1 : IRepository {
}

/// <summary>
/// 仓储2
/// </summary>
public class Repository2 : IRepository {
}

有两个应用服务,服务1需要仓储1的实例,服务2需要仓储2的实例.

c# 复制代码
/// <summary>
/// 服务1
/// </summary>
public class Service1 {
    public Service1( IRepository repository ) {
    }
}

/// <summary>
/// 服务2
/// </summary>
public class Service2 {
    public Service2( IRepository repository ) {
    }
}

现在, IRepository有两个实例,并且这两个实例都处于使用状态.

两个服务都注入了 IRepository 接口, 如何把正确的仓储实例注入到指定的服务中?

一些依赖注入框架可以为特定实现类命名,然后为服务传递特定命名的依赖项,不过这种方法复杂且容易出错.

一种简单有效的方法是创建更具体的接口,从而让每种生效的实现类只有一个.

c# 复制代码
/// <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 创建,更复杂的实体创建过程使用工厂进行封装.

基础用法

通过构造方法获取依赖服务

只需在构造方法定义需要的服务参数即可.

范例:

c# 复制代码
/// <summary>
/// 测试服务
/// </summary>
public class TestService {
    public TestService( ITestRepository repository ) {
    }
}

配置依赖服务

Asp.Net Core 标准的依赖配置方法是调用 IServiceCollection 扩展方法.

范例:

配置 ITestService 接口的实现类为 TestService,生命周期为 Scope.

c# 复制代码
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 服务.

c# 复制代码
/// <summary>
/// 服务
/// </summary>
public interface IService : IScopeDependency {
}

更改实现类依赖配置优先级

当使用 ISingletonDependency 等接口自动配置依赖关系时,如果服务接口有多个实现类,究竟哪个生效?

Util应用框架提供了 Util.Dependency.IocAttribute 特性,用于更改依赖优先级,从而精确指定实现类.

范例:

服务 Service1 实现了服务接口 IService, IService 从 IScopeDependency 继承.

实现类的默认优先级为 0.

IocAttribute 特性接收一个表示优先级的整数,值越大,表示优先级越高.

服务 Service2 的依赖优先级设置为 1,比 Service1 大,所以注入 IService 接口的实现类是 Service2.

c# 复制代码
/// <summary>
/// 服务1
/// </summary>
public class Service1 : IService {
}

/// <summary>
/// 服务2
/// </summary>
[Ioc(1)]
public class Service2 : IService {
}

服务定位器

构造方法依赖注入简单清晰,只需查看构造方法就能了解依赖的服务.

不过它也带来了一些问题.

如果服务基类使用了构造方法依赖注入,每当依赖服务发生变化,都需要修改所有子类的构造方法,这会导致架构的脆弱性.

另一个问题是无法通过依赖注入为静态方法提供依赖项.

在业务场景使用静态方法是一种陋习,需要坚决抵制.

但是某些工具类使用静态方法可能更方便.

服务定位器概述

服务定位器从对象容器中主动拉取依赖服务.

依赖注入和服务定位器都从对象容器获取依赖项,但依赖注入的依赖项是从外部被动推入的.

服务定位器比依赖注入的耦合度高,也更难测试,不过它能解决之前提到的问题.

为了让服务基类稳定,可以在基类构造方法获取 IServiceProvider 参数.

IServiceProvider 是 .Net 服务提供程序,可以调用它获取依赖服务.

下面来看看Util应用服务基类.

c# 复制代码
/// <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地址.

c# 复制代码
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 改造上面的例子.

c# 复制代码
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类,查找它们的接口,并注册相应生命周期的依赖关系.

c# 复制代码
/// <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 工具类内置了一个对象容器,如果没有为它设置服务提供器,它将从内置对象容器获取依赖,这是导致副作用的根源.

c# 复制代码
/// <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 环境, 设置正确的服务提供器可能非常困难.

c# 复制代码
/// <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 相关依赖,可以禁用它.

c# 复制代码
ServiceRegistrarConfig.Instance.DisableDependencyServiceRegistrar();
builder.AsBuild().AddUtil();