首页 > 其他分享 >.Net 依赖注入深入探索,做一个DI拓展,实现一个简易灵活的 自动依赖注入框架xy

.Net 依赖注入深入探索,做一个DI拓展,实现一个简易灵活的 自动依赖注入框架xy

时间:2024-10-01 14:00:54浏览次数:8  
标签:依赖 服务 DI 作用域 注册 注入

合集 - .Net 框架探索(2)1..Net Web项目中,实现轻量级本地事件总线 框架09-27:悠兔机场2..Net 依赖注入深入探索,做一个DI拓展,实现一个简易灵活的 自动依赖注入框架09-30收起

一、依赖注入相关知识

1.1、依赖注入的原理和优点

  • 依赖注入(DI),是IOC控制反转思想 的实现。由一个DI容器,去统一管理所有的服务生命周期,服务的创建、销毁、获取,都是由DI容器去处理的。
  • 依赖注入,很大程度解耦了服务之间的依赖关系,服务之间依赖的是抽象(依赖的是 服务/服务接口 的 “类型”),而不是依赖具体的实现(服务不用关注他依赖的服务的创建,仅通过构造函数声明依赖的服务类型即可拿到依赖的服务实例,实际的服务实例是由容器去创建出来的)。
  • 在获取服务时,DI能够解析所有服务依赖关系树,将所有直接、间接 依赖的服务都创建了出来(不同的生命周期类型,创建的时机不同,见1.3),可以直接使用。
  • 优点:解耦、生命周期管理、提高可维护性、服务可替代性。(服务之间的依赖仅是类型,不关注依赖服务的创建,因此任何服务的改动,之间的影响是非常微小的,并且依赖的接口服务,在注册时候接口的实现可以直接替换。)
  • .net framework中 new() 创建服务,缺点:服务之间依赖的是具体的实现,依赖的服务需要手动创建出来,任何服务的改动,影响都是非常多的地方。当服务依赖层级很深的时候,外层服务使用底层服务,需要按个从低到把所有依赖的服务都创建出来。当其中某个服务构造函数发生变化时,所有用到他的地方,创建都需要更改,影响会很大,不利于维护。并且需要手动控制一些非托管资源服务的生命周期,需要注意内存泄漏的问题。

1.2、.Net 服务注册

  • 1.2.1、服务类型/服务接口类型 注册
// 将服务注册为接口 
services.AddTransient(); // 泛型注册
services.AddScoped();
services.AddSingleton();

services.AddTransient(); // 直接注册服务

service.AddTransient(typeof(IMyService),typeof(MyService)); // 类型注册
service.AddTransient(typeof(MyService));

  • 1.2.2、服务实例注册(仅适用于单例模式服务)
MyService myService = new MyService(); // 先new一个实例,然后注册为单例模式服务
services.AddSingleton(myService); // 直接注册
services.AddSingleton(myService); // 注册为接口

  • 1.2.3、ServiceDescriptor 使用服务描述类,注册
builder.Services.Add(new ServiceDescriptor(typeof(IMyService), typeof(MyService), ServiceLifetime.Transient)); // 注册的服务类型,服务的实现类型,服务的生命周期
builder.Services.Add(ServiceDescriptor.Transient(typeof(IMyService), typeof(MyService)));

// 替换服务,已存在就替换;不存在,就新增
builder.Services.Replace(ServiceDescriptor.Transient(typeof(IMyService), typeof(MyService))); // Replace:如果IMyService服务已存在,就替换;不存在,就新增。

1.3、服务的三种生命周期类型,ServiceLifetime 枚举

Singleton 单例模式

  • 从DI中拿到该服务,任何时候拿到的都是同一个实例。
  • 单例服务,一般是程序运行时就创建,也可指定为第一次访问该服务时创建。单例服务 程序不会自动释放,一般情况下不需要释放;但需要注意内存问题,若包含非托管资源,需要注意避免内存泄漏问题。
  • 单例服务,不可直接依赖作用域服务。因为单例服务在程序运行时创建,这时候并没有任何服务作用域,直接依赖作用域服务会在程序运行时就抛异常。
  • 若单例服务,需要用到作用域服务,通常需要在合适的时机创建一个服务作用域,通过服务作用域的DI,拿到作用域服务。此作用域也需要在合适的时机手动释放掉。

Scoped 作用域模式

  • DI的同一个服务作用域中,拿到的是同一个实例。
  • 在服务作用域创建时,会将所有作用域服务都创建出来。作用域释放时候,会释放掉。
  • .Net Web程序 底层,所有控制器类都会自动注册为作用域服务。在每次发起http请求时,都会自动创建一个当前http请求的服务作用域,在请求结束时候,释放掉。

Transient 瞬时模式

  • 从DI中,每次获取该服务时,都会创建一个新实例,每次获取到该服务都不是同一实例。
  • 当服务不再被引用时,GC会自动回收释放;或者当服务作用域结束时候,也会释放掉瞬时服务。

