写点什么

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

作者:何镇汐
  • 2023-11-11
    四川
  • 本文字数:9155 字

    阅读完需:约 30 分钟

依赖注入


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

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

依赖注入最佳实践

一个接口配置一个实现

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


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


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


/// <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();
复制代码


用户头像

何镇汐

关注

15年以上.Net开发经验,擅长代码封装 2023-11-01 加入

15年以上.Net开发经验,擅长代码封装,主要作品为Util应用框架

评论

发布
暂无评论
Util应用框架基础(一)依赖注入_开源_何镇汐_InfoQ写作社区