您的位置:首頁 > 軟件教程 > 教程 > 零基礎(chǔ)寫框架:從零設(shè)計一個模塊化和自動服務(wù)注冊框架

零基礎(chǔ)寫框架:從零設(shè)計一個模塊化和自動服務(wù)注冊框架

來源:好特整理 | 時間:2024-06-03 09:45:40 | 閱讀:78 |  標簽: 基礎(chǔ) 注冊 一個 服務(wù) 設(shè)計   | 分享到:

關(guān)于從零設(shè)計 .NET 開發(fā)框架 作者:癡者工良 教程說明: 倉庫地址:https://github.com/whuanle/maomi 文檔地址:https://maomi.whuanle.cn 作者博客: https://www.whuanle.cn https://www.cnblogs.co

關(guān)于從零設(shè)計 .NET 開發(fā)框架
作者:癡者工良
教程說明:

倉庫地址: https://github.com/whuanle/maomi

文檔地址: https://maomi.whuanle.cn

作者博客:

https://www.whuanle.cn

https://www.cnblogs.com/whuanle

模塊化和自動服務(wù)注冊

基于 ASP.NET Core 開發(fā)的 Web 框架中,最著名的是 ABP,ABP 主要特點之一開發(fā)不同項目(程序集)時,在每個項目中創(chuàng)建一個模塊類,程序加載每個程序集中,掃描出所有的模塊類,然后通過模塊類作為入口,初始化程序集。

使用模塊化開發(fā)程序,好處是不需要關(guān)注程序集如何加載配置。開發(fā)人員開發(fā)程序集時,在模塊類中配置如何初始化、如何讀取配置,使用者只需要將模塊類引入進來即可,由框架自動啟動模塊類。

Maomi.Core 也提供了模塊化開發(fā)的能力,同時還包括簡單易用的自動服務(wù)注冊。Maomi.Core 是一個很簡潔的包,可以在控制臺、Web 項目、WPF 項目中使用,在 WPF 項目中結(jié)合 MVVM 可以大量減少代碼復(fù)雜度,讓代碼更加清晰明朗。

快速入手

有 Demo1.Api、Demo1.Application 兩個項目,每個項目都有一個模塊類,模塊類需要實現(xiàn) IModule 接口。

零基礎(chǔ)寫框架:從零設(shè)計一個模塊化和自動服務(wù)注冊框架

Demo1.Application 項目的 ApplicationModule.cs 文件內(nèi)容如下:

    public class ApplicationModule : IModule
    {
        // 模塊類中可以使用依賴注入
        private readonly IConfiguration _configuration;
        public ApplicationModule(IConfiguration configuration)
        {
            _configuration = configuration;
        }

        public void ConfigureServices(ServiceContext services)
        {
            // 這里可以編寫模塊初始化代碼
        }
    }

如果要將服務(wù)注冊到容器中,在 class 上加上 [InjectOn] 特性即可。

    public interface IMyService
    {
        int Sum(int a, int b);
    }

    [InjectOn] // 自動注冊的標記
    public class MyService : IMyService
    {
        public int Sum(int a, int b)
        {
            return a + b;
        }
    }

上層模塊 Demo1.Api 中的 ApiModule.cs 可以通過特性注解引用底層模塊。

    [InjectModule]
    public class ApiModule : IModule
    {
        public void ConfigureServices(ServiceContext services)
        {
            // 這里可以編寫模塊初始化代碼
        }
    }

最后,在程序啟動時配置模塊入口,并進行初始化。

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

// 注冊模塊化服務(wù),并設(shè)置 ApiModule 為入口
builder.Services.AddModule();

var app = builder.Build();

模塊可以依賴注入

在 ASP.NET Core 配置 Host 時,會自動注入一些框架依賴的服務(wù),如 IConfiguration 等,因此在 .AddModule() 開始初始化模塊服務(wù)時,模塊獲取已經(jīng)注入的服務(wù)。

零基礎(chǔ)寫框架:從零設(shè)計一個模塊化和自動服務(wù)注冊框架

