【17MKH】我在框架中对.Net依赖注入的扩展
说明
依赖注入(DI)是控制反转(IoC)的一种技术实现,它应该算是.Net中最核心,也是最基本的一个功能。但是官方只是实现了基本的功能和扩展方法,而我呢,在自己的框架 https://github.com/17MKH/Mkh 中,根据自己的使用习惯以及框架的约定,又做了进一步的封装。
依赖注入的生命周期
官方对注入的服务提供了三种生命周期
瞬时(Transient)
单例(Singleton)
范围(Scoped)
其中瞬时
以及单例
在所有类型的应用(Web,Client,Console等)中都可以使用,而范围
则比较特殊,它只能在Web类型的应用中使用,也就是单次请求只会创建一次。
对于三种生命周期,官方也提供了很多的扩展方法来方便大家注入服务,比如:
通过指定服务和实现注入:Add{LIFETIME}<{SERVICE}, {IMPLEMENTATION}>()
services.AddTransient<IMyDep, MyDep>();
services.AddSingleton<IMyDep, MyDep>();
services.AddScoped<IMyDep, MyDep>();
通过委托注入手动创建的实例:Add{LIFETIME}<{SERVICE}>(sp => new {IMPLEMENTATION})
services.AddTransient<IMyDep>(sp => new MyDep());
services.AddSingleton<IMyDep>(sp => new MyDep());
services.AddScoped<IMyDep>(sp => new MyDep());
虽然官方提供这些扩展方法挺好用,但是当你每次新增一个服务需要注入的时候,你都要手动添加注入的代码,大部分人喜欢把这些代码放到一个类中的方法里面,比如:
/// <summary>
/// 注入自定义服务
/// </summary>
/// <param name="services"></param>
private void ConfigureCustomServices(IServiceCollection services)
{
services.AddSingleton<IServiceA, ServiceA>();
services.AddSingleton<IServiceB, ServiceB>();
services.AddSingleton<IServiceC, ServiceC>();
services.AddSingleton<IServiceD, ServiceD>();
services.AddSingleton<IServiceE, ServiceE>();
....此处省略500行代码
}
我相信不少人都见过这种代码,虽然是统一管理服务注入的代码,并且看起来很规范,但是存在两个问题。
1、每次新增或者更改某个服务实现,都要找到这段代码并进行修改,如果新增的服务与该代码不在一个项目中,修改起来相对麻烦。
2、修改时容易出错,比如改错了要注入的服务,从而导致其它功能所需的服务出现异常。
而为了避免出现上述两个问题,我封装了第一个扩展点特性注入
。
使用特性注入替代集中式的注入方式
整体思路就是,将自定义的特性,添加到需要注入的服务实现上,然后在程序启动时通过反射来解析出需要注入的服务、对应实现以及注入方式。
服务注入时有两种情况:
1、注入接口和实现,比如
IAccountService
和AccountService
,注入时采用services.AddSingleton<IAccountService, AccountService>()
的方式;
2、使用类自身进行注入,比如
IAccountService
和AccountService
,注入时采用services.AddSingleton<AccountService>()
的方式,或者只有一个MoYuService
服务,注入时就是services.AddSingleton<MoYuService>()
;
首先,针对服务的三种生命周期以及上面的两种情况,我定义了三个特性:
单例注入特性 SingletonAttribute
using System;
namespace Mkh.Utils.Annotations;
/// <summary>
/// 单例注入(使用该特性的服务系统会自动注入)
/// </summary>
[AttributeUsage(AttributeTargets.Class)]
public class SingletonAttribute : Attribute
{
/// <summary>
/// 是否使用自身的类型进行注入
/// </summary>
public bool Itself { get; set; }
/// <summary>
///
/// </summary>
public SingletonAttribute()
{
Itself = false;
}
/// <summary>
/// 是否使用自身的类型进行注入
/// </summary>
/// <param name="itself"></param>
public SingletonAttribute(bool itself)
{
Itself = itself;
}
}
瞬时注入特性 TransientAttribute
using System;
namespace Mkh.Utils.Annotations;
/// <summary>
/// 瞬时注入(使用该特性的服务系统会自动注入)
/// </summary>
[AttributeUsage(AttributeTargets.Class)]
public class TransientAttribute : Attribute
{
/// <summary>
/// 是否使用自身的类型进行注入
/// </summary>
public bool Itself { get; set; }
/// <summary>
///
/// </summary>
public TransientAttribute()
{
}
/// <summary>
/// 是否使用自身的类型进行注入
/// </summary>
/// <param name="itself"></param>
public TransientAttribute(bool itself = false)
{
Itself = itself;
}
}
范围注入特性 ScopedAttribute
using System;
namespace Mkh.Utils.Annotations;
/// <summary>
/// 单例注入(使用该特性的服务系统会自动注入)
/// </summary>
[AttributeUsage(AttributeTargets.Class)]
public class ScopedAttribute : Attribute
{
/// <summary>
/// 是否使用自身的类型进行注入
/// </summary>
public bool Itself { get; set; }
/// <summary>
///
/// </summary>
public ScopedAttribute()
{
}
/// <summary>
/// 是否使用自身的类型进行注入
/// </summary>
/// <param name="itself"></param>
public ScopedAttribute(bool itself = false)
{
Itself = itself;
}
}
对于上面说到的注入自身的情况,一种时要注入的服务本身没有继承任何接口,那么只需要添加对应的特性即可,还有一种情况是服务继承了某个接口,这个时候就会用到特性类的Itself
属性了,当添加注入特性并把该属性设置为true时,则可以实现上面说的效果。
接下来举几个例子:
首先,在我自己的框架中,包含了一些常用的帮助类,在上古时代,这些类中的方法基本都是定义成静态方法的,而在.Net Core中,则采用单例注入的方式,为了能够方便的注入,我给这些类都是用了单例特性注入,比如程序集操作帮助类AssemblyHelper.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Runtime.Loader;
using Microsoft.Extensions.DependencyModel;
using Mkh.Utils.Annotations;
namespace Mkh.Utils.Helpers;
/// <summary>
/// 程序集操作帮助类
/// </summary>
[Singleton]
public class AssemblyHelper
{
/// <summary>
/// 加载程序集
/// </summary>
/// <returns></returns>
public List<Assembly> Load(Func<RuntimeLibrary, bool> predicate = null)
{
var list = DependencyContext.Default.RuntimeLibraries.ToList();
if (predicate != null)
list = DependencyContext.Default.RuntimeLibraries.Where(predicate).ToList();
return list.Select(m =>
{
try
{
return AssemblyLoadContext.Default.LoadFromAssemblyName(new AssemblyName(m.Name));
}
catch
{
return null;
}
}).Where(m => m != null).ToList();
}
/// <summary>
/// 根据名称结尾查询程序集
/// </summary>
/// <param name="endString"></param>
/// <returns></returns>
public Assembly LoadByNameEndString(string endString)
{
return Load(m => m.Name.EndsWith(endString)).FirstOrDefault();
}
/// <summary>
/// 获取当前程序集的名称
/// </summary>
/// <returns></returns>
public string GetCurrentAssemblyName()
{
return Assembly.GetCallingAssembly().GetName().Name;
}
}
更多案例,您可以参考代码https://github.com/17MKH/Mkh/tree/main/src/01_Utils/Utils/Helpers
PS:至于为什么使用单例注入而不是静态方法,我也不知道,我只知道dudu老大这么说的~
上面特性以及使用方式都讲了,下面就贴一下反射注入的代码吧,其它就没啥要讲的了,代码一看就懂~
https://github.com/17MKH/Mkh/blob/main/src/01_Utils/Utils/ServiceCollectionExtensions.cs
using System;
using System.Linq;
using System.Reflection;
using Microsoft.Extensions.DependencyInjection;
using Mkh.Utils.Annotations;
using Mkh.Utils.Helpers;
namespace Mkh.Utils;
public static class ServiceCollectionExtensions
{
/// <summary>
/// 从指定程序集中注入服务
/// </summary>
/// <param name="services"></param>
/// <param name="assembly"></param>
/// <returns></returns>
public static IServiceCollection AddServicesFromAssembly(this IServiceCollection services, Assembly assembly)
{
foreach (var type in assembly.GetTypes())
{
#region ==单例注入==
var singletonAttr = (SingletonAttribute)Attribute.GetCustomAttribute(type, typeof(SingletonAttribute));
if (singletonAttr != null)
{
//注入自身类型
if (singletonAttr.Itself)
{
services.AddSingleton(type);
continue;
}
var interfaces = type.GetInterfaces().Where(m => m != typeof(IDisposable)).ToList();
if (interfaces.Any())
{
foreach (var i in interfaces)
{
services.AddSingleton(i, type);
}
}
else
{
services.AddSingleton(type);
}
continue;
}
#endregion
#region ==瞬时注入==
var transientAttr = (TransientAttribute)Attribute.GetCustomAttribute(type, typeof(TransientAttribute));
if (transientAttr != null)
{
//注入自身类型
if (transientAttr.Itself)
{
services.AddSingleton(type);
continue;
}
var interfaces = type.GetInterfaces().Where(m => m != typeof(IDisposable)).ToList();
if (interfaces.Any())
{
foreach (var i in interfaces)
{
services.AddTransient(i, type);
}
}
else
{
services.AddTransient(type);
}
continue;
}
#endregion
#region ==Scoped注入==
var scopedAttr = (ScopedAttribute)Attribute.GetCustomAttribute(type, typeof(ScopedAttribute));
if (scopedAttr != null)
{
//注入自身类型
if (scopedAttr.Itself)
{
services.AddSingleton(type);
continue;
}
var interfaces = type.GetInterfaces().Where(m => m != typeof(IDisposable)).ToList();
if (interfaces.Any())
{
foreach (var i in interfaces)
{
services.AddScoped(i, type);
}
}
else
{
services.AddScoped(type);
}
}
#endregion
}
return services;
}
/// <summary>
/// 扫描并注入所有使用特性注入的服务
/// </summary>
/// <param name="services"></param>
/// <returns></returns>
public static IServiceCollection AddServicesFromAttribute(this IServiceCollection services)
{
var assemblies = new AssemblyHelper().Load();
foreach (var assembly in assemblies)
{
try
{
services.AddServicesFromAssembly(assembly);
}
catch
{
//此处防止第三方库抛出一场导致系统无法启动,所以需要捕获异常来处理一下
}
}
return services;
}
}
按照框架约定注入
无论时谁做的框架,为了达到简单易用的效果,肯定都包含一些规范和约定,然后根据这些规范和约定进行一些封装。比如我的框架中,每个业务模块都会包含很多仓储(Repository)和应用服务(Service),而且都需要注入,虽然可以使用上面介绍的特性注入的方式,但是还是有点麻烦。我的业务模块中的仓储和应用服务都是按照约定,存放在指定的目录结构中,并且采用规定的命名方式,比如仓储必须以Repository
结尾,服务必须以Service
结尾,既然有了这些约定,那么我完全可以在启动时,通过反射来统一扫描所有模块中的仓储和服务并注入。
具体就不再详说了,因为牵扯到业务模块化的相关内容,下面贴一段代码
https://github.com/17MKH/Mkh/blob/main/src/02_Data/Data.Core/DbBuilder.cs
/// <summary>
/// 加载仓储
/// </summary>
private void LoadRepositories()
{
if (_repositoryAssemblies.IsNullOrEmpty())
return;
foreach (var assembly in _repositoryAssemblies)
{
/*
* 仓储约定:
* 1、仓储统一放在Repositories目录中
* 2、仓储默认使用SqlServer数据库,如果数据库之间有差异无法通过ORM规避时,采用以下方式解决:
* a)将对应的方法定义为虚函数
* b)假如当前方法在MySql中实现有差异,则在Repositories新建一个MySql目录
* c)在MySql目录中新建一个仓储(我称之为兼容仓储)并继承默认仓储
* d)在新建的兼容仓储中使用MySql语法重写对应的方法
*/
var repositoryTypes = assembly.GetTypes()
.Where(m => !m.IsInterface && typeof(IRepository).IsImplementType(m))
//排除兼容仓储
.Where(m => m.FullName!.Split('.')[^2].EqualsIgnoreCase("Repositories"))
.ToList();
//兼容仓储列表
var compatibilityRepositoryTypes = assembly.GetTypes()
.Where(m => !m.IsInterface && typeof(IRepository).IsImplementType(m))
//根据数据库类型来过滤
.Where(m => m.FullName!.Split('.')[^2].EqualsIgnoreCase(Options.Provider.ToString()))
.ToList();
foreach (var type in repositoryTypes)
{
//按照框架约定,仓储的第三个接口类型就是所需的仓储接口
var interfaceType = type.GetInterfaces()[2];
//按照约定,仓储接口的第一个接口的泛型参数即为对应实体类型
var entityType = interfaceType.GetInterfaces()[0].GetGenericArguments()[0];
//保存实体描述符
DbContext.EntityDescriptors.Add(new EntityDescriptor(DbContext, entityType));
//优先使用兼容仓储
var implementationType = compatibilityRepositoryTypes.FirstOrDefault(m => m.Name == type.Name) ?? type;
Services.AddScoped(interfaceType, sp =>
{
var instance = Activator.CreateInstance(implementationType);
var initMethod = implementationType.GetMethod("Init", BindingFlags.Instance | BindingFlags.NonPublic);
initMethod!.Invoke(instance, new Object[] { DbContext });
//保存仓储实例
var manager = sp.GetService<IRepositoryManager>();
manager?.Add((IRepository)instance);
return instance;
});
//保存仓储描述符
DbContext.RepositoryDescriptors.Add(new RepositoryDescriptor(entityType, interfaceType, implementationType));
}
}
}
https://github.com/17MKH/Mkh/blob/main/src/03_Module/Module.Core/ServiceCollectionExtensions.cs
/// <summary>
/// 添加应用服务
/// </summary>
/// <param name="services"></param>
/// <param name="module"></param>
public static IServiceCollection AddApplicationServices(this IServiceCollection services, ModuleDescriptor module)
{
var assembly = module.LayerAssemblies.Core;
//按照约定,应用服务必须采用Service结尾
var implementationTypes = assembly.GetTypes().Where(m => m.Name.EndsWith("Service") && !m.IsInterface).ToList();
foreach (var implType in implementationTypes)
{
//按照约定,服务的第一个接口类型就是所需的应用服务接口
var serviceType = implType.GetInterfaces()[0];
services.AddScoped(implType);
module.ApplicationServices.Add(serviceType, implType);
}
return services;
}
好了,以上就是我在自己的框架中,对依赖注入进行的一些扩展,如果您有更好的方式,欢迎交流~
广告
17MKH,全称一起模块化,江湖人称一起骂客户,是一个基于.Net6+Vue3开发的业务模块化前后端分离快速开发框架,前身时NetModular框架,有兴趣的可以看一看,最好是能给个小小的star~
GitHub:https://github.com/17MKH/Mkh
Gitee:https://gitee.com/mkh_yes/mkh
【17MKH】我在框架中对.Net依赖注入的扩展的更多相关文章
- 为什么多线程、junit 中无法使用spring 依赖注入?
为什么多线程.junit 中无法使用spring 依赖注入? 这个问题,其实体现了,我们对spring已依赖太深,以至于不想自己写实例了. 那么到底是为什么在多线程和junit单元测试中不能使用依赖注 ...
- Java自动化测试框架-09 - TestNG之依赖注入篇 (详细教程)
1.-依赖注入 TestNG支持两种不同类型的依赖项注入:本机(由TestNG本身执行)和外部(由诸如Guice的依赖项注入框架执行). 1.1-本机依赖项注入 TestNG允许您在方法中声明其他参数 ...
- 深入浅出spring IOC中三种依赖注入方式
深入浅出spring IOC中三种依赖注入方式 spring的核心思想是IOC和AOP,IOC-控制反转,是一个重要的面向对象编程的法则来消减计算机程序的耦合问题,控制反转一般分为两种类型,依赖注入和 ...
- 转:深入浅出spring IOC中四种依赖注入方式
转:https://blog.csdn.net/u010800201/article/details/72674420 深入浅出spring IOC中四种依赖注入方式 PS:前三种是我转载的,第四种是 ...
- 第四节:配置的读取、StartUp类、内置依赖注入和扩展改造
一. 配置的读取 在Asp.Net Core中,有一个 appsettings.json 文件,用于存储相应的配置信息,读取的时,要通过构造函数注入:IConfiguration Configurat ...
- ASP.NET Core中如影随形的”依赖注入”[下]: 历数依赖注入的N种玩法
在对ASP.NET Core管道中关于依赖注入的两个核心对象(ServiceCollection和ServiceProvider)有了足够的认识之后,我们将关注的目光转移到编程层面.在ASP.NET ...
- ASP.NET Core中如影随形的”依赖注入”[上]: 从两个不同的ServiceProvider说起
我们一致在说 ASP.NET Core广泛地使用到了依赖注入,通过前面两个系列的介绍,相信读者朋友已经体会到了这一点.由于前面两章已经涵盖了依赖注入在管道构建过程中以及管道在处理请求过程的应用,但是内 ...
- spring框架--IOC容器,依赖注入
思考: 1. 对象创建创建能否写死? 2. 对象创建细节 对象数量 action 多个 [维护成员变量] service 一个 [不需要维护公共变量] dao 一个 [不需要维护 ...
- Dora.Interception,为.NET Core度身打造的AOP框架 [4]:与依赖注入框架的无缝集成
Dora.Interception最初的定位就是专门针对.NET Core的AOP框架,所以在整个迭代过程中我大部分是在做减法.对于.NET Core程序开发来说,依赖注入已经成为无处不在并且“深入骨 ...
随机推荐
- vue-cli安装记录
docker安装 docker network rm mydkdocker network create --subnet=192.168.1.0/24 mydk cat centos-7-x86_ ...
- javascript将平行的拥有上下级关系的数据转换成树形结构
转换函数 var Littlehow = {}; /** * littlehow 2019-05-15 * 平行数据树形转换器 * @type {{format: tree.format, sort: ...
- ubuntu 使用mysql
一: 安装: sudo apt-get install mysql-serversudo apt-get install mysql-clientsudo apt-get install libmys ...
- 为什么volatile能保证有序性不能保证原子性
对于内存模型的三大特性:有序性.原子性.可见性. 大家都知道volatile能保证可见性和有序性但是不能保证原子性,但是为什么呢? 一.原子性.有序性.可见性 1.原子性: (1)原子的意思代表着-- ...
- uni-app使用腾讯地图注意点
地图map组件使用腾讯地图自定义样式: 1:在使用地图map组件腾讯地图时,获取本地定位,经纬度转地址与地址转经纬度解析时,小程序可以直接使用.但是h5版本会报跨域问题,目前前端没有找到更好的解决方法 ...
- 【模型推理】Tengine 模型转换及量化
欢迎关注我的公众号 [极智视界],回复001获取Google编程规范 O_o >_< o_O O_o ~_~ o_O 本文介绍一下 Tengine 模型转换 ...
- react-hook简单使用
一.函数式组件创建 function HelloComponent(props, /* context */) { return <div>Hello {props.name}</d ...
- iOS 实现简单的界面切换
以下是在iOS中最简单的界面切换示例.使用了多个Controller,并演示Controller之间在切换界面时的代码处理. 实现的应用界面: 首先,创建一个window-based applicat ...
- CF581B Luxurious Houses 题解
Content 一条大街上有 \(n\) 个房子,第 \(i\) 个房子的楼层数量是 \(h_i\).如果一个房子的楼层数量大于位于其右侧的所有房屋,则房屋是豪华的.对于第 \(i\) 个房子,请求出 ...
- CF1506C Double-ended Strings 题解
Content 有两个字符串 \(a,b\).我们每次操作可以将两个字符串中的一个字符串的最前面一个字符或这最后面一个字符删去(可以将某个字符串通过若干次操作变为空串).求需要多少次操作才能够使 \( ...