關(guān)于從零設(shè)計(jì) .NET 開(kāi)發(fā)框架 作者:癡者工良 教程說(shuō)明: 倉(cāng)庫(kù)地址:https://github.com/whuanle/maomi 文檔地址:https://maomi.whuanle.cn 作者博客: https://www.whuanle.cn https://www.cnblogs.co
關(guān)于從零設(shè)計(jì) .NET 開(kāi)發(fā)框架
作者:癡者工良
教程說(shuō)明:
倉(cāng)庫(kù)地址: https://github.com/whuanle/maomi
文檔地址: https://maomi.whuanle.cn
作者博客:
https://www.cnblogs.com/whuanle
基于 ASP.NET Core 開(kāi)發(fā)的 Web 框架中,最著名的是 ABP,ABP 主要特點(diǎn)之一開(kāi)發(fā)不同項(xiàng)目(程序集)時(shí),在每個(gè)項(xiàng)目中創(chuàng)建一個(gè)模塊類(lèi),程序加載每個(gè)程序集中,掃描出所有的模塊類(lèi),然后通過(guò)模塊類(lèi)作為入口,初始化程序集。
使用模塊化開(kāi)發(fā)程序,好處是不需要關(guān)注程序集如何加載配置。開(kāi)發(fā)人員開(kāi)發(fā)程序集時(shí),在模塊類(lèi)中配置如何初始化、如何讀取配置,使用者只需要將模塊類(lèi)引入進(jìn)來(lái)即可,由框架自動(dòng)啟動(dòng)模塊類(lèi)。
Maomi.Core 也提供了模塊化開(kāi)發(fā)的能力,同時(shí)還包括簡(jiǎn)單易用的自動(dòng)服務(wù)注冊(cè)。Maomi.Core 是一個(gè)很簡(jiǎn)潔的包,可以在控制臺(tái)、Web 項(xiàng)目、WPF 項(xiàng)目中使用,在 WPF 項(xiàng)目中結(jié)合 MVVM 可以大量減少代碼復(fù)雜度,讓代碼更加清晰明朗。
有 Demo1.Api、Demo1.Application 兩個(gè)項(xiàng)目,每個(gè)項(xiàng)目都有一個(gè)模塊類(lèi),模塊類(lèi)需要實(shí)現(xiàn) IModule 接口。
Demo1.Application 項(xiàng)目的 ApplicationModule.cs 文件內(nèi)容如下:
public class ApplicationModule : IModule
{
// 模塊類(lèi)中可以使用依賴(lài)注入
private readonly IConfiguration _configuration;
public ApplicationModule(IConfiguration configuration)
{
_configuration = configuration;
}
public void ConfigureServices(ServiceContext services)
{
// 這里可以編寫(xiě)模塊初始化代碼
}
}
如果要將服務(wù)注冊(cè)到容器中,在 class 上加上
[InjectOn]
特性即可。
public interface IMyService
{
int Sum(int a, int b);
}
[InjectOn] // 自動(dòng)注冊(cè)的標(biāo)記
public class MyService : IMyService
{
public int Sum(int a, int b)
{
return a + b;
}
}
上層模塊 Demo1.Api 中的 ApiModule.cs 可以通過(guò)特性注解引用底層模塊。
[InjectModule]
public class ApiModule : IModule
{
public void ConfigureServices(ServiceContext services)
{
// 這里可以編寫(xiě)模塊初始化代碼
}
}
最后,在程序啟動(dòng)時(shí)配置模塊入口,并進(jìn)行初始化。
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
// 注冊(cè)模塊化服務(wù),并設(shè)置 ApiModule 為入口
builder.Services.AddModule();
var app = builder.Build();
在 ASP.NET Core 配置 Host 時(shí),會(huì)自動(dòng)注入一些框架依賴(lài)的服務(wù),如 IConfiguration 等,因此在
.AddModule
開(kāi)始初始化模塊服務(wù)時(shí),模塊獲取已經(jīng)注入的服務(wù)。
每個(gè)模塊都需要實(shí)現(xiàn) IModule 接口,其定義如下:
///
/// 模塊接口
///
public interface IModule
{
///
/// 模塊中的依賴(lài)注入
///
/// 模塊服務(wù)上下文
void ConfigureServices(ServiceContext context);
}
除了可以直接在模塊構(gòu)造函數(shù)注入服務(wù)之外,還可以通過(guò)
ServiceContext context
獲取服務(wù)和配置。
///
/// 模塊上下文
///
public class ServiceContext
{
private readonly IServiceCollection _serviceCollection;
private readonly IConfiguration _configuration;
internal ServiceContext(IServiceCollection serviceCollection, IConfiguration configuration)
{
_serviceCollection = serviceCollection;
_configuration = configuration;
}
///
/// 依賴(lài)注入服務(wù)
///
public IServiceCollection Services => _serviceCollection;
///
/// 配置
///
public IConfiguration Configuration => _configuration;
}
因?yàn)槟K之間會(huì)有依賴(lài)關(guān)系,為了識(shí)別這些依賴(lài)關(guān)系,Maomi.Core 使用樹(shù)來(lái)表達(dá)依賴(lài)關(guān)系。Maomi.Core 在啟動(dòng)模塊服務(wù)時(shí),掃描所有模塊類(lèi),然后將模塊依賴(lài)關(guān)系存放到模塊樹(shù)中,然后按照左序遍歷的算法對(duì)模塊逐個(gè)初始化,也就是先從底層模塊開(kāi)始進(jìn)行初始化。
Maomi.Core 可以識(shí)別模塊循環(huán)依賴(lài)
比如,有以下模塊和依賴(lài):
[InjectModule()]
[InjectModule()]
class C:IModule
[InjectModule()]
class B:IModule
// 這里出現(xiàn)了循環(huán)依賴(lài)
[InjectModule()]
class A:IModule
// C 是入口模塊
services.AddModule();
因?yàn)?C 模塊依賴(lài) A、B 模塊,所以 A、B 是節(jié)點(diǎn) C 的子節(jié)點(diǎn),而 A、B 的父節(jié)點(diǎn)則是 C。當(dāng)把 A、B、C 三個(gè)模塊以及依賴(lài)關(guān)系掃描完畢之后,會(huì)得到以下的模塊依賴(lài)樹(shù)。
如下圖所示,每個(gè)模塊都做了下標(biāo),表示不同的依賴(lài)關(guān)系,一個(gè)模塊可以出現(xiàn)多次,
C1 -> A0
表示 C 依賴(lài) A。
C 0 開(kāi)始,沒(méi)有父節(jié)點(diǎn),則不存在循環(huán)依賴(lài)。
從 A 0 開(kāi)始,A 0 -> C 0 ,該鏈路中也沒(méi)有出現(xiàn)重復(fù)的 A 模塊。
從 C 1 開(kāi)始,C 1 -> A 0 -> C 0 ,該鏈路中 C 模塊重復(fù)出現(xiàn),則說(shuō)明出現(xiàn)了循環(huán)依賴(lài)。
從 C 2 開(kāi)始,C 2 -> A 1 -> B 0 -> C 0 ,該鏈路中 C 模塊重復(fù)出現(xiàn),則說(shuō)明出現(xiàn)了循環(huán)依賴(lài)。
在生成模塊樹(shù)之后,通過(guò)對(duì)模塊樹(shù)進(jìn)行后序遍歷即可。
比如,有以下模塊以及依賴(lài)。
[InjectModule()]
[InjectModule()]
class E:IModule
[InjectModule()]
[InjectModule()]
class C:IModule
[InjectModule()]
class D:IModule
[InjectModule()]
class B:IModule
class A:IModule
// E 是入口模塊
services.AddModule();
生成模塊依賴(lài)樹(shù)如圖所示:
首先從 E 0 開(kāi)始掃描,因?yàn)?E 0 下存在子節(jié)點(diǎn) C 0 、 D 0 ,那么就會(huì)先順著 C 0 再次掃描,掃描到 A 0 時(shí),因?yàn)?A 0 下已經(jīng)沒(méi)有子節(jié)點(diǎn)了,所以會(huì)對(duì) A 0 對(duì)應(yīng)的模塊 A 進(jìn)行初始化。根據(jù)上圖模塊依賴(lài)樹(shù)進(jìn)行后序遍歷,初始化模塊的順序是(已經(jīng)被初始化的模塊會(huì)跳過(guò)):
Maomi.Core 是通過(guò)
[InjectOn]
識(shí)別要注冊(cè)該服務(wù)到容器中,其定義如下:
///
/// 依賴(lài)注入標(biāo)記
///
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
public class InjectOnAttribute : Attribute
{
///
/// 要注入的服務(wù)
///
public Type[]? ServicesType { get; set; }
///
/// 生命周期
///
public ServiceLifetime Lifetime { get; set; }
///
/// 注入模式
///
public InjectScheme Scheme { get; set; }
///
/// 是否注入自己
///
public bool Own { get; set; } = false;
///
///
///
///
///
public InjectOnAttribute(ServiceLifetime lifetime = ServiceLifetime.Transient, InjectScheme scheme = InjectScheme.OnlyInterfaces)
{
Lifetime = lifetime;
Scheme = scheme;
}
}
使用
[InjectOn]
時(shí),默認(rèn)是注冊(cè)服務(wù)為
Transient
生命周期,且注冊(cè)所有接口。
[InjectOn]
public class MyService : IAService, IBService
等同于:
services.AddTransient();
services.AddTransient();
如果只想注冊(cè)
IAService
,可以將注冊(cè)模式設(shè)置為
InjectScheme.Some
,然后自定義注冊(cè)的類(lèi)型:
[InjectOn(
lifetime: ServiceLifetime.Transient,
Scheme = InjectScheme.Some,
ServicesType = new Type[] { typeof(IAService) }
)]
public class MyService : IAService, IBService
也可以把自身注冊(cè)到容器中:
[InjectOn(Own = true)]
public class MyService : IMyService
等同于:
services.AddTransient();
services.AddTransient();
如果服務(wù)繼承了類(lèi)、接口,只想注冊(cè)父類(lèi),那么可以這樣寫(xiě):
public class ParentService { }
[InjectOn(
Scheme = InjectScheme.OnlyBaseClass
)]
public class MyService : ParentService, IDisposable
等同于:
services.AddTransient();
services.AddTransient();
如果只注冊(cè)自身,忽略接口等,可以使用:
[InjectOn(ServiceLifetime.Scoped, Scheme = InjectScheme.None, Own = true)]
在本小節(jié)中,我們將會(huì)開(kāi)始設(shè)計(jì)一個(gè)支持模塊化和自動(dòng)服務(wù)注冊(cè)的小框架,從設(shè)計(jì)和實(shí)現(xiàn) Maomi.Core 開(kāi)始,我們?cè)诤竺娴恼鹿?jié)中會(huì)掌握更多框架技術(shù)的設(shè)計(jì)思路和實(shí)現(xiàn)方法,從而掌握從零開(kāi)始編寫(xiě)一個(gè)框架的能力。
創(chuàng)建一個(gè)名為 Maomi.Core 的類(lèi)庫(kù)項(xiàng)目,這個(gè)類(lèi)庫(kù)中將會(huì)包含框架核心抽象和實(shí)現(xiàn)代碼。
為了減少命名空間長(zhǎng)度,便于開(kāi)發(fā)的時(shí)候引入需要的命名空間,打開(kāi) Maomi.Core.csproj 文件,在 PropertyGroup 屬性中,添加一行配置:
Maomi
配置
屬性之后,我們?cè)?Maomi.Core 項(xiàng)目中創(chuàng)建的類(lèi)型,其命名空間都會(huì)以
Maomi.
開(kāi)頭,而不是
Maomi.Core
。
接著為項(xiàng)目添加兩個(gè)依賴(lài)包,以便實(shí)現(xiàn)自動(dòng)依賴(lài)注入和初始化模塊時(shí)提供配置。
Microsoft.Extensions.DependencyInjection
Microsoft.Extensions.Configuration.Abstractions
當(dāng)本章的代碼編寫(xiě)完畢之后,我們可以這樣實(shí)現(xiàn)一個(gè)模塊、初始化模塊、引入依賴(lài)模塊。代碼示例如下:
[InjectModule]
public class ApiModule : IModule
{
private readonly IConfiguration _configuration;
public ApiModule(IConfiguration configuration)
{
_configuration = configuration;
}
public void ConfigureServices(ServiceContext context)
{
var configuration = context.Configuration;
context.Services.AddCors();
}
}
從這段代碼,筆者以從上到下的順序來(lái)解讀我們需要實(shí)現(xiàn)哪些技術(shù)點(diǎn)。
1,模塊依賴(lài)。
[InjectModule
表示當(dāng)前模塊需要依賴(lài)哪些模塊。如果需要依賴(lài)多個(gè)模塊,可以使用多個(gè)特性,示例如下:
[InjectModule]
[InjectModule]
2,模塊接口和初始化。
每一個(gè)模塊都需要實(shí)現(xiàn) IModule 接口,框架識(shí)別到類(lèi)型繼承了這個(gè)接口后才會(huì)把類(lèi)型當(dāng)作一個(gè)模塊類(lèi)進(jìn)行處理。IModule 接口很簡(jiǎn)單,只有
ConfigureServices(ServiceContext context)
一個(gè)方法,可以在這個(gè)方法中編寫(xiě)初始化模塊的代碼。ConfigureServices 方法中有一個(gè) ServiceContext 類(lèi)型的參數(shù), ServiceContext 中包含了 IServiceCollection、IConfiguration ,模塊可以從 ServiceContext 中獲得當(dāng)前容器的服務(wù)、啟動(dòng)時(shí)的配置等。
3,依賴(lài)注入
每個(gè)模塊的構(gòu)造函數(shù)都可以使用依賴(lài)注入,可以在模塊類(lèi)中注入需要的服務(wù),開(kāi)發(fā)者可以在模塊初始化時(shí),通過(guò)這些服務(wù)初始化模塊。
基于以上三點(diǎn),我們可以先抽象出特性類(lèi)、接口等,由于這些類(lèi)型不包含具體的邏輯,因此從這一部分先下手,實(shí)現(xiàn)起來(lái)會(huì)更簡(jiǎn)單,可以避免大腦混亂,編寫(xiě)框架時(shí)不知道要從哪里先下手。
創(chuàng)建一個(gè)
ServiceContext
類(lèi),用于在模塊間傳遞服務(wù)上下文信息,其代碼如下:
public class ServiceContext
{
private readonly IServiceCollection _serviceCollection;
private readonly IConfiguration _configuration;
internal ServiceContext(IServiceCollection serviceCollection, IConfiguration configuration)
{
_serviceCollection = serviceCollection;
_configuration = configuration;
}
public IServiceCollection Services => _serviceCollection;
public IConfiguration Configuration => _configuration;
}
根據(jù)實(shí)際需求,還可以在 ServiceContext 中添加日志等屬性字段。
創(chuàng)建 IModule 接口。
public interface IModule
{
void ConfigureServices(ServiceContext services);
}
創(chuàng)建
InjectModuleAttribute
特性,用于引入依賴(lài)模塊。
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false)]
public class InjectModuleAttribute : Attribute
{
// 依賴(lài)的模塊
public Type ModuleType { get; private init; }
public InjectModuleAttribute(Type type)
{
ModuleType = type;
}
}
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false)]
public sealed class InjectModuleAttribute : InjectModuleAttribute
where TModule : IModule
{
public InjectModuleAttribute() : base(typeof(TModule)){}
}
泛型特性屬于 C# 11 的新語(yǔ)法。
定義兩個(gè)特性類(lèi)后,我們可以使用
[InjectModule(typeof(AppModule))]
或
InjectModule
的方式定義依賴(lài)模塊。
當(dāng)完成本章的代碼編寫(xiě)后,如果需要注入服務(wù),只需要標(biāo)記
[InjectOn]
特性即可。
// 簡(jiǎn)單注冊(cè)
[InjectOn]
public class MyService : IMyService
// 注注冊(cè)并設(shè)置生命周期為 scope
[InjectOn(ServiceLifetime.Scoped)]
public class MyService : IMyService
// 只注冊(cè)接口,不注冊(cè)父類(lèi)
[InjectOn(InjectScheme.OnlyInterfaces)]
public class MyService : ParentService, IMyService
有時(shí)我們會(huì)有各種各樣的需求,例如
MyService
繼承了父類(lèi)
ParentService
和接口
IMyService
,但是只需要注冊(cè)
ParentService
,而不需要注冊(cè)接口;又或者只需要注冊(cè) MyService,而不需要注冊(cè)
ParentService
、
IMyService
。
創(chuàng)建 InjectScheme 枚舉,定義注冊(cè)模式:
public enum InjectScheme
{
// 注入父類(lèi)、接口
Any,
// 手動(dòng)選擇要注入的服務(wù)
Some,
// 只注入父類(lèi)
OnlyBaseClass,
// 只注入實(shí)現(xiàn)的接口
OnlyInterfaces,
// 此服務(wù)不會(huì)被注入到容器中
None
}
定義服務(wù)注冊(cè)特性:
// 依賴(lài)注入標(biāo)記
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
public class InjectOnAttribute : Attribute
{
// 要注入的服務(wù)
public Type[]? ServicesType { get; set; }
// 生命周期
public ServiceLifetime Lifetime { get; set; }
// 注入模式
public InjectScheme Scheme { get; set; }
// 是否注入自己
public bool Own { get; set; } = false;
public InjectOnAttribute(ServiceLifetime lifetime = ServiceLifetime.Transient,
InjectScheme scheme = InjectScheme.OnlyInterfaces)
{
Lifetime = lifetime;
Scheme = scheme;
}
}
因?yàn)槟K之間會(huì)有依賴(lài)關(guān)系,因此為了生成模塊樹(shù),需要定義一個(gè) ModuleNode 類(lèi)表示模塊節(jié)點(diǎn), 一個(gè) ModuleNode 實(shí)例標(biāo)識(shí)一個(gè)依賴(lài)關(guān)系 。
///
/// 模塊節(jié)點(diǎn)
///
internal class ModuleNode
{
// 當(dāng)前模塊類(lèi)型
public Type ModuleType { get; set; } = null!;
// 鏈表,指向父模塊節(jié)點(diǎn),用于循環(huán)引用檢測(cè)
public ModuleNode? ParentModule { get; set; }
// 依賴(lài)的其它模塊
public HashSet? Childs { get; set; }
// 通過(guò)鏈表檢測(cè)是否出現(xiàn)了循環(huán)依賴(lài)
public bool ContainsTree(ModuleNode childModule)
{
if (childModule.ModuleType == ModuleType) return true;
if (this.ParentModule == null) return false;
// 如果當(dāng)前模塊找不到記錄,則向上查找
return this.ParentModule.ContainsTree(childModule);
}
public override int GetHashCode()
{
return ModuleType.GetHashCode();
}
public override bool Equals(object? obj)
{
if (obj == null) return false;
if(obj is ModuleNode module)
{
return GetHashCode() == module.GetHashCode();
}
return false;
}
}
框架在掃描所有程序集之后,通過(guò) ModuleNode 實(shí)例將所有模塊以及模塊依賴(lài)組成一顆模塊樹(shù),通過(guò)模塊樹(shù)來(lái)判斷是否出現(xiàn)了循環(huán)依賴(lài)。
比如,有以下模塊和依賴(lài):
[InjectModule()]
[InjectModule()]
class C:IModule
[InjectModule()]
class B:IModule
// 這里出現(xiàn)了循環(huán)依賴(lài)
[InjectModule()]
class A:IModule
// C 是入口模塊
services.AddModule();
因?yàn)?C 模塊依賴(lài) A、B 模塊,所以 A、B 是節(jié)點(diǎn) C 的子節(jié)點(diǎn),而 A、B 的父節(jié)點(diǎn)則是 C。
C.Childs = new (){ A , B}
A.ParentModule => C
B.ParentModule => C
當(dāng)把 A、B、C 三個(gè)模塊以及依賴(lài)關(guān)系掃描完畢之后,會(huì)得到以下的模塊依賴(lài)樹(shù)。一個(gè)節(jié)點(diǎn)即是一個(gè) ModuleNode 實(shí)例,一個(gè)模塊被多次引入,就會(huì)出現(xiàn)多次。
那么,如果識(shí)別到循環(huán)依賴(lài)呢?只需要調(diào)用
ModuleNode.ContainsTree()
從一個(gè) ModuleNode 實(shí)例中,不斷往上查找
ModuleNode.ParentModule
即可,如果該鏈表中包含相同類(lèi)型的模塊,即為循環(huán)依賴(lài),需要拋出異常。
比如從 C 0 開(kāi)始,沒(méi)有父節(jié)點(diǎn),則不存在循環(huán)依賴(lài)。
從 A 0 開(kāi)始,A 0 -> C 0 ,該鏈路中也沒(méi)有出現(xiàn)重復(fù)的 A 模塊。
從 C 1 開(kāi)始,C 1 -> A 0 -> C 0 ,該鏈路中 C 模塊重復(fù)出現(xiàn),則說(shuō)明出現(xiàn)了循環(huán)依賴(lài)。
所以,是否出現(xiàn)了循環(huán)依賴(lài)判斷起來(lái)是很簡(jiǎn)單的,我們只需要從
ModuleNode.ContainsTree()
往上查找即可。
在生成模塊樹(shù)之后,通過(guò)對(duì)模塊樹(shù)進(jìn)行后序遍歷即可。
比如,有以下模塊以及依賴(lài)。
[InjectModule()]
[InjectModule()]
class E:IModule
[InjectModule()]
[InjectModule()]
class C:IModule
[InjectModule()]
class D:IModule
[InjectModule()]
class B:IModule
class A:IModule
// E 是入口模塊
services.AddModule();
生成模塊依賴(lài)樹(shù)如圖所示:
首先從 E 0 開(kāi)始掃描,因?yàn)?E 0 下存在子節(jié)點(diǎn) C 0 、 D 0 ,那么就會(huì)先順著 C 0 再次掃描,掃描到 A 0 時(shí),因?yàn)?A 0 下已經(jīng)沒(méi)有子節(jié)點(diǎn)了,所以會(huì)對(duì) A 0 對(duì)應(yīng)的模塊 A 進(jìn)行初始化。根據(jù)上圖模塊依賴(lài)樹(shù)進(jìn)行后序遍歷,初始化模塊的順序是(已經(jīng)被初始化的模塊會(huì)跳過(guò)):
偽代碼示例如下:
private static void InitModuleTree(ModuleNode moduleNode)
{
if (moduleNode.Childs != null)
{
foreach (var item in moduleNode.Childs)
{
InitModuleTree(item);
}
}
// 如果該節(jié)點(diǎn)已經(jīng)沒(méi)有子節(jié)點(diǎn)
// 如果模塊沒(méi)有處理過(guò)
if (!moduleTypes.Contains(moduleNode.ModuleType))
{
InitInjectService(moduleNode.ModuleType);
}
}
本小節(jié)的代碼都在
ModuleExtensions.cs
中。
當(dāng)我們把接口、枚舉、特性等類(lèi)型定義之后,接下來(lái)我們便要思考如何實(shí)例化模塊、檢測(cè)模塊的依賴(lài)關(guān)系,實(shí)現(xiàn)自動(dòng)服務(wù)注冊(cè)。為了簡(jiǎn)化設(shè)計(jì),我們可以將模塊化自動(dòng)服務(wù)注冊(cè)寫(xiě)在一起,當(dāng)初始化一個(gè)模塊時(shí),框架同時(shí)會(huì)掃描該程序集中的服務(wù)進(jìn)行注冊(cè)。如果程序集中不包含模塊類(lèi),那么框架不會(huì)掃描該程序集,也就不會(huì)注冊(cè)服務(wù)。
接下來(lái),我們思考模塊化框架需要解決哪些問(wèn)題或支持哪些功能:
如何識(shí)別和注冊(cè)服務(wù);
框架能夠識(shí)別模塊的依賴(lài),生成模塊依賴(lài)樹(shù),能夠檢測(cè)到循環(huán)依賴(lài)等問(wèn)題;
多個(gè)模塊可能引用了同一個(gè)模塊 A,但是模塊 A 只能被實(shí)例化一次;
初始化模塊的順序;
模塊類(lèi)本身要作為服務(wù)注冊(cè)到容器中,實(shí)例化模塊類(lèi)時(shí),需要支持依賴(lài)注入,也就是說(shuō)模塊類(lèi)的構(gòu)造函數(shù)可以注入其它服務(wù);
我們先解決第一個(gè)問(wèn)題,
因?yàn)樽詣?dòng)服務(wù)注冊(cè)是根據(jù)模塊所在的程序集掃描標(biāo)記類(lèi),識(shí)別所有使用了
InjectOnAttribute
特性的類(lèi)型,所以我們可以先編寫(xiě)一個(gè)程序集掃描方法,該方法的功能是通過(guò)程序集掃描所有類(lèi)型,然后根據(jù)特性配置注冊(cè)服務(wù)。
///
/// 自動(dòng)依賴(lài)注入
///
///
///
/// 已被注入的服務(wù)
private static void InitInjectService(IServiceCollection services, Assembly assembly, HashSet injectTypes)
{
// 只掃描可實(shí)例化的類(lèi),不掃描靜態(tài)類(lèi)、接口、抽象類(lèi)、嵌套類(lèi)、非公開(kāi)類(lèi)等
foreach (var item in assembly.GetTypes().Where(x => x.IsClass && !x.IsAbstract && !x.IsNestedPublic))
{
var inject = item.GetCustomAttributes().FirstOrDefault(x => x.GetType() == typeof(InjectOnAttribute)) as InjectOnAttribute;
if (inject == null) continue;
if (injectTypes.Contains(item)) continue;
injectTypes.Add(item);
// 如果需要注入自身
if (inject.Own)
{
switch (inject.Lifetime)
{
case ServiceLifetime.Transient: services.AddTransient(item); break;
case ServiceLifetime.Scoped: services.AddScoped(item); break;
case ServiceLifetime.Singleton: services.AddSingleton(item); break;
}
}
if (inject.Scheme == InjectScheme.None) continue;
// 注入所有接口
if (inject.Scheme == InjectScheme.OnlyInterfaces || inject.Scheme == InjectScheme.Any)
{
var interfaces = item.GetInterfaces();
if (interfaces.Count() == 0) continue;
switch (inject.Lifetime)
{
case ServiceLifetime.Transient: interfaces.ToList().ForEach(x => services.AddTransient(x, item)); break;
case ServiceLifetime.Scoped: interfaces.ToList().ForEach(x => services.AddScoped(x, item)); break;
case ServiceLifetime.Singleton: interfaces.ToList().ForEach(x => services.AddSingleton(x, item)); break;
}
}
// 注入父類(lèi)
if (inject.Scheme == InjectScheme.OnlyBaseClass || inject.Scheme == InjectScheme.Any)
{
var baseType = item.BaseType;
if (baseType == null) throw new ArgumentException($"{item.Name} 注入模式 {nameof(inject.Scheme)} 未找到父類(lèi)!");
switch (inject.Lifetime)
{
case ServiceLifetime.Transient: services.AddTransient(baseType, item); break;
case ServiceLifetime.Scoped: services.AddScoped(baseType, item); break;
case ServiceLifetime.Singleton: services.AddSingleton(baseType, item); break;
}
}
if (inject.Scheme == InjectScheme.Some)
{
var types = inject.ServicesType;
if (types == null) throw new ArgumentException($"{item.Name} 注入模式 {nameof(inject.Scheme)} 未找到服務(wù)!");
switch (inject.Lifetime)
{
case ServiceLifetime.Transient: types.ToList().ForEach(x => services.AddTransient(x, item)); break;
case ServiceLifetime.Scoped: types.ToList().ForEach(x => services.AddScoped(x, item)); break;
case ServiceLifetime.Singleton: types.ToList().ForEach(x => services.AddSingleton(x, item)); break;
}
}
}
}
定義兩個(gè)擴(kuò)展函數(shù),用于注入入口模塊。
///
/// 注冊(cè)模塊化服務(wù)
///
/// 入口模塊
///
public static void AddModule(this IServiceCollection services)
where TModule : IModule
{
AddModule(services, typeof(TModule));
}
///
/// 注冊(cè)模塊化服務(wù)
///
///
/// 入口模塊
public static void AddModule(this IServiceCollection services, Type startupModule)
{
if (startupModule?.GetInterface(nameof(IModule)) == null)
{
throw new TypeLoadException($"{startupModule?.Name} 不是有效的模塊類(lèi)");
}
IServiceProvider scope = BuildModule(services, startupModule);
}
框架需要從入口模塊程序集開(kāi)始查找被依賴(lài)的模塊程序集,然后通過(guò)后序遍歷初始化每個(gè)模塊,并掃描該模塊程序集中的服務(wù)。
創(chuàng)建一個(gè)
BuildModule
函數(shù),BuildModule 為構(gòu)建模塊依賴(lài)樹(shù)、初始化模塊提前創(chuàng)建環(huán)境。
///
/// 構(gòu)建模塊依賴(lài)樹(shù)并初始化模塊
///
///
///
///
///
private static IServiceProvider BuildModule(IServiceCollection services, Type startupModule)
{
// 生成根模塊
ModuleNode rootTree = new ModuleNode()
{
ModuleType = startupModule,
Childs = new HashSet()
};
// 根模塊依賴(lài)的其他模塊
// IModule => InjectModuleAttribute
var rootDependencies = startupModule.GetCustomAttributes(false)
.Where(x => x.GetType().IsSubclassOf(typeof(InjectModuleAttribute)))
.OfType();
// 構(gòu)建模塊依賴(lài)樹(shù)
BuildTree(services, rootTree, rootDependencies);
// 構(gòu)建一個(gè) Ioc 實(shí)例,以便初始化模塊類(lèi)
var scope = services.BuildServiceProvider();
// 初始化所有模塊類(lèi)
var serviceContext = new ServiceContext(services, scope.GetService()!);
// 記錄已經(jīng)處理的程序集、模塊和服務(wù),以免重復(fù)處理
HashSet moduleAssemblies = new HashSet { startupModule.Assembly };
HashSet moduleTypes = new HashSet();
HashSet injectTypes = new HashSet();
// 后序遍歷樹(shù)并初始化每個(gè)模塊
InitModuleTree(scope, serviceContext, moduleAssemblies, moduleTypes, injectTypes, rootTree);
return scope;
}
第一步,構(gòu)建模塊依賴(lài)樹(shù)。
///
/// 構(gòu)建模塊依賴(lài)樹(shù)
///
///
///
/// 其依賴(lài)的模塊
private static void BuildTree(IServiceCollection services, ModuleNode currentNode, IEnumerable injectModules)
{
services.AddTransient(currentNode.ModuleType);
if (injectModules == null || injectModules.Count() == 0) return;
foreach (var childModule in injectModules)
{
var childTree = new ModuleNode
{
ModuleType = childModule.ModuleType,
ParentModule = currentNode
};
// 循環(huán)依賴(lài)檢測(cè)
// 檢查當(dāng)前模塊(parentTree)依賴(lài)的模塊(childTree)是否在之前出現(xiàn)過(guò),如果是,則說(shuō)明是循環(huán)依賴(lài)
var isLoop = currentNode.ContainsTree(childTree);
if (isLoop)
{
throw new OverflowException($"檢測(cè)到循環(huán)依賴(lài)引用或重復(fù)引用!{currentNode.ModuleType.Name} 依賴(lài)的 {childModule.ModuleType.Name} 模塊在其父模塊中出現(xiàn)過(guò)!");
}
if (currentNode.Childs == null)
{
currentNode.Childs = new HashSet();
}
currentNode.Childs.Add(childTree);
// 子模塊依賴(lài)的其他模塊
var childDependencies = childModule.ModuleType.GetCustomAttributes(inherit: false)
.Where(x => x.GetType().IsSubclassOf(typeof(InjectModuleAttribute))).OfType().ToHashSet();
// 子模塊也依賴(lài)其他模塊
BuildTree(services, childTree, childDependencies);
}
}
通過(guò)后序遍歷識(shí)別依賴(lài)時(shí),由于一個(gè)模塊可能會(huì)出現(xiàn)多次,所以初始化時(shí)需要判斷模塊是否已經(jīng)初始化,然后對(duì)模塊進(jìn)行初始化并掃描模塊程序集中所有的類(lèi)型,進(jìn)行服務(wù)注冊(cè)。
///
/// 從模塊樹(shù)中遍歷
///
///
///
/// 已經(jīng)被注冊(cè)到容器中的模塊類(lèi)
/// 模塊類(lèi)所在的程序集'
/// 已被注冊(cè)到容器的服務(wù)
/// 模塊節(jié)點(diǎn)
private static void InitModuleTree(IServiceProvider serviceProvider,
ServiceContext context,
HashSet moduleAssemblies,
HashSet moduleTypes,
HashSet injectTypes,
ModuleNode moduleNode)
{
if (moduleNode.Childs != null)
{
foreach (var item in moduleNode.Childs)
{
InitModuleTree(serviceProvider, context, moduleAssemblies, moduleTypes, injectTypes, item);
}
}
// 如果模塊沒(méi)有處理過(guò)
if (!moduleTypes.Contains(moduleNode.ModuleType))
{
moduleTypes.Add(moduleNode.ModuleType);
// 實(shí)例化此模塊
// 掃描此模塊(程序集)中需要依賴(lài)注入的服務(wù)
var module = (IModule)serviceProvider.GetRequiredService(moduleNode.ModuleType);
module.ConfigureServices(context);
InitInjectService(context.Services, moduleNode.ModuleType.Assembly, injectTypes);
moduleAssemblies.Add(moduleNode.ModuleType.Assembly);
}
}
至此,Maomi.Core 所有的代碼都已經(jīng)講解完畢,通過(guò)本章的實(shí)踐,我們擁有了一個(gè)具有模塊化和自動(dòng)服務(wù)注冊(cè)的框架。可是,別高興得太早,我們應(yīng)當(dāng)如何驗(yàn)證框架是可靠的呢?答案是單元測(cè)試。在完成 Maomi.Core 項(xiàng)目之后,筆者立即編寫(xiě)了 Maomi.Core.Tests 單元測(cè)試項(xiàng)目,只有當(dāng)單元測(cè)試全部通過(guò)之后,筆者才能自信地把代碼放到書(shū)中。為項(xiàng)目編寫(xiě)單元測(cè)試是一個(gè)好習(xí)慣,尤其是對(duì)框架類(lèi)的項(xiàng)目,我們需要編寫(xiě)大量的單元測(cè)試驗(yàn)證框架的可靠性,同時(shí)單元測(cè)試中大量的示例是其他開(kāi)發(fā)者了解框架、入手框架的極佳參考。
們開(kāi)發(fā)了一個(gè)支持模塊化和自動(dòng)服務(wù)注冊(cè)的框架,通過(guò) Maomi.Core 實(shí)現(xiàn)模塊化應(yīng)用的開(kāi)發(fā)。
完成代碼后,我們需要將代碼共享給其他人,那么可以使用 nuget 包的方式。
當(dāng)類(lèi)庫(kù)開(kāi)發(fā)完成后,我們可以打包成 nuget 文件,上傳這個(gè)到 nuget.org ,或者是內(nèi)部的私有倉(cāng)庫(kù),供其他開(kāi)發(fā)者使用。
在
Maomi.Core.csproj
項(xiàng)目的的
PropertyGroup
屬性中加上以下配置,以便能夠在發(fā)布類(lèi)庫(kù)時(shí),生成 nuget 包。
true
1.0.0
貓咪框架
True
或者右鍵點(diǎn)擊項(xiàng)目-屬性-打包。
當(dāng)然,你也可以在 Visual Studio 中點(diǎn)擊項(xiàng)目右鍵屬性,在面板中進(jìn)行可視化配置。
你可以配置項(xiàng)目的 github 地址、發(fā)布說(shuō)明、開(kāi)源許可證等。
配置完成后,可以使用 Visual Studio 發(fā)布項(xiàng)目,或使用
dotnet publish -c Release
命令發(fā)布項(xiàng)目。
發(fā)布項(xiàng)目后,可以在輸出目錄找到
.nupkg
文件。
打開(kāi)
https://www.nuget.org/packages/manage/upload
,登錄后上傳
.nupkg
文件。
在 .NET 中,安裝 .NET SDK 時(shí)默認(rèn)攜帶了一些項(xiàng)目模板,使用
dotnet new list
可以看到本機(jī)中已經(jīng)按照的項(xiàng)目模板,然后通過(guò)
dotnet new {模板名稱(chēng)}
命令使用模板快速創(chuàng)建一個(gè)應(yīng)用。
通過(guò)模板創(chuàng)建一個(gè)應(yīng)用是很方便的,項(xiàng)目模板提前組織好解決方案中的項(xiàng)目結(jié)構(gòu)、代碼文件,開(kāi)發(fā)者使用模板時(shí)只需要提供一個(gè)名稱(chēng),然后即可生成一個(gè)完整的應(yīng)用。那么在本節(jié)中,筆者將會(huì)介紹如何制作自己的項(xiàng)目模板,進(jìn)一步打包到 nuget 中,分享給更多的開(kāi)發(fā)者使用。當(dāng)然,在企業(yè)開(kāi)發(fā)中,架構(gòu)師可以規(guī)劃好基礎(chǔ)代碼、設(shè)計(jì)項(xiàng)目架構(gòu),然后制作模板項(xiàng)目,業(yè)務(wù)開(kāi)發(fā)者需要?jiǎng)?chuàng)建新的項(xiàng)目時(shí),從企業(yè)基礎(chǔ)項(xiàng)目模板一鍵生成即可,從而可以快速開(kāi)發(fā)項(xiàng)目。
本節(jié)的示例代碼在
demo/1/templates
中。
讓我們來(lái)體驗(yàn)筆者已經(jīng)制作好的項(xiàng)目模板,執(zhí)行以下命令從 nuget 中安裝模板。
dotnet new install Maomi.Console.Templates::2.0.0
命令執(zhí)行完畢后,控制臺(tái)會(huì)打。
模板名 短名稱(chēng) 語(yǔ)言 標(biāo)記
------------ ------ ---- --------------
Maomi 控制臺(tái) maomi [C#] Common/Console
使用模板名稱(chēng)
maomi
創(chuàng)建自定義名稱(chēng)的項(xiàng)目:
dotnet new maomi --name MyTest
打開(kāi) Visual Studio,可以看到最近通過(guò) nuget 安裝的模板。
接下來(lái),我們來(lái)上手制作一個(gè)屬于自己的模板。
打開(kāi)
demo/1/templates
目錄,可以看到文件組織如下所示:
.
│ MaomiPack.csproj
│
└─templates
│ Maomi.Console.sln
│ template.json
│
├─Maomi.Console
│ ConsoleModule.cs
│ Maomi.Console.csproj
│ Program.cs
│
└─Maomi.Lib
IMyService.cs
LibModule.cs
Maomi.Lib.csproj
MyService.cs
創(chuàng)建 MaomiPack.csproj 文件(名稱(chēng)可以自定義),該文件用于將代碼打包到 nuget 包中,否則 dotnet cli 會(huì)先編譯項(xiàng)目再打包到 nuget 包中。
Template
2.0.0
Maomi.Console.Templates
dotnet-new;templates;contoso
Maomi 框架控制臺(tái)模板
癡者工良
用于示范 Maomi 框架的模板項(xiàng)目包.
net8.0
true
false
content
$(NoWarn);NU5128
PackageVersion
:模板版本號(hào)。
PackageId
:模板 id,在 nuget.org 中唯一。
PackageTags
:nuget 包的標(biāo)記。
Title
:nuget 包標(biāo)題。
Authors
:作者名稱(chēng)。
Description
:nuget 包描述。
創(chuàng)建一個(gè)空目錄存儲(chǔ)項(xiàng)目代碼,一般使用 templates 命名,你可以參考
demo/1/templates/templates
中的解決方案。接著在該目錄下創(chuàng)建
template.json
文件,文件內(nèi)容如下:
{
"$schema": "http://json.schemastore.org/template",
"author": "癡者工良",
"classifications": [
"Common",
"Console"
],
"identity": "Maomi.Console",
"name": "Maomi 控制臺(tái)",
"description": "這是一個(gè)使用 Maomi.Core 搭建的模塊化應(yīng)用模板。",
"shortName": "maomi",
"tags": {
"language": "C#",
"type": "project"
},
"sourceName": "Maomi",
"preferNameDirectory": true
}
template.json
文件用于配置項(xiàng)目模板屬性,在安裝模板后相關(guān)信息會(huì)顯示到 Visual Studio 項(xiàng)目模板列表,以及創(chuàng)建項(xiàng)目時(shí)自動(dòng)替換
Maomi
前綴為自定義的名稱(chēng)。
author
:作者名稱(chēng)。
classifications
:項(xiàng)目類(lèi)型,如控制臺(tái)、Web、Wpf 等。
identity
:模板唯一標(biāo)識(shí)。
name
:模板名稱(chēng)。
description
:
模板描述信息
。
shortName
:縮寫(xiě),使用
dotnet new {shortName}
命令時(shí)可以簡(jiǎn)化模板名稱(chēng)。
tags
:指定了模板使用的語(yǔ)言和項(xiàng)目類(lèi)型。
sourceName
:可以被替換的名稱(chēng),例如
Maomi.Console
將會(huì)被替換為
MyTest.Console
,模板中所有文件名稱(chēng)、字符串內(nèi)容都會(huì)被替換。
組織好模板之后,在 MaomiPack.csproj 所在目錄下執(zhí)行
dotnet pack
命令打包項(xiàng)目為 nuget 包,最后根據(jù)提示生成的 nuget 文件,上傳到 nuget.org 即可。
機(jī)器學(xué)習(xí):神經(jīng)網(wǎng)絡(luò)構(gòu)建(下)
閱讀華為Mate品牌盛典:HarmonyOS NEXT加持下游戲性能得到充分釋放
閱讀實(shí)現(xiàn)對(duì)象集合與DataTable的相互轉(zhuǎn)換
閱讀鴻蒙NEXT元服務(wù):論如何免費(fèi)快速上架作品
閱讀算法與數(shù)據(jù)結(jié)構(gòu) 1 - 模擬
閱讀升訊威在線客服與營(yíng)銷(xiāo)系統(tǒng)介紹
閱讀基于鴻蒙NEXT的血型遺傳計(jì)算器開(kāi)發(fā)案例
閱讀5. Spring Cloud OpenFeign 聲明式 WebService 客戶(hù)端的超詳細(xì)使用
閱讀Java代理模式:靜態(tài)代理和動(dòng)態(tài)代理的對(duì)比分析
閱讀Win11筆記本“自動(dòng)管理應(yīng)用的顏色”顯示規(guī)則
閱讀本站所有軟件,都由網(wǎng)友上傳,如有侵犯你的版權(quán),請(qǐng)發(fā)郵件[email protected]
湘ICP備2022002427號(hào)-10 湘公網(wǎng)安備:43070202000427號(hào)© 2013~2025 haote.com 好特網(wǎng)