每個模塊都需要實現(xiàn) IModule 接口,其定義如下:

    /// 
    /// 模塊接口
    /// 
    public interface IModule
    {
        /// 
        /// 模塊中的依賴注入
        /// 
        /// 模塊服務(wù)上下文
        void ConfigureServices(ServiceContext context);
    }

除了可以直接在模塊構(gòu)造函數(shù)注入服務(wù)之外,還可以通過 ServiceContext context 獲取服務(wù)和配置。

    /// 
    /// 模塊上下文
    /// 
    public class ServiceContext
    {
        private readonly IServiceCollection _serviceCollection;
        private readonly IConfiguration _configuration;


        internal ServiceContext(IServiceCollection serviceCollection, IConfiguration configuration)
        {
            _serviceCollection = serviceCollection;
            _configuration = configuration;
        }

        /// 
        /// 依賴注入服務(wù)
        /// 
        public IServiceCollection Services => _serviceCollection;

        /// 
        /// 配置
        /// 
        public IConfiguration Configuration => _configuration;
    }

模塊化

因為模塊之間會有依賴關(guān)系,為了識別這些依賴關(guān)系,Maomi.Core 使用樹來表達依賴關(guān)系。Maomi.Core 在啟動模塊服務(wù)時,掃描所有模塊類,然后將模塊依賴關(guān)系存放到模塊樹中,然后按照左序遍歷的算法對模塊逐個初始化,也就是先從底層模塊開始進行初始化。

循環(huán)依賴檢測

Maomi.Core 可以識別模塊循環(huán)依賴

比如,有以下模塊和依賴:

[InjectModule()]
[InjectModule()]
class C:IModule

[InjectModule()]
class B:IModule

// 這里出現(xiàn)了循環(huán)依賴
[InjectModule()]
class A:IModule

// C 是入口模塊
services.AddModule();

因為 C 模塊依賴 A、B 模塊,所以 A、B 是節(jié)點 C 的子節(jié)點,而 A、B 的父節(jié)點則是 C。當把 A、B、C 三個模塊以及依賴關(guān)系掃描完畢之后,會得到以下的模塊依賴樹。

如下圖所示,每個模塊都做了下標,表示不同的依賴關(guān)系,一個模塊可以出現(xiàn)多次, C1 -> A0 表示 C 依賴 A。

零基礎(chǔ)寫框架:從零設(shè)計一個模塊化和自動服務(wù)注冊框架

C 0 開始,沒有父節(jié)點,則不存在循環(huán)依賴。

從 A 0 開始,A 0 -> C 0 ,該鏈路中也沒有出現(xiàn)重復(fù)的 A 模塊。

從 C 1 開始,C 1 -> A 0 -> C 0 ,該鏈路中 C 模塊重復(fù)出現(xiàn),則說明出現(xiàn)了循環(huán)依賴。

從 C 2 開始,C 2 -> A 1 -> B 0 -> C 0 ,該鏈路中 C 模塊重復(fù)出現(xiàn),則說明出現(xiàn)了循環(huán)依賴。

模塊初始化順序

在生成模塊樹之后,通過對模塊樹進行后序遍歷即可。

比如,有以下模塊以及依賴。

[InjectModule()]
[InjectModule()]
class E:IModule

[InjectModule()]
[InjectModule()]
class C:IModule

[InjectModule()]
class D:IModule
    
[InjectModule()]
class B:IModule
    
class A:IModule

// E 是入口模塊
services.AddModule();

生成模塊依賴樹如圖所示:

零基礎(chǔ)寫框架:從零設(shè)計一個模塊化和自動服務(wù)注冊框架

首先從 E 0 開始掃描,因為 E 0 下存在子節(jié)點 C 0 、 D 0 ,那么就會先順著 C 0 再次掃描,掃描到 A 0 時,因為 A 0 下已經(jīng)沒有子節(jié)點了,所以會對 A 0 對應(yīng)的模塊 A 進行初始化。根據(jù)上圖模塊依賴樹進行后序遍歷,初始化模塊的順序是(已經(jīng)被初始化的模塊會跳過):

