探索ABP基礎架構

張飛洪[廈門]發表於2022-05-15

為了瞭解應用程式是如何配置和初始化,本文將探討ASP.NET Core和ABP框架最基本的構建模組。我們將從 ASP.NET Core 的 Startup類開始瞭解為什麼我們需要模組化系統,以及 ABP 如何提供模組化方式來配置和初始化應用程式。然後我們將探索 ASP.NET Core 的依賴注入,以及ABP是如何使用預定義規則(predefined rules)自動進行依賴注入。最後,我們將瞭解 ASP.NET Core 的配置和選項框架,以及其他類庫。

以下是本文的所有主題:

  • 瞭解模組化
  • 使用依賴注入系統
  • 配置應用程式
  • 實現選項模式
  • 日誌系統

一、瞭解模組化

模組化是一種將大型軟體按功能分解為更小的部分,並允許每個部分通過標準化介面進行通訊。模組化有以下主要好處:

  • 模組按規則進行隔離後,大大降低了系統複雜性。
  • 模組之間鬆散耦合,提供了更大的靈活性。因為模組是可組裝、可替換的。
  • 因為模組是獨立的,所以它允許跨應用被重用。

大多數企業的軟體被設計成模組化,但是,實現模組化並不容易。ABP 框架的主要目標之一是為模組化提供基礎設施和工具。我們將在後面詳細介紹模組化開發,本節只介紹 ABP 模組的基礎知識。

Startup 類

在定義ABP的模組之前,建議先熟悉 ASP.NET Core 中的StartUp類,我們看下ASP.NET Core 的Startup類:

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddMvc();
        services.AddTransient<MyService>();
    }
    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        app.UseRouting();
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
        app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllers();
        });
    }
}

ConfigureServices方法用於配置服務並將新服務註冊到依賴注入系統。另一方面,Configure方法用於配置 ASP.NET Core 管道中介軟體,用於處理 HTTP 請求。
在應用程式啟動之前,我們需要在Program.cs中配置Startup類:

public class Program
{
    public static void Main(string[] args)
    {
        CreateHostBuilder(args).Build().Run();
    }
    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args).ConfigureWebHostDefaults(webBuilder =>
            {
                webBuilder.UseStartup<Startup>();
            });
}

這個Startup類是獨一無二的,我們只有一個點來配置和初始化所有的服務。但是,在模組化應用程式中,我們希望每個模組都能獨立配置和初始化與該模組相關的服務。此外,一個模組通常需要使用或依賴於其他模組,因此模組配置順序和初始化就非常重要了。我們來看下 ABP 的模組是如何定義的

模組定義

ABP 模組是一組型別(比如類或介面),它們一同開發一同交付的。它是一個程式集(一般來說是Visual Studio 中的一個專案),派生自AbpModule,模組類負責配置和初始化,並在必要時配置依賴模組。

下面是一個簡訊傳送模組的簡單定義:

using Microsoft.Extensions.DependencyInjection;
using Volo.Abp.Modularity;
namespace SmsSending
{
    public class SmsSendingModule : AbpModule 
    {
        public override void ConfigureServices(
ServiceConfigurationContext context)
        {
            context.Services.AddTransient<SmsService>();
        }
    }
}

每個模組都可以重寫ConfigureServices方法,以便將其服務註冊到依賴注入系統。此示例中的SmsService服務被註冊為瞬態生命週期。該示例和上面Startup類似。但是,大多時候,您不需要手動註冊服務,這要歸功ABP 框架的按約定註冊系統。

OnApplicationInitialization方法用在服務註冊完成後,並且在應用準備就緒後執行。使用此方法,您可以在應用啟動時執行任何操作。例如,您可以初始化一個服務:

public class SmsSendingModule : AbpModule 
{
    //...
    public override void OnApplicationInitialization(ApplicationInitializationContext context)
    {
        var service = context.ServiceProvider.GetRequiredService<SmsService>();
        service.Initialize();
    }
}

這裡,我們使用context.ServiceProvider從依賴注入系統請求並初始化服務。可見,此時服務已經完成註冊。

您也可以將OnApplicationInitialization方法等同於Startup類的Configure方法。

您可以在此處構建 ASP.NET Core 請求管道。但是,通常我們會在啟動模組中配置請求管道,如下一節所述。

模組依賴和啟動模組

一個業務應用通常由多個模組組成,ABP 框架允許您宣告模組之間的依賴關係。一個應用必須要有一個啟動模組。啟動模組可以依賴於其他模組,其他模組可以再依賴於其他模組,以此類推。

下圖是一個簡單的模組依賴關係圖:

如果所示,如果模組 A 依賴於模組 B,則模組 B 總是在模組 A 之前初始化。這允許模組 A 使用、設定、更改或覆蓋模組 B 定義的配置和服務。

對於示例圖,模組初始化的順序應該是:G、F、E、D、B、C、A。

您不必知道確切的初始化順序;只需要知道如果你的模組依賴於模組xx,那麼模組xx在你的模組之前被初始化。

ABP使用[DependsOn](屬性宣告)方式來定義模組依賴:

[DependsOn(typeof(ModuleB), typeof(ModuleC))]
public class ModuleA : AbpModule
{    
}

這裡,ModuleA通過[DependsOn]依賴於ModuleBModuleC
本例中,啟動模組ModuleA負責設定ASP.NET Core 的請求管道:

[DependsOn(typeof(ModuleB), typeof(ModuleC))]
public class ModuleA : AbpModule
{
    //...
    public override void OnApplicationInitialization(ApplicationInitializationContext context)
    {
        var app = context.GetApplicationBuilder();
        var env = context.GetEnvironment();
        
        app.UseRouting();
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
        app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllers();
        });
    }
}

[程式碼塊和之前ASP.NET Core的 Startup類 建立請求管道相同。context.GetApplicationBuilder()context.GetEnvironment()用於從依賴注入中獲IApplicationBuilderIWebHostEnvironment服務。

最後,我們在Startup裡將ASP.NET Core 和 ABP 框架進行整合:

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddApplication<ModuleA>();
    }
    public void Configure(IApplicationBuilder app)
    {
        app.InitializeApplication();
    }
}

services.AddApplication()方法由 ABP 框架定義,用於ABP的模組配置。它按順序執行了所有模組的ConfigureServices方法。而app.InitializeApplication()方法也是由 ABP 框架定義,它也是按照模組依賴的順序來執行所有模組的OnApplicationInitialization方法。

ConfigureServicesOnApplicationInitialization方法是模組類中最常用的方法。

模組生命週期

AbpModule中定義的生命週期方法,除了上面看到的ConfigureServicesOnApplicationInitialization,下面羅列其他生命週期相關方法:

  • PreConfigureServices: 這個方法在ConfigureServices方法之前被呼叫。它允許您配置服務之前執行的程式碼。
  • ConfigureServices:這是配置模組和註冊服務的主要方法。
  • PostConfigureServices: 該方法在ConfigureServices之後呼叫(包括依賴於您模組的模組),這裡可以配置服務後執行的程式碼。
  • OnPreApplicationInitialization: 這個方法在OnApplicationInitialization之前被呼叫。在這個階段,您可以從依賴注入中解析服務,因為服務已經被初始化。
  • OnApplicationInitialization:此方法用來配置 ASP.NET Core 請求管道並初始化您的服務。
  • OnPostApplicationInitialization: 這個方法在初始化階段後被呼叫。
  • OnApplicationShutdown:您可以根據需要自己實現模組的關閉邏輯。
    Pre…Post…字首的方法與原始方法具有相同的目的。它們提供了一種在模組之前或之後執行的一些配置/初始化程式碼,一般情況下我們很少使用到。

非同步生命週期方法

本節介紹的生命週期方法是同步的。在編寫本書時,ABP 框架團隊正努力在 框架 5.1 版本引入非同步生命週期方法。

如前所述,模組類主要包含註冊和配置與該模組相關的服務的程式碼。在下一節中,我們將介紹如何使用 ABP 框架註冊服務。

二、使用依賴注入系統

.NET 原生依賴注入

依賴注入是一種獲取類的依賴的技術,它將建立類與使用該類分開。

假設我們有一個UserRegistrationService類,它呼叫SmsService類來傳送驗證簡訊,如下:

public class UserRegistrationService
{
    private readonly SmsService _smsService;
    public UserRegistrationService(SmsService smsService)
    {
        _smsService = smsService;
    }
    public async Task RegisterAsync(
        string username,
        string password,
        string phoneNumber)
    {
        //...save user in the database
        await _smsService.SendAsync(
            phoneNumber,
            "Your verification code: 1234"
        );
    }
}

這裡的SmsService使用建構函式注入來獲取例項。也就是說,依賴注入系統會自動幫我們例項化類的依賴項,並將它們賦值給我們的_smsService。

注意:ABP採用的是ASP.NET Core原生的依賴注入框架,他自己並沒有發明依賴注入框架。

在設計服務時,我們還要考慮另外一件重要的事情:服務生命週期。
ASP.NET Core 為服務註冊提供了三個生命週期選項:

  • Transient(瞬態):每次您請求/注入服務時,都會建立一個新例項。
  • Scoped(範圍): 通常這由請求生命週期來評估,您只有在同一範圍內才能共享相同的例項。
  • Singleton(單例):在應用內有且僅有一個例項。所有請求都使用相同的例項。該物件在第一次請求建立。
    以下模組註冊了兩個服務,一個是瞬態的,另一個是單例的:
public class MyModule : AbpModule
{
    public override void ConfigureServices(ServiceConfigurationContext context)
    {
        context.Services.AddTransient<ISmsService, SmsService>();
        context.Services.AddSingleton<OtherService>();
    }
}

context.Services的型別是IServiceCollection,它是一個擴充套件方法。

在第一個示例中使用介面註冊,第二個示例使用引用類註冊為單例。

ABP的依賴注入

使用 ABP 框架時,您不必考慮服務註冊,這要歸功於 ABP 框架獨特的服務註冊系統。

1.約定式註冊

在 ASP.NET Core 中,所有服務需要顯式註冊到IServiceCollection,如上一節所示。這些註冊大多重複,完全可以自動化操作。

ABP 對於以下型別採用自動註冊:

  • MVC controllers
  • Razor page models
  • View components
  • Razor components
  • SignalR hubs
  • Application services
  • Domain services
  • Repositories
    以上型別均使用瞬態生命週期自動註冊。如果您還有別的型別,可以考慮介面註冊。

2.介面註冊

您可以實現以下三種介面來註冊:

  • ITransientDependency
  • IScopedDependency
  • ISingletonDependency

例如,在下面程式碼塊中,我們將服務註冊為單例:

public class UserPermissionCache : ISingletonDependency
{ }

介面註冊很容易並且是推薦的方式,但與下面的屬性註冊相比,它有一定的侷限性。

3.屬性註冊

屬性註冊更精細,下面是和屬性註冊相關的配置引數

  • Lifetime(enum): 服務的生命週期,包括Singleton,TransientScoped
  • TryRegister(bool):僅當服務尚未註冊時才註冊
  • ReplaceServices(bool):如果服務已經註冊,則替換之前的註冊

示例程式碼:

using Microsoft.Extensions.DependencyInjection;
using Volo.Abp.DependencyInjection;
namespace UserManagement
{
    [Dependency(ServiceLifetime.Transient, TryRegister = true)]
    public class UserPermissionCache
    { }
}

4.介面屬性混合註冊

屬性介面一起使用。如果屬性定義了屬性,屬性比介面優先順序更高。

如果一個類可能被注入不同的類或介面,具體取決於暴露的型別。

暴露服務

當一個類沒有實現介面時,只能通過類引用注入。上一節中的UserPermissionCache類就是通過注入類引用來使用的。

假設我們有一個抽象 SMS 傳送的介面:

public interface ISmsService
{
    Task SendAsync(string phoneNumber, string message);
}

假設您要ISmsService實現 Azure 服務:

public class AzureSmsService : ISmsService, ITransientDependency
{
    public async Task SendAsync(string phoneNumber, string message)
    {
        //TODO: ...
    }
}

這裡的AzureSmsService實現了ISmsServiceITransientDependency兩個介面。而ITransientDependency介面才是用於自動註冊到依賴注入中的。這裡的注入主要通過命名約定來實現,因為AzureSmsServiceSmsService作為字尾結尾。
我們再舉一個通過命名約定的例子,假設我們有一個實現多個介面的類:

public class PdfExporter: IExporter, IPdfExporter, ICanExport, ITransientDependency
{ }

PdfExporter服務可以通過注入IPdfExporterIExporter介面來使用,也可以直接注入PdfExporter類引用來使用。但是,您不能使用ICanExport介面注入它,因為名稱PdfExporter不以CanExport為字尾。

一旦您使用該ExposeServices屬性來暴露服務,如以下程式碼塊所示:

[ExposeServices(typeof(IPdfExporter))]
public class PdfExporter: IExporter, IPdfExporter, ICanExport, ITransientDependency
{ }

現在,您只能通過注入IPdfExporter介面來使用PdfExporter類。

我應該為每個服務定義介面嗎?

ABP 不會強迫你這麼做,但是通用介面來定義是最佳實踐:如果你想鬆散地耦合你的服務。比如,在單元測試中可以輕鬆模擬測試資料。

這就是為什麼我們將介面與實現物理分離(例如,我們在專案中定義Application.Contracts介面,並在Application專案中實現它們,或者在領域層中定義儲存庫介面,在基礎設施層中實現它們)。

我們已經瞭解瞭如何註冊和消費服務。另外,某些服務具有選項配置,您需要在使用它們之前對其進行配置。接下來的兩節將展開介紹。

待續

文章有點長,下篇將繼續介紹ABP的配置和選項模式,感謝你的閱讀。

相關文章