• 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 每次调用创建一个新实例

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

    依赖注入最佳实践

    一个接口配置一个实现

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

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

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

    /// 
    /// 仓储
    /// 
    public interface IRepository {
    }
    
    /// 
    /// 仓储1
    /// 
    public class Repository1 : IRepository {
    }
    
    /// 
    /// 仓储2
    /// 
    public class Repository2 : IRepository {
    }
    

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

    /// 
    /// 服务1
    /// 
    public class Service1 {
        public Service1( IRepository repository ) {
        }
    }
    
    /// 
    /// 服务2
    /// 
    public class Service2 {
        public Service2( IRepository repository ) {
        }
    }
    

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

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

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

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

    /// 
    /// 仓储
    /// 
    public interface IRepository {
    }
    
    /// 
    /// 仓储1
    /// 
    public interface IRepository1 : IRepository {
    }
    
    /// 
    /// 仓储2
    /// 
    public interface IRepository2 : IRepository {
    }
    
    /// 
    /// 仓储1
    /// 
    public class Repository1 : IRepository1 {
    }
    
    /// 
    /// 仓储2
    /// 
    public class Repository2 : IRepository2 {
    }
    
    /// 
    /// 服务1
    /// 
    public class Service1 {
        public Service1( IRepository1 repository ) {
        }
    }
    
    /// 
    /// 服务2
    /// 
    public class Service2 {
        public Service2( IRepository2 repository ) {
        }
    }
    

    由于注入了更具体的接口,所以不需要特定的依赖配置方法.

    不要奇怪,虽然现在每个接口只有一个实现,但你在任何时候都可以增加实现类进行切换.

    唯一需要记住的是,任何时候,生效的实现类应该只有一个.

    依赖注入的使用范围

    通常对服务类型使用依赖注入,比如控制器,应用服务,领域服务,仓储等.

    实体可能也包含某些依赖项,但不能使用依赖注入框架创建实体.

    简单实体使用 new 创建,更复杂的实体创建过程使用工厂进行封装.

    基础用法

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

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

    范例:

    /// 
    /// 测试服务
    /// 
    public class TestService {
        public TestService( ITestRepository repository ) {
        }
    }
    

    配置依赖服务

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

    范例:

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

    var builder = WebApplication.CreateBuilder( args );
    builder.Services.AddScoped();
    

    不过,大部分时候,你都不需要手工配置依赖服务,它由Util应用框架自动扫描配置.

    依赖配置扩展

    Util应用框架提供了三个接口,用于自动配置相应生命周期的依赖服务.

    • Util.Dependency.ISingletonDependency
      配置生命周期为 Singleton 的服务.
    • Util.Dependency.IScopeDependency
      配置生命周期为 Scope 的服务.
    • Util.Dependency.ITransientDependency
      配置生命周期为 Transient 的服务.

    限制: 必须把 ISingletonDependency 这三个接口放在需要配置的接口上,不能放在实现类上.

    范例:

    服务基接口 IService 继承了 IScopeDependency 接口.

    所有继承了 IService 的服务接口,在启动时自动查找相应的实现类,并设置为 Scope 服务.

    /// 
    /// 服务
    /// 
    public interface IService : IScopeDependency {
    }
    

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

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

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

    范例:

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

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

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

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

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

    服务定位器

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

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

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

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

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

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

    服务定位器概述

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

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

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

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

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

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

    /// 
    /// 应用服务
    /// 
    public abstract class ServiceBase : IService {
        /// 
        /// 初始化应用服务
        /// 
        /// 服务提供器
        protected ServiceBase( IServiceProvider serviceProvider ) {
            ServiceProvider = serviceProvider ?? throw new ArgumentNullException( nameof( serviceProvider ) );
            Session = serviceProvider.GetService() ?? NullSession.Instance;
            IntegrationEventBus = serviceProvider.GetService() ?? NullIntegrationEventBus.Instance;
            var logFactory = serviceProvider.GetService();
            Log = logFactory?.CreateLog( GetType() ) ?? NullLog.Instance;
        }
    
        /// 
        /// 服务提供器
        /// 
        protected IServiceProvider ServiceProvider { get; }
    
        /// 
        /// 用户会话
        /// 
        protected ISession Session { get; }
    
        /// 
        /// 集成事件总线
        /// 
        protected IIntegrationEventBus IntegrationEventBus { get; }
    
        /// 
        /// 日志操作
        /// 
        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 {
        /// 
        /// 获取客户端Ip地址
        /// 
        public static string GetIp() {
            var httpContext = Ioc.Create()?.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 {
        /// 
        /// 获取客户端Ip地址
        /// 
        public static string GetIp() {
            return Web.HttpContext?.Connection.RemoteIpAddress?.ToString();
        }
    }
    

    你可以通过 Web.HttpContext 获取当前Http上下文,比使用 Ioc.Create 方便得多.

    源码解析

    DependencyServiceRegistrar 依赖服务注册器

    依赖服务注册器提供对 Util.Dependency.ISingletonDependency 等接口的依赖配置扩展支持.

    通过类型查找器分别查找实现了 ISingletonDependency,IScopeDependency,ITransientDependency 三个接口的所有class.

    对每个class类,查找它们的接口,并注册相应生命周期的依赖关系.

    /// 
    /// 依赖服务注册器 - 用于扫描注册ISingletonDependency,IScopeDependency,ITransientDependency
    /// 
    public class DependencyServiceRegistrar : IServiceRegistrar {
        /// 
        /// 获取服务名
        /// 
        public static string ServiceName => "Util.Infrastructure.DependencyServiceRegistrar";
    
        /// 
        /// 排序号
        /// 
        public int OrderId => 100;
    
        /// 
        /// 是否启用
        /// 
        public bool Enabled => ServiceRegistrarConfig.IsEnabled( ServiceName );
    
        /// 
        /// 注册服务
        /// 
        /// 服务上下文
        public Action Register( ServiceContext serviceContext ) {
            return () => {
                serviceContext.HostBuilder.ConfigureServices( ( context, services ) => {
                    RegisterDependency( services, serviceContext.TypeFinder, ServiceLifetime.Singleton );
                    RegisterDependency( services, serviceContext.TypeFinder, ServiceLifetime.Scoped );
                    RegisterDependency( services, serviceContext.TypeFinder, ServiceLifetime.Transient );
                } );
            };
        }
    
        /// 
        /// 注册依赖
        /// 
        private void RegisterDependency( IServiceCollection services, ITypeFinder finder, ServiceLifetime lifetime ) {
            var types = GetTypes( finder );
            var result = FilterTypes( types );
            foreach ( var item in result )
                RegisterType( services, item.Item1, item.Item2, lifetime );
        }
    
        /// 
        /// 获取接口类型和实现类型列表
        /// 
        private List<(Type, Type)> GetTypes( ITypeFinder finder ) {
            var result = new List<(Type, Type)>();
            var classTypes = finder.Find();
            foreach ( var classType in classTypes ) {
                var interfaceTypes = Util.Helpers.Reflection.GetInterfaceTypes( classType, typeof( TDependencyInterface ) );
                interfaceTypes.ForEach( interfaceType => result.Add( (interfaceType, classType) ) );
            }
            return result;
        }
    
        /// 
        /// 过滤类型
        /// 
        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;
        }
    
        /// 
        /// 获取优先级类型
        /// 
        private (Type, Type) GetTypesByPriority( IGrouping 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 );
        }
    
        /// 
        /// 获取优先级
        /// 
        private int GetPriority( Type type ) {
            var attribute = type.GetCustomAttribute();
            if ( attribute == null )
                return 0;
            return attribute.Priority;
        }
    
        /// 
        /// 注册类型
        /// 
        private void RegisterType( IServiceCollection services, Type interfaceType, Type classType, ServiceLifetime lifetime ) {
            services.TryAdd( new ServiceDescriptor( interfaceType, classType, lifetime ) );
        }
    }
    

    Ioc 服务定位器工具类

    Ioc 工具类内置了一个对象容器,如果没有为它设置服务提供器,它将从内置对象容器获取依赖,这是导致副作用的根源.

    /// 
    /// 容器操作
    /// 
    public static class Ioc {
        /// 
        /// 容器
        /// 
        private static readonly Util.Dependency.Container _container = Util.Dependency.Container.Instance;
        /// 
        /// 获取服务提供器操作
        /// 
        private static Func _getServiceProviderAction;
    
        /// 
        /// 服务范围工厂
        /// 
        public static IServiceScopeFactory ServiceScopeFactory { get; set; }
    
        /// 
        /// 创建新容器
        /// 
        public static Util.Dependency.Container CreateContainer() {
            return new Util.Dependency.Container();
        }
    
        /// 
        /// 获取服务集合
        /// 
        public static IServiceCollection GetServices() {
            return _container.GetServices();
        }
    
        /// 
        /// 设置获取服务提供器操作
        /// 
        /// 获取服务提供器操作
        public static void SetServiceProviderAction( Func action ) {
            _getServiceProviderAction = action;
        }
    
        /// 
        /// 获取
        /// 
        public static IServiceProvider GetServiceProvider() {
            var provider = _getServiceProviderAction?.Invoke();
            if ( provider != null )
                return provider;
            return _container.GetServiceProvider();
        }
    
        /// 
        /// 创建对象
        /// 
        /// 对象类型
        public static T Create() {
            return Create( typeof( T ) );
        }
    
        /// 
        /// 创建对象
        /// 
        /// 返回对象类型
        /// 对象类型
        public static T Create( Type type ) {
            var service = Create( type );
            if( service == null )
                return default;
            return (T)service;
        }
    
        /// 
        /// 创建对象
        /// 
        /// 对象类型
        public static object Create( Type type ) {
            if( type == null )
                return null;
            var provider = GetServiceProvider();
            return provider.GetService( type );
        }
    
        /// 
        /// 创建对象集合
        /// 
        /// 返回类型
        public static List CreateList() {
            return CreateList( typeof( T ) );
        }
    
        /// 
        /// 创建对象集合
        /// 
        /// 返回类型
        /// 对象类型
        public static List CreateList( Type type ) {
            Type serviceType = typeof( IEnumerable<> ).MakeGenericType( type );
            var result = Create( serviceType );
            if( result == null )
                return new List();
            return ( (IEnumerable)result ).ToList();
        }
    
        /// 
        /// 创建服务范围
        /// 
        public static IServiceScope CreateScope() {
            var provider = GetServiceProvider();
            return provider.CreateScope();
        }
    
        /// 
        /// 清理
        /// 
        public static void Clear() {
            _container.Clear();
        }
    }
    

    Ioc 工具类需要获取正确的服务提供器,可以通过 SetServiceProviderAction 方法进行设置.

    对于 Asp.Net Core 环境, AspNetCoreServiceRegistrar 服务注册器已经正确设置Ioc工具类的服务提供器.

    但对于非 Asp.Net Core 环境, 设置正确的服务提供器可能非常困难.

    /// 
    /// AspNetCore服务注册器
    /// 
    public class AspNetCoreServiceRegistrar : IServiceRegistrar {
        /// 
        /// 获取服务名
        /// 
        public static string ServiceName => "Util.Infrastructure.AspNetCoreServiceRegistrar";
    
        /// 
        /// 排序号
        /// 
        public int OrderId => 200;
    
        /// 
        /// 是否启用
        /// 
        public bool Enabled => ServiceRegistrarConfig.IsEnabled( ServiceName );
    
        /// 
        /// 注册服务
        /// 
        /// 服务上下文
        public Action Register( ServiceContext serviceContext ) {
            serviceContext.HostBuilder.ConfigureServices( ( context, services ) => {
                RegisterHttpContextAccessor( services );
                RegisterServiceLocator();
            } );
            return null;
        }
    
        /// 
        /// 注册Http上下文访问器
        /// 
        private void RegisterHttpContextAccessor( IServiceCollection services ) {
            var httpContextAccessor = new HttpContextAccessor();
            services.TryAddSingleton( httpContextAccessor );
            Web.HttpContextAccessor = httpContextAccessor;
        }
    
        /// 
        /// 注册服务定位器
        /// 
        private void RegisterServiceLocator() {
            Ioc.SetServiceProviderAction( () => Web.ServiceProvider );
        }
    }
    

    禁用依赖服务注册器

    如果你不想自动扫描注册 ISingletonDependency,IScopeDependency,ITransientDependency 相关依赖,可以禁用它.

    ServiceRegistrarConfig.Instance.DisableDependencyServiceRegistrar();
    builder.AsBuild().AddUtil();
    
  • 相关阅读:
    ESP8266 Node Mcu开发板连接WIFI并上报数据到MQTT服务器——物联网应用开发
    数据分析编程检验——车流量统计(不能使用pandas和numpy)
    学习ArkTS --页面路由
    QMI8658A Datasheet Rev A-勘误表
    MySQL读取的记录和我想象的不一致——事物隔离级别和MVCC
    如何进行网络编程和套接字操作?
    SQL数据库设计 用语言查询数据
    matlab 小数据法求liyapunov指数
    【JavaScript-节点操作】什么是节点,节点操作怎么用,操作节点能干吗?
    Spring Cloud 学习笔记(1 / 3)
  • 原文地址:https://www.cnblogs.com/xiadao521/p/17805092.html