零基礎(chǔ)寫框架:從零設(shè)計一個模塊化和自動服務(wù)注冊框架

服務(wù)自動注冊

Maomi.Core 是通過 [InjectOn] 識別要注冊該服務(wù)到容器中,其定義如下:

    /// 
    /// 依賴注入標記
    /// 
    [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] 時,默認是注冊服務(wù)為 Transient 生命周期,且注冊所有接口。

    [InjectOn]
    public class MyService : IAService, IBService

等同于:

services.AddTransient();
services.AddTransient();

如果只想注冊 IAService ,可以將注冊模式設(shè)置為 InjectScheme.Some ,然后自定義注冊的類型:

    [InjectOn(
        lifetime: ServiceLifetime.Transient,
        Scheme = InjectScheme.Some,
        ServicesType = new Type[] { typeof(IAService) }
        )]
    public class MyService : IAService, IBService

也可以把自身注冊到容器中:

	[InjectOn(Own = true)]
	public class MyService : IMyService

等同于:

services.AddTransient();
services.AddTransient();

如果服務(wù)繼承了類、接口,只想注冊父類,那么可以這樣寫:

    public class ParentService { }

    [InjectOn(
        Scheme = InjectScheme.OnlyBaseClass
        )]
    public class MyService : ParentService, IDisposable 

等同于:

services.AddTransient();
services.AddTransient();

如果只注冊自身,忽略接口等,可以使用:

[InjectOn(ServiceLifetime.Scoped, Scheme = InjectScheme.None, Own = true)]

模塊化和自動服務(wù)注冊的設(shè)計和實現(xiàn)

在本小節(jié)中,我們將會開始設(shè)計一個支持模塊化和自動服務(wù)注冊的小框架,從設(shè)計和實現(xiàn) Maomi.Core 開始,我們在后面的章節(jié)中會掌握更多框架技術(shù)的設(shè)計思路和實現(xiàn)方法,從而掌握從零開始編寫一個框架的能力。

項目說明

創(chuàng)建一個名為 Maomi.Core 的類庫項目,這個類庫中將會包含框架核心抽象和實現(xiàn)代碼。

為了減少命名空間長度,便于開發(fā)的時候引入需要的命名空間,打開 Maomi.Core.csproj 文件,在 PropertyGroup 屬性中,添加一行配置:

Maomi

配置 屬性之后,我們在 Maomi.Core 項目中創(chuàng)建的類型,其命名空間都會以 Maomi. 開頭,而不是 Maomi.Core 。

接著為項目添加兩個依賴包,以便實現(xiàn)自動依賴注入和初始化模塊時提供配置。

Microsoft.Extensions.DependencyInjection
Microsoft.Extensions.Configuration.Abstractions

模塊化設(shè)計

當本章的代碼編寫完畢之后,我們可以這樣實現(xiàn)一個模塊、初始化模塊、引入依賴模塊。代碼示例如下:

    [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();
        }
    }

從這段代碼,筆者以從上到下的順序來解讀我們需要實現(xiàn)哪些技術(shù)點。

1,模塊依賴。

[InjectModule] 表示當前模塊需要依賴哪些模塊。如果需要依賴多個模塊,可以使用多個特性,示例如下:

[InjectModule]
[InjectModule]

2,模塊接口和初始化。

每一個模塊都需要實現(xiàn) IModule 接口,框架識別到類型繼承了這個接口后才會把類型當作一個模塊類進行處理。IModule 接口很簡單,只有 ConfigureServices(ServiceContext context) 一個方法,可以在這個方法中編寫初始化模塊的代碼。ConfigureServices 方法中有一個 ServiceContext 類型的參數(shù), ServiceContext 中包含了 IServiceCollection、IConfiguration ,模塊可以從 ServiceContext 中獲得當前容器的服務(wù)、啟動時的配置等。

3,依賴注入

每個模塊的構(gòu)造函數(shù)都可以使用依賴注入,可以在模塊類中注入需要的服務(wù),開發(fā)者可以在模塊初始化時,通過這些服務(wù)初始化模塊。

