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

零基礎(chǔ)寫(xiě)框架:從零設(shè)計(jì)一個(gè)模塊化和自動(dòng)服務(wù)注冊(cè)框架

來(lái)源:好特整理 | 時(shí)間:2024-06-03 09:45:40 | 閱讀:109 |  標(biāo)簽: 基礎(chǔ) 注冊(cè) 一個(gè) 服務(wù) 設(shè)計(jì)   | 分享到:

關(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.whuanle.cn

https://www.cnblogs.com/whuanle

模塊化和自動(dòng)服務(wù)注冊(cè)

基于 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 接口。

零基礎(chǔ)寫(xiě)框架:從零設(shè)計(jì)一個(gè)模塊化和自動(dòng)服務(wù)注冊(cè)框架

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

模塊可以依賴(lài)注入

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

零基礎(chǔ)寫(xiě)框架:從零設(shè)計(jì)一個(gè)模塊化和自動(dòng)服務(wù)注冊(cè)框架

每個(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)行初始化。

循環(huán)依賴(lài)檢測(cè)

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。

零基礎(chǔ)寫(xiě)框架:從零設(shè)計(jì)一個(gè)模塊化和自動(dòng)服務(wù)注冊(cè)框架

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ù)如圖所示:

零基礎(chǔ)寫(xiě)框架:從零設(shè)計(jì)一個(gè)模塊化和自動(dòng)服務(wù)注冊(cè)框架

首先從 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ò)):

零基礎(chǔ)寫(xiě)框架:從零設(shè)計(jì)一個(gè)模塊化和自動(dòng)服務(wù)注冊(cè)框架

服務(wù)自動(dòng)注冊(cè)

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)]

模塊化和自動(dòng)服務(wù)注冊(cè)的設(shè)計(jì)和實(shí)現(xiàn)

在本小節(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è)框架的能力。

項(xiàng)目說(shuō)明

創(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

模塊化設(shè)計(jì)

當(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)服務(wù)注冊(cè)的設(shè)計(jì)

當(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;
        }
    }

模塊依賴(lài)

因?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)多次。

零基礎(chǔ)寫(xiě)框架:從零設(shè)計(jì)一個(gè)模塊化和自動(dòng)服務(wù)注冊(cè)框架

那么,如果識(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ù)如圖所示:

零基礎(chǔ)寫(xiě)框架:從零設(shè)計(jì)一個(gè)模塊化和自動(dòng)服務(wù)注冊(cè)框架

首先從 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ò)):

零基礎(chǔ)寫(xiě)框架:從零設(shè)計(jì)一個(gè)模塊化和自動(dòng)服務(wù)注冊(cè)框架

偽代碼示例如下:

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

實(shí)現(xiàn)模塊化和自動(dòng)服務(wù)注冊(cè)

本小節(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ā)者了解框架、入手框架的極佳參考。

發(fā)布到 nuget

們開(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)目-屬性-打包。

零基礎(chǔ)寫(xiě)框架:從零設(shè)計(jì)一個(gè)模塊化和自動(dòng)服務(wù)注冊(cè)框架

當(dāng)然,你也可以在 Visual Studio 中點(diǎn)擊項(xiàng)目右鍵屬性,在面板中進(jìn)行可視化配置。

零基礎(chǔ)寫(xiě)框架:從零設(shè)計(jì)一個(gè)模塊化和自動(dòng)服務(wù)注冊(cè)框架

你可以配置項(xiàng)目的 github 地址、發(fā)布說(shuō)明、開(kāi)源許可證等。

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

零基礎(chǔ)寫(xiě)框架:從零設(shè)計(jì)一個(gè)模塊化和自動(dòng)服務(wù)注冊(cè)框架

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

零基礎(chǔ)寫(xiě)框架:從零設(shè)計(jì)一個(gè)模塊化和自動(dòng)服務(wù)注冊(cè)框架

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

零基礎(chǔ)寫(xiě)框架:從零設(shè)計(jì)一個(gè)模塊化和自動(dòng)服務(wù)注冊(cè)框架

制作模板項(xiàng)目

在 .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

零基礎(chǔ)寫(xiě)框架:從零設(shè)計(jì)一個(gè)模塊化和自動(dòng)服務(wù)注冊(cè)框架

打開(kāi) Visual Studio,可以看到最近通過(guò) nuget 安裝的模板。

零基礎(chǔ)寫(xiě)框架:從零設(shè)計(jì)一個(gè)模塊化和自動(dòng)服務(wù)注冊(cè)框架

接下來(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 即可。

小編推薦閱讀

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

相關(guān)視頻攻略

更多

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

掃二維碼進(jìn)入好特網(wǎng)微信公眾號(hào)!

本站所有軟件,都由網(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)