說明
依賴注入(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