基于以上三點,我們可以先抽象出特性類、接口等,由于這些類型不包含具體的邏輯,因此從這一部分先下手,實現(xiàn)起來會更簡單,可以避免大腦混亂,編寫框架時不知道要從哪里先下手。

創(chuàng)建一個 ServiceContext 類,用于在模塊間傳遞服務(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ù)實際需求,還可以在 ServiceContext 中添加日志等屬性字段。

創(chuàng)建 IModule 接口。

    public interface IModule
    {
        void ConfigureServices(ServiceContext services);
    }

創(chuàng)建 InjectModuleAttribute 特性,用于引入依賴模塊。

    [AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false)]
    public class InjectModuleAttribute : Attribute
    {
        // 依賴的模塊
        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 的新語法。

定義兩個特性類后,我們可以使用 [InjectModule(typeof(AppModule))] InjectModule 的方式定義依賴模塊。

自動服務(wù)注冊的設(shè)計

當完成本章的代碼編寫后,如果需要注入服務(wù),只需要標記 [InjectOn] 特性即可。

// 簡單注冊
[InjectOn]
public class MyService : IMyService
// 注注冊并設(shè)置生命周期為 scope
[InjectOn(ServiceLifetime.Scoped)]
public class MyService : IMyService

// 只注冊接口,不注冊父類
[InjectOn(InjectScheme.OnlyInterfaces)]
public class MyService : ParentService, IMyService

有時我們會有各種各樣的需求,例如 MyService 繼承了父類 ParentService 和接口 IMyService ,但是只需要注冊 ParentService ,而不需要注冊接口;又或者只需要注冊 MyService,而不需要注冊 ParentService IMyService 。

創(chuàng)建 InjectScheme 枚舉,定義注冊模式:

    public enum InjectScheme
    {
        // 注入父類、接口
        Any,
        
        // 手動選擇要注入的服務(wù)
        Some,
        
        // 只注入父類
        OnlyBaseClass,
        
        // 只注入實現(xiàn)的接口
        OnlyInterfaces,
        
        // 此服務(wù)不會被注入到容器中
        None
    }

定義服務(wù)注冊特性:

    // 依賴注入標記
    [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;
        }
    }

模塊依賴

因為模塊之間會有依賴關(guān)系,因此為了生成模塊樹,需要定義一個 ModuleNode 類表示模塊節(jié)點, 一個 ModuleNode 實例標識一個依賴關(guān)系 。

    /// 
    /// 模塊節(jié)點
    /// 
    internal class ModuleNode
    {
        // 當前模塊類型
        public Type ModuleType { get; set; } = null!;

        // 鏈表,指向父模塊節(jié)點,用于循環(huán)引用檢測
        public ModuleNode? ParentModule { get; set; }
        
        // 依賴的其它模塊
        public HashSet? Childs { get; set; }

        // 通過鏈表檢測是否出現(xiàn)了循環(huán)依賴
        public bool ContainsTree(ModuleNode childModule)
        {
            if (childModule.ModuleType == ModuleType) return true;
            if (this.ParentModule == null) return false;
            // 如果當前模塊找不到記錄,則向上查找
            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;
        }
    }

框架在掃描所有程序集之后,通過 ModuleNode 實例將所有模塊以及模塊依賴組成一顆模塊樹,通過模塊樹來判斷是否出現(xiàn)了循環(huán)依賴。

比如,有以下模塊和依賴:

[InjectModule()]
[InjectModule()]
class C:IModule

[InjectModule()]
class B:IModule

// 這里出現(xiàn)了循環(huán)依賴
[InjectModule()]
class A:IModule

// C 是入口模塊
services.AddModule();

因為 C 模塊依賴 A、B 模塊,所以 A、B 是節(jié)點 C 的子節(jié)點,而 A、B 的父節(jié)點則是 C。

C.Childs = new (){ A , B}

A.ParentModule => C
B.ParentModule => C

當把 A、B、C 三個模塊以及依賴關(guān)系掃描完畢之后,會得到以下的模塊依賴樹。一個節(jié)點即是一個 ModuleNode 實例,一個模塊被多次引入,就會出現(xiàn)多次。

