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; }
}
应用服务基类定义了用户会话和日志操作等依赖项,但不是从构造方法获取的,而是调用服务提供程序 IServiceProvider 的 GetService 方法.
通过传递 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();