1.6、服务注入、服务获取

  • 常规是构造函数注入,也有第三方依赖注入框架如:Autofac 等,实现了属性注入。但.net 推荐做法还是常规构造函数注入。
  • 通过 IServiceProvider,可以拿到所有注册的服务。
public class EFCoreController : ControllerBase
{
    IMyTestService _myTestService;
    IServiceProvider _serviceProvider; // DI服务提供器,是单例模式。通过他可以获取到DI中所有单例、瞬时服务。若在某个服务作用域内,则也可以获取到所有作用域服务。此处控制器中,是在http请求的服务作用域内。
    public EFCoreController(
        IServiceProvider serviceProvider,
        IMyTestService myTestService)
    {
        _myTestService = serviceProvider;
        _myTestService = myTestService;
        _serviceProvider.GetService<T>(); // 从DI中获取服务,若服务未创建,获取不到,返回 null
        _serviceProvider.GetRequiredService<T>(); // 从DI中获取服务,若服务未创建,获取不到,则会抛异常
    }
}

1.5、依赖注入高级用法

  • 手动创建服务作用域,拿到作用域服务实例(通常用于在单例服务中,需要用到作用域服务的情况)
    (如:RabbitMQ的发布订阅,在BackgroundService后台任务(后台任务是单例模式)中注册RabbitMQ消费者,RabbitMQ发布事件,后台任务中的消费者接受消息时,可以通过消息中的 人员信息 等其他信息,手动创建一个服务作用域,并手动特殊处理一些有状态的服务,比如用户信息上下文等。在消息消费完成时,释放服务作用域。)
var app = builder.Build();

IServiceProvider rootServiceProvider = app.Services; // web程序运行时的DI,可以访问单例服务和瞬时服务,但无法访问作用域服务。
using (IServiceScope serviceScope = rootServiceProvider.CreateScope()) // IServiceProvider.CreateScope() 创建一个服务作用域,此时所有作用域服务都会创建出来。
{
    IServiceProvider serviceProvider = serviceScope.ServiceProvider; // 使用服务作用域中的DI,可以访问 作用域服务
    TestScopeService? testScopeService = serviceProvider.GetService(); // 测试拿到作用域服务实例
}

二、设计一个轻量级 自动依赖注入框架 【设计目标】

2.1、设计目标

  • 将想要注入的服务,标注一个特性,就可以直接注入到DI中
  • 该特性可以指定将该服务注册为某个接口的实现
  • 该特性可以指定该服务的生命周期类型
  • 在DI中,一键注入整个程序集中所有指定了该特性的服务

2.2、使用示例

2.2.1、在要注入的服务类上,标注CoreDI特性,指定要注册的服务类型、注册的服务生命周期类型

 [CoreDI(typeof(ITestSigletonService), ServiceLifetime.Singleton)] // 将该服务以接口形式注册到DI(这个类必须是该接口的实现);指定生命周期类型
 public class TestSingletonService: ITestSigletonService
 {
     public void Test()
     {
         Console.WriteLine("单例服务");
     }
 }

 [CoreDI]  // 不指定接口,直接注册该类;不指定生命周期类型,默认是作用域生命周期
 public class TestScopeService
 {
     public void Test()
     {
         Console.WriteLine("作用域服务");
     }
 }

2.2.2、将当前程序集类所有标注了CoreDI特性的服务,全部注入到DI

builder.Services.AddCoreDIServices(typeof(TestSingletonService).Assembly); 

三、设计一个轻量级 自动依赖注入框架 【代码实现】

3.1、CoreDIAttribute

/// 
/// 特性 将服务注入到DI中 
/// 
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
public class CoreDIAttribute : Attribute
{
    /// 
    /// 要注入的接口类型
    /// 
    public Type? InterfaceType { get; private set; }
    /// 
    /// 服务的生命周期
    /// 
    public ServiceLifetime ServiceLifetime { get; private set; }

    public CoreDIAttribute(Type? interfaceType = null, ServiceLifetime serviceLifeTime = ServiceLifetime.Scoped)
    {
        if (!Enum.IsDefined(serviceLifeTime))
            throw new Exception($"【CoreDI】 The Enum '{nameof(ServiceLifetime)}' value error.");
        InterfaceType = interfaceType;
        ServiceLifetime = serviceLifeTime;
    }
}

3.2、CoreDIServiceExtensions