零基礎(chǔ)寫框架:從零設(shè)計一個模塊化和自動服務(wù)注冊框架

那么,如果識別到循環(huán)依賴呢?只需要調(diào)用 ModuleNode.ContainsTree() 從一個 ModuleNode 實例中,不斷往上查找 ModuleNode.ParentModule 即可,如果該鏈表中包含相同類型的模塊,即為循環(huán)依賴,需要拋出異常。

比如從 C 0 開始,沒有父節(jié)點,則不存在循環(huán)依賴。

從 A 0 開始,A 0 -> C 0 ,該鏈路中也沒有出現(xiàn)重復(fù)的 A 模塊。

從 C 1 開始,C 1 -> A 0 -> C 0 ,該鏈路中 C 模塊重復(fù)出現(xiàn),則說明出現(xiàn)了循環(huán)依賴。

所以,是否出現(xiàn)了循環(huán)依賴判斷起來是很簡單的,我們只需要從 ModuleNode.ContainsTree() 往上查找即可。

在生成模塊樹之后,通過對模塊樹進行后序遍歷即可。

比如,有以下模塊以及依賴。

[InjectModule()]
[InjectModule()]
class E:IModule

[InjectModule()]
[InjectModule()]
class C:IModule

[InjectModule()]
class D:IModule
    
[InjectModule()]
class B:IModule
    
class A:IModule

// E 是入口模塊
services.AddModule();

生成模塊依賴樹如圖所示:

零基礎(chǔ)寫框架:從零設(shè)計一個模塊化和自動服務(wù)注冊框架

首先從 E 0 開始掃描,因為 E 0 下存在子節(jié)點 C 0 、 D 0 ,那么就會先順著 C 0 再次掃描,掃描到 A 0 時,因為 A 0 下已經(jīng)沒有子節(jié)點了,所以會對 A 0 對應(yīng)的模塊 A 進行初始化。根據(jù)上圖模塊依賴樹進行后序遍歷,初始化模塊的順序是(已經(jīng)被初始化的模塊會跳過):

零基礎(chǔ)寫框架:從零設(shè)計一個模塊化和自動服務(wù)注冊框架

偽代碼示例如下:

		private static void InitModuleTree(ModuleNode moduleNode)
		{
			if (moduleNode.Childs != null)
			{
				foreach (var item in moduleNode.Childs)
				{
					InitModuleTree(item);
				}
			}
            
            // 如果該節(jié)點已經(jīng)沒有子節(jié)點
			// 如果模塊沒有處理過
			if (!moduleTypes.Contains(moduleNode.ModuleType))
			{
				InitInjectService(moduleNode.ModuleType);
			}
		}

實現(xiàn)模塊化和自動服務(wù)注冊

本小節(jié)的代碼都在 ModuleExtensions.cs 中。

當我們把接口、枚舉、特性等類型定義之后,接下來我們便要思考如何實例化模塊、檢測模塊的依賴關(guān)系,實現(xiàn)自動服務(wù)注冊。為了簡化設(shè)計,我們可以將模塊化自動服務(wù)注冊寫在一起,當初始化一個模塊時,框架同時會掃描該程序集中的服務(wù)進行注冊。如果程序集中不包含模塊類,那么框架不會掃描該程序集,也就不會注冊服務(wù)。

接下來,我們思考模塊化框架需要解決哪些問題或支持哪些功能:

  • 如何識別和注冊服務(wù);

  • 框架能夠識別模塊的依賴,生成模塊依賴樹,能夠檢測到循環(huán)依賴等問題;

  • 多個模塊可能引用了同一個模塊 A,但是模塊 A 只能被實例化一次;

  • 初始化模塊的順序;

  • 模塊類本身要作為服務(wù)注冊到容器中,實例化模塊類時,需要支持依賴注入,也就是說模塊類的構(gòu)造函數(shù)可以注入其它服務(wù);

我們先解決第一個問題,

