天天看點

【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老大這麼說的~

【17MKH】我在架構中對.Net依賴注入的擴充

上面特性以及使用方式都講了,下面就貼一下反射注入的代碼吧,其它就沒啥要講的了,代碼一看就懂~

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

為之則易,不為則難

繼續閱讀