/// 
/// 依赖注入 拓展
/// 
public static class CoreDIServiceExtensions
{
    /// 
    /// 将这些程序集中带有CoreDI特性的服务自动注入到DI容器
    /// 
    public static void AddCoreDIServices(this IServiceCollection services, params Assembly[] assemblies)
    {
        // 找到这些程序集中包含CoreDI特性的所有服务,注册到DI
        foreach (var assembly in assemblies)
        {
            Dictionary> dic = assembly.GetTypes().ToDictionary(x => x, x => x.GetCustomAttributes());
            foreach (var item in dic)
            {
                Type type = item.Key;
                IEnumerable attributes = item.Value;
                if (attributes.Count() == 0)
                    continue;
                foreach (CoreDIAttribute di in attributes)
                {
                    if (di.InterfaceType != null && !type.GetInterfaces().Any(x => x == di.InterfaceType)) // 若服务注册为接口形式,则必须是该接口的实现类
                        throw new Exception($"【CoreDI】 The Service '{type?.Name}' can not be resolving to the Service '{di.InterfaceType.Name}' ");
                    services.Replace(new ServiceDescriptor(di.InterfaceType ?? type, type, di.ServiceLifetime)); // 使用服务描述类 ServiceDescriptor 将服务注册到DI,存在就替换,不存在就新增;从CoreDI特性中拿到指定的生命周期类型
                }
            }
        }
    }
}

标签:依赖,服务,DI,作用域,注册,注入
From: https://www.cnblogs.com/westworldss/p/18442843

相关文章

  • 243 Adding Route Transitions
    步骤1、BaseDialog.vue添加动画添加transition,命名为dialog,添加相关css<template><teleportto="body"><divv-if="show"@click="tryClose"class="backdrop"></div><transitionname="dialog&qu......
  • Codeforces Round 958 (Div. 2)
    手速前三题,D题想到树形dp但是没做出来。A.SplittheMultiset题意给你一个多集,一开始这个多集里只有一个数字。每次操作可以选择删掉多集里的一个数,然后添加\(k\)个数,并且使得这\(k\)个数的和等于删掉的数。问你最少需要操作多少次多集里的数的个数等于等于\(n\)。思路......
  • 解决MacOS 13.0.1 苹果M1芯片 导入pyaudio报错的问题
    【问题】如果正常按照网上的教程,在terminal先使用brew安装portaudio(brewinstallportaudio),再使用pip在conda环境里安装pyaudio(pipinstallpyaudio),然后python直接导入pyaudio(importpyaudio)会报错如下:【分析】可知报错来自于portaudio动态库。网上搜索解决方案,除了重装、重启......
  • 系统找不到gepdit.msc的解决方法
    关于gpedit.msc文件打不开,Windows说找不到该文件,主要原因是Windows系统版本家庭版不具备这个功能。1.点击最上面的查看然后在文件扩展名前面打钩2.然后在桌面空白处右击新建文本文档3.把下面代码全选复制粘贴进去保存4.在点击这个文件夹修改后缀把.txt修改成......
  • .Net 依赖注入深入探索,做一个DI拓展,实现一个简易灵活的 自动依赖注入框架
    合集-.Net框架探索(2) 1..NetWeb项目中,实现轻量级本地事件总线框架09-272..Net依赖注入深入探索,做一个DI拓展,实现一个简易灵活的自动依赖注入框架09-30收起 一、依赖注入相关知识1.1、依赖注入的原理和优点依赖注入(DI),是IOC控制反转思想的实现。由一个D......
  • VMware Tanzu Kubernetes Grid Integrated Edition (TKGI) 1.20.0 - 运营商 Kubernete
    VMwareTanzuKubernetesGridIntegratedEdition(TKGI)1.20.0-运营商Kubernetes解决方案Kubernetes-basedcontainersolutionwithadvancednetworking,aprivatecontainerregistry,andlifecyclemanagement请访问原文链接:https://sysin.org/blog/vmware-tkgi/,......
  • 第24天sql注入(小迪安全学习)
    前置知识(搭建环境失败,搞不了实验了,学下理论知识吧)sql注入脚本代码在实现代码与数据库进行数据通讯时(从数据库中取出相关数据),将定义的SQL语句进行执行查询数据时其中的SQL语句能通过参数传递自定义值来实现控制sQL语句,从而执行恶意的SQL语句可以实现查询其他数据(数据库中的敏......
  • Cannon-es.js之Distance Constrait模拟布料
    本文目录前言最终效果1、Particle2、前置代码准备2.1代码2.2效果3、使用距离约束模拟布料3.1代码3.2效果前言在现代Web开发中,实现逼真的物理效果对于提升用户体验至关重要。Cannon-es.js,作为Cannon.js的ES6模块版本,凭借其轻量级、高性能和易于集成的特点,在Web......
  • 高级java每日一道面试题-2024年9月30日-服务器篇[Redis篇]-Redis持久化有几种方式?
    如果有遗漏,评论区告诉我进行补充面试官:Redis持久化有几种方式?我回答:Redis是一个高性能的键值存储系统,常用于缓存、消息队列和实时数据分析等场景。为了保证数据的持久性,Redis提供了两种主要的持久化方式:RDB(RedisDatabaseBackup)和AOF(AppendOnlyFile)。这两种方......
  • redis的数据结构,内存处理,缓存问题
    redisObjectredis任意数据的key和value都会被封装为一个RedisObject,也叫redis对象:这就redis的头信息,占有16个字节redis中有两个热门数据结构1.SkipList,跳表,首先是链表,和普通链表有以下差异:元素按照升序排列存储节点可能包含多个指针,指针跨度不同那么跳表的特点有以下:......