因為自動服務(wù)注冊是根據(jù)模塊所在的程序集掃描標記類,識別所有使用了 InjectOnAttribute 特性的類型,所以我們可以先編寫一個程序集掃描方法,該方法的功能是通過程序集掃描所有類型,然后根據(jù)特性配置注冊服務(wù)。

/// 
/// 自動依賴注入
/// 
/// 
/// 
/// 已被注入的服務(wù)
private static void InitInjectService(IServiceCollection services, Assembly assembly, HashSet injectTypes)
{
	// 只掃描可實例化的類,不掃描靜態(tà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;
			}
		}

		// 注入父類
		if (inject.Scheme == InjectScheme.OnlyBaseClass || inject.Scheme == InjectScheme.Any)
		{
			var baseType = item.BaseType;
			if (baseType == null) throw new 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) 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;
			}
		}
	}
}

定義兩個擴展函數(shù),用于注入入口模塊。

		/// 
		/// 注冊模塊化服務(wù)
		/// 
		/// 入口模塊
		/// 
		public static void AddModule(this IServiceCollection services)
			where TModule : IModule
		{
			AddModule(services, typeof(TModule));
		}


		/// 
		/// 注冊模塊化服務(wù)
		/// 
		/// 
		/// 入口模塊
		public static void AddModule(this IServiceCollection services, Type startupModule)
		{
			if (startupModule?.GetInterface(nameof(IModule)) == null)
			{
				throw new TypeLoadException($"{startupModule?.Name} 不是有效的模塊類");
			}

			IServiceProvider scope = BuildModule(services, startupModule);
		}

框架需要從入口模塊程序集開始查找被依賴的模塊程序集,然后通過后序遍歷初始化每個模塊,并掃描該模塊程序集中的服務(wù)。

創(chuàng)建一個 BuildModule 函數(shù),BuildModule 為構(gòu)建模塊依賴樹、初始化模塊提前創(chuàng)建環(huán)境。

		/// 
		/// 構(gòu)建模塊依賴樹并初始化模塊
		/// 
		/// 
		/// 
		/// 
		/// 
		private static IServiceProvider BuildModule(IServiceCollection services, Type startupModule)
		{
			// 生成根模塊
			ModuleNode rootTree = new ModuleNode()
			{
				ModuleType = startupModule,
				Childs = new HashSet()
			};

			// 根模塊依賴的其他模塊
			// IModule => InjectModuleAttribute
			var rootDependencies = startupModule.GetCustomAttributes(false)
				.Where(x => x.GetType().IsSubclassOf(typeof(InjectModuleAttribute)))
				.OfType();

			// 構(gòu)建模塊依賴樹
			BuildTree(services, rootTree, rootDependencies);

			// 構(gòu)建一個 Ioc 實例,以便初始化模塊類
			var scope = services.BuildServiceProvider();

			// 初始化所有模塊類
			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();

            // 后序遍歷樹并初始化每個模塊
			InitModuleTree(scope, serviceContext, moduleAssemblies, moduleTypes, injectTypes, rootTree);

			return scope;
		}

第一步,構(gòu)建模塊依賴樹。

		/// 
		/// 構(gòu)建模塊依賴樹
		/// 
		/// 
		/// 
		/// 其依賴的模塊
		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)依賴檢測
				// 檢查當前模塊(parentTree)依賴的模塊(childTree)是否在之前出現(xiàn)過,如果是,則說明是循環(huán)依賴
				var isLoop = currentNode.ContainsTree(childTree);
				if (isLoop)
				{
					throw new OverflowException($"檢測到循環(huán)依賴引用或重復(fù)引用!{currentNode.ModuleType.Name} 依賴的 {childModule.ModuleType.Name} 模塊在其父模塊中出現(xiàn)過!");
				}

				if (currentNode.Childs == null)
				{
					currentNode.Childs = new HashSet();
				}

				currentNode.Childs.Add(childTree);
				// 子模塊依賴的其他模塊
				var childDependencies = childModule.ModuleType.GetCustomAttributes(inherit: false)
					.Where(x => x.GetType().IsSubclassOf(typeof(InjectModuleAttribute))).OfType().ToHashSet();
				// 子模塊也依賴其他模塊
				BuildTree(services, childTree, childDependencies);
			}
		}

