關於從零設計 .NET 開發框架
作者:癡者工良
教程說明:
倉庫地址:https://github.com/whuanle/maomi
文件地址:https://maomi.whuanle.cn
作者部落格:
https://www.whuanle.cn
https://www.cnblogs.com/whuanle
模組化和自動服務註冊
基於 ASP.NET Core 開發的 Web 框架中,最著名的是 ABP,ABP 主要特點之一開發不同計畫(程式集)時,在每個計畫中建立一個模組類,程式載入每個程式集中,掃描出所有的模組類,然後透過模組類作為入口,初始化程式集。
使用模組化開發程式,好處是不需要關註程式集如何載入配置。開發人員開發程式集時,在模組類中配置如何初始化、如何讀取配置,使用者只需要將模組類引入進來即可,由框架自動啟動模組類。
Maomi.Core 也提供了模組化開發的能力,同時還包括簡單易用的自動服務註冊。Maomi.Core 是一個很簡潔的包,可以在控制台、Web 計畫、WPF 計畫中使用,在 WPF 計畫中結合 MVVM 可以大量減少程式碼復雜度,讓程式碼更加清晰明朗。
快速入手
有 Demo1.Api、Demo1.Application 兩個計畫,每個計畫都有一個模組類,模組類需要實作 IModule 介面。
Demo1.Application 計畫的 ApplicationModule.cs 檔內容如下:
public classApplicationModule : IModule
{
// 模組類中可以使用依賴註入
privatereadonly IConfiguration _configuration;
publicApplicationModule(IConfiguration configuration)
{
_configuration = configuration;
}
publicvoidConfigureServices(ServiceContext services)
{
// 這裏可以編寫模組初始化程式碼
}
}
如果要將服務註冊到容器中,在 class 上加上
[InjectOn]
特性即可。
publicinterfaceIMyService
{
intSum(int a, int b);
}
[InjectOn] // 自動註冊的標記
public classMyService : IMyService
{
publicintSum(int a, int b)
{
return a + b;
}
}
上層模組 Demo1.Api 中的 ApiModule.cs 可以透過特性註解參照底層模組。
[InjectModule<ApplicationModule>]
public classApiModule : IModule
{
publicvoidConfigureServices(ServiceContext services)
{
// 這裏可以編寫模組初始化程式碼
}
}
最後,在程式啟動時配置模組入口,並進行初始化。
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
// 註冊模組化服務,並設定 ApiModule 為入口
builder.Services.AddModule<ApiModule>();
var app = builder.Build();
模組可以依賴註入
在 ASP.NET Core 配置 Host 時,會自動註入一些框架依賴的服務,如 IConfiguration 等,因此在
.AddModule<ApiModule>()
開始初始化模組服務時,模組獲取已經註入的服務。
每個模組都需要實作 IModule 介面,其定義如下:
///<summary>
/// 模組介面
///</summary>
publicinterfaceIModule
{
///<summary>
/// 模組中的依賴註入
///</summary>
///<param name="context">模組服務上下文</param>
voidConfigureServices(ServiceContext context);
}
除了可以直接在模組建構函式註入服務之外,還可以透過
ServiceContext context
獲取服務和配置。
///<summary>
/// 模組上下文
///</summary>
public classServiceContext
{
privatereadonly IServiceCollection _serviceCollection;
privatereadonly IConfiguration _configuration;
internalServiceContext(IServiceCollection serviceCollection, IConfiguration configuration)
{
_serviceCollection = serviceCollection;
_configuration = configuration;
}
///<summary>
/// 依賴註入服務
///</summary>
public IServiceCollection Services => _serviceCollection;
///<summary>
/// 配置
///</summary>
public IConfiguration Configuration => _configuration;
}
模組化
因為模組之間會有依賴關系,為了辨識這些依賴關系,Maomi.Core 使用樹來表達依賴關系。Maomi.Core 在啟動模組服務時,掃描所有模組類,然後將模組依賴關系存放到模組樹中,然後按照左序遍歷的演算法對模組逐個初始化,也就是先從底層模組開始進行初始化。
迴圈依賴檢測
Maomi.Core 可以辨識模組迴圈依賴
比如,有以下模組和依賴:
[InjectModule<A>()]
[InjectModule<B>()]
classC:IModule
[InjectModule<A>()]
classB:IModule
// 這裏出現了迴圈依賴
[InjectModule<C>()]
classA:IModule
// C 是入口模組
services.AddModule<C>();
因為 C 模組依賴 A、B 模組,所以 A、B 是節點 C 的子節點,而 A、B 的父節點則是 C。當把 A、B、C 三個模組以及依賴關系掃描完畢之後,會得到以下的模組依賴樹。
如下圖所示,每個模組都做了下標,表示不同的依賴關系,一個模組可以出現多次,
C1 -> A0
表示 C 依賴 A。
C 0 開始,沒有父節點,則不存在迴圈依賴。
從 A 0 開始,A 0 -> C 0 ,該鏈路中也沒有出現重復的 A 模組。
從 C 1 開始,C 1 -> A 0 -> C 0 ,該鏈路中 C 模組重復出現,則說明出現了迴圈依賴。
從 C 2 開始,C 2 -> A 1 -> B 0 -> C 0 ,該鏈路中 C 模組重復出現,則說明出現了迴圈依賴。
模組初始化順序
在生成模組樹之後,透過對模組樹進行後序遍歷即可。
比如,有以下模組以及依賴。
[InjectModule<C>()]
[InjectModule<D>()]
classE:IModule
[InjectModule<A>()]
[InjectModule<B>()]
classC:IModule
[InjectModule<B>()]
classD:IModule
[InjectModule<A>()]
classB:IModule
classA:IModule
// E 是入口模組
services.AddModule<E>();
生成模組依賴樹如圖所示:
首先從 E 0 開始掃描,因為 E 0 下存在子節點 C 0 、 D 0 ,那麽就會先順著 C 0 再次掃描,掃描到 A 0 時,因為 A 0 下已經沒有子節點了,所以會對 A 0 對應的模組 A 進行初始化。根據上圖模組依賴樹進行後序遍歷,初始化模組的順序是(已經被初始化的模組會跳過):
服務自動註冊
Maomi.Core 是透過
[InjectOn]
辨識要註冊該服務到容器中,其定義如下:
///<summary>
/// 依賴註入標記
///</summary>
[AttributeUsage(AttributeTargets. class, AllowMultiple = false, Inherited = false)]
public classInjectOnAttribute : Attribute
{
///<summary>
/// 要註入的服務
///</summary>
public Type[]? ServicesType { get; set; }
///<summary>
/// 生命周期
///</summary>
public ServiceLifetime Lifetime { get; set; }
///<summary>
/// 註入模式
///</summary>
public InjectScheme Scheme { get; set; }
///<summary>
/// 是否註入自己
///</summary>
publicbool Own { get; set; } = false;
///<summary>
///
///</summary>
///<param name="lifetime"></param>
///<param name="scheme"></param>
publicInjectOnAttribute(ServiceLifetime lifetime = ServiceLifetime.Transient, InjectScheme scheme = InjectScheme.OnlyInterfaces)
{
Lifetime = lifetime;
Scheme = scheme;
}
}
使用
[InjectOn]
時,預設是註冊服務為
Transient
生命周期,且註冊所有介面。
[InjectOn]
public classMyService : IAService, IBService
等同於:
services.AddTransient<IAService, MyService>();
services.AddTransient<IBService, MyService>();
如果只想註冊
IAService
,可以將註冊模式設定為
InjectScheme.Some
,然後自訂註冊的型別:
[InjectOn(
lifetime: ServiceLifetime.Transient,
Scheme = InjectScheme.Some,
ServicesType = new Type[] { typeof(IAService) }
)]
public classMyService : IAService, IBService
也可以把自身註冊到容器中:
[InjectOn(Own = true)]
public classMyService : IMyService
等同於:
services.AddTransient<IAService, MyService>();
services.AddTransient<MyService>();
如果服務繼承了類、介面,只想註冊父類,那麽可以這樣寫:
public classParentService { }
[InjectOn(
Scheme = InjectScheme.OnlyBase class
)]
public classMyService : ParentService, IDisposable
等同於:
services.AddTransient<ParentService, MyService>();
services.AddTransient<MyService>();
如果只註冊自身,忽略介面等,可以使用:
[InjectOn(ServiceLifetime.Scoped, Scheme = InjectScheme.None, Own = true)]
模組化和自動服務註冊的設計和實作
在本小節中,我們將會開始設計一個支持模組化和自動服務註冊的小框架,從設計和實作 Maomi.Core 開始,我們在後面的章節中會掌握更多框架技術的設計思路和實作方法,從而掌握從零開始編寫一個框架的能力。
計畫說明
建立一個名為 Maomi.Core 的類別庫計畫,這個類別庫中將會包含框架核心抽象和實作程式碼。
為了減少名稱空間長度,便於開發的時候引入需要的名稱空間,開啟 Maomi.Core.csproj 檔,在 PropertyGroup 內容中,添加一行配置:
<RootNamespace>Maomi</RootNamespace>
配置
<RootNamespace>
內容之後,我們在 Maomi.Core 計畫中建立的型別,其名稱空間都會以
Maomi.
開頭,而不是
Maomi.Core
。
接著為計畫添加兩個依賴包,以便實作自動依賴註入和初始化模組時提供配置。
Microsoft.Extensions.DependencyInjection
Microsoft.Extensions.Configuration.Abstractions
模組化設計
當本章的程式碼編寫完畢之後,我們可以這樣實作一個模組、初始化模組、引入依賴模組。程式碼範例如下:
[InjectModule<ApplicationModule>]
public classApiModule : IModule
{
privatereadonly IConfiguration _configuration;
publicApiModule(IConfiguration configuration)
{
_configuration = configuration;
}
publicvoidConfigureServices(ServiceContext context)
{
var configuration = context.Configuration;
context.Services.AddCors();
}
}
從這段程式碼,筆者以從上到下的順序來解讀我們需要實作哪些技術點。
1,模組依賴。
[InjectModule<ApplicationModule>]
表示當前模組需要依賴哪些模組。如果需要依賴多個模組,可以使用多個特性,範例如下:
[InjectModule<DomainModule>]
[InjectModule<ApplicationModule>]
2,模組介面和初始化。
每一個模組都需要實作 IModule 介面,框架辨識到型別繼承了這個介面後才會把型別當作一個模組類進行處理。IModule 介面很簡單,只有
ConfigureServices(ServiceContext context)
一個方法,可以在這個方法中編寫初始化模組的程式碼。ConfigureServices 方法中有一個 ServiceContext 型別的參數, ServiceContext 中包含了 IServiceCollection、IConfiguration ,模組可以從 ServiceContext 中獲得當前容器的服務、啟動時的配置等。
3,依賴註入
每個模組的建構函式都可以使用依賴註入,可以在模組類中註入需要的服務,開發者可以在模組初始化時,透過這些服務初始化模組。
基於以上三點,我們可以先抽象出特性類、介面等,由於這些型別不包含具體的邏輯,因此從這一部份先下手,實作起來會更簡單,可以避免大腦混亂,編寫框架時不知道要從哪裏先下手。
建立一個
ServiceContext
類,用於在模組間傳遞服務上下文資訊,其程式碼如下:
public classServiceContext
{
privatereadonly IServiceCollection _serviceCollection;
privatereadonly IConfiguration _configuration;
internalServiceContext(IServiceCollection serviceCollection, IConfiguration configuration)
{
_serviceCollection = serviceCollection;
_configuration = configuration;
}
public IServiceCollection Services => _serviceCollection;
public IConfiguration Configuration => _configuration;
}
根據實際需求,還可以在 ServiceContext 中添加日誌等內容欄位。
建立 IModule 介面。
publicinterfaceIModule
{
voidConfigureServices(ServiceContext services);
}
建立
InjectModuleAttribute
特性,用於引入依賴模組。
[AttributeUsage(AttributeTargets. class, AllowMultiple = true, Inherited = false)]
public classInjectModuleAttribute : Attribute
{
// 依賴的模組
public Type ModuleType { get; privateinit; }
publicInjectModuleAttribute(Type type)
{
ModuleType = type;
}
}
[AttributeUsage(AttributeTargets. class, AllowMultiple = true, Inherited = false)]
publicsealed classInjectModuleAttribute<TModule> : InjectModuleAttribute
whereTModule : IModule
{
publicInjectModuleAttribute() : base(typeof(TModule)){}
}
泛型特性屬於 C# 11 的新語法。
定義兩個特性類後,我們可以使用
[InjectModule(typeof(AppModule))]
或
InjectModule<AppModule>
的方式定義依賴模組。
自動服務註冊的設計
當完成本章的程式碼編寫後,如果需要註入服務,只需要標記
[InjectOn]
特性即可。
// 簡單註冊
[InjectOn]
public classMyService : IMyService
// 註註冊並設定生命周期為 scope
[InjectOn(ServiceLifetime.Scoped)]
public classMyService : IMyService
// 只註冊介面,不註冊父類
[InjectOn(InjectScheme.OnlyInterfaces)]
public classMyService : ParentService, IMyService
有時我們會有各種各樣的需求,例如
MyService
繼承了父類
ParentService
和介面
IMyService
,但是只需要註冊
ParentService
,而不需要註冊介面;又或者只需要註冊 MyService,而不需要註冊
ParentService
、
IMyService
。
建立 InjectScheme 列舉,定義註冊模式:
publicenum InjectScheme
{
// 註入父類、介面
Any,
// 手動選擇要註入的服務
Some,
// 只註入父類
OnlyBase class,
// 只註入實作的介面
OnlyInterfaces,
// 此服務不會被註入到容器中
None
}
定義服務註冊特性:
// 依賴註入標記
[AttributeUsage(AttributeTargets. class, AllowMultiple = false, Inherited = false)]
public classInjectOnAttribute : Attribute
{
// 要註入的服務
public Type[]? ServicesType { get; set; }
// 生命周期
public ServiceLifetime Lifetime { get; set; }
// 註入模式
public InjectScheme Scheme { get; set; }
// 是否註入自己
publicbool Own { get; set; } = false;
publicInjectOnAttribute(ServiceLifetime lifetime = ServiceLifetime.Transient,
InjectScheme scheme = InjectScheme.OnlyInterfaces)
{
Lifetime = lifetime;
Scheme = scheme;
}
}
模組依賴
因為模組之間會有依賴關系,因此為了生成模組樹,需要定義一個 ModuleNode 類表示模組節點, 一個 ModuleNode 例項標識一個依賴關系 。
///<summary>
/// 模組節點
///</summary>
internal classModuleNode
{
// 當前模組型別
public Type ModuleType { get; set; } = null!;
// 連結串列,指向父模組節點,用於迴圈參照檢測
public ModuleNode? ParentModule { get; set; }
// 依賴的其它模組
public HashSet<ModuleNode>? Childs { get; set; }
// 透過連結串列檢測是否出現了迴圈依賴
publicboolContainsTree(ModuleNode childModule)
{
if (childModule.ModuleType == ModuleType) returntrue;
if (this.ParentModule == null) returnfalse;
// 如果當前模組找不到記錄,則向上尋找
returnthis.ParentModule.ContainsTree(childModule);
}
publicoverrideintGetHashCode()
{
return ModuleType.GetHashCode();
}
publicoverrideboolEquals(object? obj)
{
if (obj == null) returnfalse;
if(obj is ModuleNode module)
{
return GetHashCode() == module.GetHashCode();
}
returnfalse;
}
}
框架在掃描所有程式集之後,透過 ModuleNode 例項將所有模組以及模組依賴組成一顆模組樹,透過模組樹來判斷是否出現了迴圈依賴。
比如,有以下模組和依賴:
[InjectModule<A>()]
[InjectModule<B>()]
classC:IModule
[InjectModule<A>()]
classB:IModule
// 這裏出現了迴圈依賴
[InjectModule<C>()]
classA:IModule
// C 是入口模組
services.AddModule<C>();
因為 C 模組依賴 A、B 模組,所以 A、B 是節點 C 的子節點,而 A、B 的父節點則是 C。
C.Childs = new (){ A , B}
A.ParentModule => C
B.ParentModule => C
當把 A、B、C 三個模組以及依賴關系掃描完畢之後,會得到以下的模組依賴樹。一個節點即是一個 ModuleNode 例項,一個模組被多次引入,就會出現多次。
那麽,如果辨識到迴圈依賴呢?只需要呼叫
ModuleNode.ContainsTree()
從一個 ModuleNode 例項中,不斷往上尋找
ModuleNode.ParentModule
即可,如果該連結串列中包含相同型別的模組,即為迴圈依賴,需要丟擲異常。
比如從 C 0 開始,沒有父節點,則不存在迴圈依賴。
從 A 0 開始,A 0 -> C 0 ,該鏈路中也沒有出現重復的 A 模組。
從 C 1 開始,C 1 -> A 0 -> C 0 ,該鏈路中 C 模組重復出現,則說明出現了迴圈依賴。
所以,是否出現了迴圈依賴判斷起來是很簡單的,我們只需要從
ModuleNode.ContainsTree()
往上尋找即可。
在生成模組樹之後,透過對模組樹進行後序遍歷即可。
比如,有以下模組以及依賴。
[InjectModule<C>()]
[InjectModule<D>()]
classE:IModule
[InjectModule<A>()]
[InjectModule<B>()]
classC:IModule
[InjectModule<B>()]
classD:IModule
[InjectModule<A>()]
classB:IModule
classA:IModule
// E 是入口模組
services.AddModule<E>();
生成模組依賴樹如圖所示:
首先從 E 0 開始掃描,因為 E 0 下存在子節點 C 0 、 D 0 ,那麽就會先順著 C 0 再次掃描,掃描到 A 0 時,因為 A 0 下已經沒有子節點了,所以會對 A 0 對應的模組 A 進行初始化。根據上圖模組依賴樹進行後序遍歷,初始化模組的順序是(已經被初始化的模組會跳過):
虛擬碼範例如下:
privatestaticvoidInitModuleTree(ModuleNode moduleNode)
{
if (moduleNode.Childs != null)
{
foreach (var item in moduleNode.Childs)
{
InitModuleTree(item);
}
}
// 如果該節點已經沒有子節點
// 如果模組沒有處理過
if (!moduleTypes.Contains(moduleNode.ModuleType))
{
InitInjectService(moduleNode.ModuleType);
}
}
實作模組化和自動服務註冊
本小節的程式碼都在
ModuleExtensions.cs
中。
當我們把介面、列舉、特性等型別定義之後,接下來我們便要思考如何例項化模組、檢測模組的依賴關系,實作自動服務註冊。為了簡化設計,我們可以將模組化自動服務註冊寫在一起,當初始化一個模組時,框架同時會掃描該程式集中的服務進行註冊。如果程式集中不包含模組類,那麽框架不會掃描該程式集,也就不會註冊服務。
接下來,我們思考模組化框架需要解決哪些問題或支持哪些功能:
如何辨識和註冊服務;
框架能夠辨識模組的依賴,生成模組依賴樹,能夠檢測到迴圈依賴等問題;
多個模組可能參照了同一個模組 A,但是模組 A 只能被例項化一次;
初始化模組的順序;
模組類本身要作為服務註冊到容器中,例項化模組類時,需要支持依賴註入,也就是說模組類的建構函式可以註入其它服務;
我們先解決第一個問題,
因為自動服務註冊是根據模組所在的程式集掃描標記類,辨識所有使用了
InjectOnAttribute
特性的型別,所以我們可以先編寫一個程式集掃描方法,該方法的功能是透過程式集掃描所有型別,然後根據特性配置註冊服務。
///<summary>
/// 自動依賴註入
///</summary>
///<param name="services"></param>
///<param name="assembly"></param>
///<param name="injectTypes">已被註入的服務</param>
privatestaticvoidInitInjectService(IServiceCollection services, Assembly assembly, HashSet<Type> injectTypes)
{
// 只掃描可例項化的類,不掃描靜態類、介面、抽象類、巢狀類、非公開類等
foreach (var item in assembly.GetTypes().Where(x => x.Is class && !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;
}
}
// 註入父類
if (inject.Scheme == InjectScheme.OnlyBase class || inject.Scheme == InjectScheme.Any)
{
var baseType = item.BaseType;
if (baseType == null) thrownew ArgumentException($"{item.Name} 註入模式 {nameof(inject.Scheme)} 未找到父類!");
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) thrownew ArgumentException($"{item.Name} 註入模式 {nameof(inject.Scheme)} 未找到服務!");
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;
}
}
}
}
定義兩個擴充套件函式,用於註入入口模組。
///<summary>
/// 註冊模組化服務
///</summary>
///<typeparam name="TModule">入口模組</typeparam>
///<param name="services"></param>
publicstaticvoidAddModule<TModule>(this IServiceCollection services)
where TModule : IModule
{
AddModule(services, typeof(TModule));
}
///<summary>
/// 註冊模組化服務
///</summary>
///<param name="services"></param>
///<param name="startupModule">入口模組</param>
publicstaticvoidAddModule(this IServiceCollection services, Type startupModule)
{
if (startupModule?.GetInterface(nameof(IModule)) == null)
{
thrownew TypeLoadException($"{startupModule?.Name} 不是有效的模組類");
}
IServiceProvider scope = BuildModule(services, startupModule);
}
框架需要從入口模組程式集開始尋找被依賴的模組程式集,然後透過後序遍歷初始化每個模組,並掃描該模組程式集中的服務。
建立一個
BuildModule
函式,BuildModule 為構建模組依賴樹、初始化模組提前建立環境。
///<summary>
/// 構建模組依賴樹並初始化模組
///</summary>
///<param name="services"></param>
///<param name="startupModule"></param>
///<returns></returns>
///<exception cref="InvalidOperationException"></exception>
privatestatic IServiceProvider BuildModule(IServiceCollection services, Type startupModule)
{
// 生成根模組
ModuleNode rootTree = new ModuleNode()
{
ModuleType = startupModule,
Childs = new HashSet<ModuleNode>()
};
// 根模組依賴的其他模組
// IModule => InjectModuleAttribute
var rootDependencies = startupModule.GetCustomAttributes(false)
.Where(x => x.GetType().IsSub classOf(typeof(InjectModuleAttribute)))
.OfType<InjectModuleAttribute>();
// 構建模組依賴樹
BuildTree(services, rootTree, rootDependencies);
// 構建一個 Ioc 例項,以便初始化模組類
var scope = services.BuildServiceProvider();
// 初始化所有模組類
var serviceContext = new ServiceContext(services, scope.GetService<IConfiguration>()!);
// 記錄已經處理的程式集、模組和服務,以免重復處理
HashSet<Assembly> moduleAssemblies = new HashSet<Assembly> { startupModule.Assembly };
HashSet<Type> moduleTypes = new HashSet<Type>();
HashSet<Type> injectTypes = new HashSet<Type>();
// 後序遍歷樹並初始化每個模組
InitModuleTree(scope, serviceContext, moduleAssemblies, moduleTypes, injectTypes, rootTree);
return scope;
}
第一步,構建模組依賴樹。
///<summary>
/// 構建模組依賴樹
///</summary>
///<param name="services"></param>
///<param name="currentNode"></param>
///<param name="injectModules">其依賴的模組</param>
privatestaticvoidBuildTree(IServiceCollection services, ModuleNode currentNode, IEnumerable<InjectModuleAttribute> 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
};
// 迴圈依賴檢測
// 檢查當前模組(parentTree)依賴的模組(childTree)是否在之前出現過,如果是,則說明是迴圈依賴
var isLoop = currentNode.ContainsTree(childTree);
if (isLoop)
{
thrownew OverflowException($"檢測到迴圈依賴參照或重復參照!{currentNode.ModuleType.Name} 依賴的 {childModule.ModuleType.Name} 模組在其父模組中出現過!");
}
if (currentNode.Childs == null)
{
currentNode.Childs = new HashSet<ModuleNode>();
}
currentNode.Childs.Add(childTree);
// 子模組依賴的其他模組
var childDependencies = childModule.ModuleType.GetCustomAttributes(inherit: false)
.Where(x => x.GetType().IsSub classOf(typeof(InjectModuleAttribute))).OfType<InjectModuleAttribute>().ToHashSet();
// 子模組也依賴其他模組
BuildTree(services, childTree, childDependencies);
}
}
透過後序遍歷辨識依賴時,由於一個模組可能會出現多次,所以初始化時需要判斷模組是否已經初始化,然後對模組進行初始化並掃描模組程式集中所有的型別,進行服務註冊。
///<summary>
/// 從模組樹中遍歷
///</summary>
///<param name="serviceProvider"></param>
///<param name="context"></param>
///<param name="moduleTypes">已經被註冊到容器中的模組類</param>
///<param name="moduleAssemblies">模組類所在的程式集</param>'
///<param name="injectTypes">已被註冊到容器的服務</param>
///<param name="moduleNode">模組節點</param>
privatestaticvoidInitModuleTree(IServiceProvider serviceProvider,
ServiceContext context,
HashSet<Assembly> moduleAssemblies,
HashSet<Type> moduleTypes,
HashSet<Type> injectTypes,
ModuleNode moduleNode)
{
if (moduleNode.Childs != null)
{
foreach (var item in moduleNode.Childs)
{
InitModuleTree(serviceProvider, context, moduleAssemblies, moduleTypes, injectTypes, item);
}
}
// 如果模組沒有處理過
if (!moduleTypes.Contains(moduleNode.ModuleType))
{
moduleTypes.Add(moduleNode.ModuleType);
// 例項化此模組
// 掃描此模組(程式集)中需要依賴註入的服務
var module = (IModule)serviceProvider.GetRequiredService(moduleNode.ModuleType);
module.ConfigureServices(context);
InitInjectService(context.Services, moduleNode.ModuleType.Assembly, injectTypes);
moduleAssemblies.Add(moduleNode.ModuleType.Assembly);
}
}
至此,Maomi.Core 所有的程式碼都已經講解完畢,透過本章的實踐,我們擁有了一個具有模組化和自動服務註冊的框架。可是,別高興得太早,我們應當如何驗證框架是可靠的呢?答案是單元測試。在完成 Maomi.Core 計畫之後,筆者立即編寫了 Maomi.Core.Tests 單元測試計畫,只有當單元測試全部透過之後,筆者才能自信地把程式碼放到書中。為計畫編寫單元測試是一個好習慣,尤其是對框架類的計畫,我們需要編寫大量的單元測試驗證框架的可靠性,同時單元測試中大量的範例是其他開發者了解框架、入手框架的極佳參考。
釋出到 nuget
們開發了一個支持模組化和自動服務註冊的框架,透過 Maomi.Core 實作模組化套用的開發。
完成程式碼後,我們需要將程式碼共享給其他人,那麽可以使用 nuget 包的方式。
當類別庫開發完成後,我們可以打包成 nuget 檔,上傳這個到 nuget.org ,或者是內部的私有倉庫,供其他開發者使用。
在
Maomi.Core.csproj
計畫的的
PropertyGroup
內容中加上以下配置,以便能夠在釋出類別庫時,生成 nuget 包。
<IsPackable>true</IsPackable>
<PackageVersion>1.0.0</PackageVersion>
<Title>貓咪框架</Title>
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
或者右鍵點選計畫-內容-打包。
當然,你也可以在 Visual Studio 中點選計畫右鍵內容,在面板中進行視覺化配置。
你可以配置計畫的 github 地址、釋出說明、開源授權證等。
配置完成後,可以使用 Visual Studio 釋出計畫,或使用
dotnet publish -c Release
命令釋出計畫。
釋出計畫後,可以在輸出目錄找到
.nupkg
檔。
開啟 https://www.nuget.org/packages/manage/upload ,登入後上傳
.nupkg
檔。
制作樣版計畫
在 .NET 中,安裝 .NET SDK 時預設攜帶了一些計畫樣版,使用
dotnet new list
可以看到本機中已經按照的計畫樣版,然後透過
dotnet new {樣版名稱}
命令使用樣版快速建立一個套用。
透過樣版建立一個套用是很方便的,計畫樣版提前組織好解決方案中的計畫結構、程式碼檔,開發者使用樣版時只需要提供一個名稱,然後即可生成一個完整的套用。那麽在本節中,筆者將會介紹如何制作自己的計畫樣版,進一步打包到 nuget 中,分享給更多的開發者使用。當然,在企業開發中,架構師可以規劃好基礎程式碼、設計計畫架構,然後制作樣版計畫,業務開發者需要建立新的計畫時,從企業基礎計畫樣版一鍵生成即可,從而可以快速開發計畫。
本節的範例程式碼在
demo/1/templates
中。
讓我們來體驗筆者已經制作好的計畫樣版,執行以下命令從 nuget 中安裝樣版。
dotnet new install Maomi.Console.Templates::2.0.0
命令執行完畢後,控制台會打印:
樣版名 短名稱 語言 標記
------------ ------ ---- --------------
Maomi 控制台 maomi [C#] Common/Console
使用樣版名稱
maomi
建立自訂名稱的計畫:
dotnet new maomi --name MyTest
開啟 Visual Studio,可以看到最近透過 nuget 安裝的樣版。
接下來,我們來上手制作一個屬於自己的樣版。
開啟
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
建立 MaomiPack.csproj 檔(名稱可以自訂),該檔用於將程式碼打包到 nuget 包中,否則 dotnet cli 會先編譯計畫再打包到 nuget 包中。
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<PackageType>Template</PackageType>
<PackageVersion>2.0.0</PackageVersion>
<PackageId>Maomi.Console.Templates</PackageId>
<PackageTags>dotnet-new;templates;contoso</PackageTags>
<Title>Maomi 框架控制台樣版</Title>
<Authors>癡者工良</Authors>
<Description>用於示範 Maomi 框架的樣版計畫包.</Description>
<TargetFramework>net8.0</TargetFramework>
<IncludeContentInPack>true</IncludeContentInPack>
<IncludeBuildOutput>false</IncludeBuildOutput>
<ContentTargetFolders>content</ContentTargetFolders>
<NoWarn>$(NoWarn);NU5128</NoWarn>
</PropertyGroup>
<ItemGroup>
<Content Include="templates\**\*"Exclude="templates\**\bin\**;templates\**\obj\**" />
<Compile Remove="**\*" />
</ItemGroup>
</Project>
PackageVersion
:樣版版本號。
PackageId
:樣版 id,在 nuget.org 中唯一。
PackageTags
:nuget 包的標記。
Title
:nuget 包標題。
Authors
:作者名稱。
Description
:nuget 包描述。
建立一個空目錄儲存計畫程式碼,一般使用 templates 命名,你可以參考
demo/1/templates/templates
中的解決方案。接著在該目錄下建立
template.json
檔,檔內容如下:
{
"$schema": "http://json.schemastore.org/template",
"author": "癡者工良",
" classifications": [
"Common",
"Console"
],
"identity": "Maomi.Console",
"name": "Maomi 控制台",
"description": "這是一個使用 Maomi.Core 搭建的模組化套用樣版。",
"shortName": "maomi",
"tags": {
"language": "C#",
"type": "project"
},
"sourceName": "Maomi",
"preferNameDirectory": true
}
template.json
檔用於配置計畫樣版內容,在安裝樣版後相關資訊會顯示到 Visual Studio 計畫樣版列表,以及建立計畫時自動替換
Maomi
字首為自訂的名稱。
author
:作者名稱。
classifications
:計畫型別,如控制台、Web、Wpf 等。
identity
:樣版唯一標識。
name
:樣版名稱。
description
:
樣版描述資訊
。
shortName
:縮寫,使用
dotnet new {shortName}
命令時可以簡化樣版名稱。
tags
:指定了樣版使用的語言和計畫型別。
sourceName
:可以被替換的名稱,例如
Maomi.Console
將會被替換為
MyTest.Console
,樣版中所有檔名稱、字串內容都會被替換。
組織好樣版之後,在 MaomiPack.csproj 所在目錄下執行
dotnet pack
命令打包計畫為 nuget 包,最後根據提示生成的 nuget 檔,上傳到 nuget.org 即可。