• 【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、注入接口和实现,比如IAccountServiceAccountService,注入时采用services.AddSingleton<IAccountService, AccountService>()的方式;

    2、使用类自身进行注入,比如IAccountServiceAccountService,注入时采用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

    为之则易,不为则难
  • 相关阅读:
    linux-满足多字符条件统计行数
    oracle的面试问题
    在开发过程中为什么需要写存储过程
    面向对象编程
    动态SQL
    触发器

    子程序
    游标
    集合
  • 原文地址:https://www.cnblogs.com/oldli/p/15769318.html
Copyright © 2020-2023  润新知