通過后序遍歷識別依賴時,由于一個模塊可能會出現(xiàn)多次,所以初始化時需要判斷模塊是否已經(jīng)初始化,然后對模塊進行初始化并掃描模塊程序集中所有的類型,進行服務(wù)注冊。

		/// 
		/// 從模塊樹中遍歷
		/// 
		/// 
		/// 
		/// 已經(jīng)被注冊到容器中的模塊類
		/// 模塊類所在的程序集'
		/// 已被注冊到容器的服務(wù)
		/// 模塊節(jié)點
		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);
				}
			}

			// 如果模塊沒有處理過
			if (!moduleTypes.Contains(moduleNode.ModuleType))
			{
				moduleTypes.Add(moduleNode.ModuleType);

				// 實例化此模塊
				// 掃描此模塊(程序集)中需要依賴注入的服務(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)講解完畢,通過本章的實踐,我們擁有了一個具有模塊化和自動服務(wù)注冊的框架。可是,別高興得太早,我們應(yīng)當如何驗證框架是可靠的呢?答案是單元測試。在完成 Maomi.Core 項目之后,筆者立即編寫了 Maomi.Core.Tests 單元測試項目,只有當單元測試全部通過之后,筆者才能自信地把代碼放到書中。為項目編寫單元測試是一個好習(xí)慣,尤其是對框架類的項目,我們需要編寫大量的單元測試驗證框架的可靠性,同時單元測試中大量的示例是其他開發(fā)者了解框架、入手框架的極佳參考。

發(fā)布到 nuget

們開發(fā)了一個支持模塊化和自動服務(wù)注冊的框架,通過 Maomi.Core 實現(xiàn)模塊化應(yīng)用的開發(fā)。

完成代碼后,我們需要將代碼共享給其他人,那么可以使用 nuget 包的方式。

當類庫開發(fā)完成后,我們可以打包成 nuget 文件,上傳這個到 nuget.org ,或者是內(nèi)部的私有倉庫,供其他開發(fā)者使用。

Maomi.Core.csproj 項目的的 PropertyGroup 屬性中加上以下配置,以便能夠在發(fā)布類庫時,生成 nuget 包。

		true
		1.0.0
		貓咪框架
		True

或者右鍵點擊項目-屬性-打包。

零基礎(chǔ)寫框架:從零設(shè)計一個模塊化和自動服務(wù)注冊框架

當然,你也可以在 Visual Studio 中點擊項目右鍵屬性,在面板中進行可視化配置。

零基礎(chǔ)寫框架:從零設(shè)計一個模塊化和自動服務(wù)注冊框架

你可以配置項目的 github 地址、發(fā)布說明、開源許可證等。

配置完成后,可以使用 Visual Studio 發(fā)布項目,或使用 dotnet publish -c Release 命令發(fā)布項目。

零基礎(chǔ)寫框架:從零設(shè)計一個模塊化和自動服務(wù)注冊框架

發(fā)布項目后,可以在輸出目錄找到 .nupkg 文件。

零基礎(chǔ)寫框架:從零設(shè)計一個模塊化和自動服務(wù)注冊框架

打開 https://www.nuget.org/packages/manage/upload ,登錄后上傳 .nupkg 文件。

零基礎(chǔ)寫框架:從零設(shè)計一個模塊化和自動服務(wù)注冊框架

制作模板項目

在 .NET 中,安裝 .NET SDK 時默認攜帶了一些項目模板,使用 dotnet new list 可以看到本機中已經(jīng)按照的項目模板,然后通過 dotnet new {模板名稱} 命令使用模板快速創(chuàng)建一個應(yīng)用。

通過模板創(chuàng)建一個應(yīng)用是很方便的,項目模板提前組織好解決方案中的項目結(jié)構(gòu)、代碼文件,開發(fā)者使用模板時只需要提供一個名稱,然后即可生成一個完整的應(yīng)用。那么在本節(jié)中,筆者將會介紹如何制作自己的項目模板,進一步打包到 nuget 中,分享給更多的開發(fā)者使用。當然,在企業(yè)開發(fā)中,架構(gòu)師可以規(guī)劃好基礎(chǔ)代碼、設(shè)計項目架構(gòu),然后制作模板項目,業(yè)務(wù)開發(fā)者需要創(chuàng)建新的項目時,從企業(yè)基礎(chǔ)項目模板一鍵生成即可,從而可以快速開發(fā)項目。

本節(jié)的示例代碼在 demo/1/templates 中。

讓我們來體驗筆者已經(jīng)制作好的項目模板,執(zhí)行以下命令從 nuget 中安裝模板。

dotnet new install Maomi.Console.Templates::2.0.0

命令執(zhí)行完畢后,控制臺會打印:

模板名        短名稱  語言  標記
------------  ------  ----  --------------
Maomi 控制臺  maomi   [C#]  Common/Console

使用模板名稱 maomi 創(chuàng)建自定義名稱的項目:

 dotnet new maomi --name MyTest

零基礎(chǔ)寫框架:從零設(shè)計一個模塊化和自動服務(wù)注冊框架

打開 Visual Studio,可以看到最近通過 nuget 安裝的模板。

零基礎(chǔ)寫框架:從零設(shè)計一個模塊化和自動服務(wù)注冊框架

接下來,我們來上手制作一個屬于自己的模板。

打開 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 文件(名稱可以自定義),該文件用于將代碼打包到 nuget 包中,否則 dotnet cli 會先編譯項目再打包到 nuget 包中。


  
    Template
    2.0.0
    Maomi.Console.Templates
    dotnet-new;templates;contoso

    Maomi 框架控制臺模板
    癡者工良
    用于示范 Maomi 框架的模板項目包.

    net8.0
   
    true
    false
    content
    $(NoWarn);NU5128
  

  
    
    
  


  • PackageVersion :模板版本號。
  • PackageId :模板 id,在 nuget.org 中唯一。
  • PackageTags :nuget 包的標記。
  • Title :nuget 包標題。
  • Authors :作者名稱。
  • Description :nuget 包描述。

創(chuà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 控制臺",
  "description": "這是一個使用 Maomi.Core 搭建的模塊化應(yīng)用模板。",
  "shortName": "maomi",
  "tags": {
    "language": "C#",
    "type": "project"
  },
  "sourceName": "Maomi",
  "preferNameDirectory": true
}

template.json 文件用于配置項目模板屬性,在安裝模板后相關(guān)信息會顯示到 Visual Studio 項目模板列表,以及創(chuàng)建項目時自動替換 Maomi 前綴為自定義的名稱。

  • author :作者名稱。
  • classifications :項目類型,如控制臺、Web、Wpf 等。
  • identity :模板唯一標識。
  • name :模板名稱。
  • description 模板描述信息 。
  • shortName :縮寫,使用 dotnet new {shortName} 命令時可以簡化模板名稱。
  • tags :指定了模板使用的語言和項目類型。
  • sourceName :可以被替換的名稱,例如 Maomi.Console 將會被替換為 MyTest.Console ,模板中所有文件名稱、字符串內(nèi)容都會被替換。

組織好模板之后,在 MaomiPack.csproj 所在目錄下執(zhí)行 dotnet pack 命令打包項目為 nuget 包,最后根據(jù)提示生成的 nuget 文件,上傳到 nuget.org 即可。

小編推薦閱讀

好特網(wǎng)發(fā)布此文僅為傳遞信息,不代表好特網(wǎng)認同期限觀點或證實其描述。

相關(guān)視頻攻略

更多

掃二維碼進入好特網(wǎng)手機版本!

掃二維碼進入好特網(wǎng)微信公眾號!

本站所有軟件,都由網(wǎng)友上傳,如有侵犯你的版權(quán),請發(fā)郵件[email protected]

湘ICP備2022002427號-10 湘公網(wǎng)安備:43070202000427號© 2013~2024 haote.com 好特網(wǎng)