ABP IO 軟體框架教學 Part 3 - 從頭開始建立新模組 - abp 生命週期與 Host 層 ABP IO 軟體框架教學 Part 3 - 從頭開始建立新模組 - abp 生命週期與 Host 層

Published on Thursday, July 6, 2023

Abp Lifecycle

目前已經完成了模組的基本架構,接下來可以準備將我們建立的模組運行起來,與一般的流程不同 abp 有自訂一套生命週期, 我們需要先學習一下 abp 生命週期才能繼續進行我們的專案。

在新的 Dotnet Api 專案中我們知道專案都是從 Program.cs 的 Program.Main 作為啟動點,之後需要建立一個 HostBuilder 並註冊需要的服務到 DI 容器中, 完成後呼叫 Build 方法建立出 Host 實體並根據順序註冊 Middleware 最後呼叫 Run 方法將 Host 運行起來。

var builder = WebApplication.CreateBuilder(args);
// Add services to the container.

builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();

app.UseAuthorization();

app.MapControllers();

app.Run();

從上面的程式碼中可以得知關鍵的流程為:

  1. var builder = WebApplication.CreateBuilder(args);
  2. var app = builder.Build();
  3. app.Run();

有學過 net 6.0 以前的寫法的人應該會記得 Startup.cs 這個檔案,我們會在這個檔案建立兩個方法 ConfigureServices 用來註冊服務到 DI 容器中, Configure 用來設定設定 Middleware,現在新的模板已經省略 Startup.cs 並全部整合到 Program.cs 中。

在 abp 中有自己的設定方法分別為 AddApplicationAsyncInitializeApplicationAsync 內部會搭配以下幾種生命週期:

  1. PreConfigureServices
  2. ConfigureServices
  3. PostConfigureServices
  4. OnPreApplicationInitialization
  5. OnApplicationInitialization
  6. OnPostApplicationInitialization
  7. OnApplicationShutdown

首先上面提到的生命週期其實我們已經使用很多次了,只要是繼承 AbpModule 的各個層級 Module 中都有使用到,一般情況下只會用到 ConfigureServicesOnApplicationInitialization 這兩個方法,你可以把 Startup.cs 的 ConfigureServices 對應成 abp 中的 ConfigureServicesConfigure 對應成 abp 中的 OnApplicationInitialization 方法。

其他方法可以從名稱中知道他的用途 PreConfigureServices 代表會在 ConfigureServices 運行之前優先執行, PostConfigureServices 代表會在 ConfigureServices 完成後運行。

因此當我們建立一個新模組時需要繼承 AbpModule 如果需要註冊服務到 DI 容器內可以 override ConfigureServices 方法來註冊服務, 如果模組需要使用新的 Middleware 可以 override OnApplicationInitialization 方法來註冊新的 Middleware。

我們可以再次看看上一篇文章的 BookStoreScratchApplicationModule 在這裡我們就有使用到 ConfigureServices 將建立的 Automapper 設定 添加到 DI 容器之中,這個模組因為不需要另外使用 Middleware 因此可以直接跳過 OnApplicationInitialization 的設定。

[DependsOn(
    typeof(AbpDddApplicationModule),
    typeof(AbpAutoMapperModule)
)]
[DependsOn(
    typeof(BookStoreScratchApplicationContractsModule),
    typeof(BookStoreScratchDomainModule)
)]
public class BookStoreScratchApplicationModule : AbpModule
{
    public override void ConfigureServices(ServiceConfigurationContext context)
    {
        context.Services.AddAutoMapperObjectMapper<BookStoreScratchApplicationModule>();
        Configure<AbpAutoMapperOptions>(options =>
        {
            options.AddMaps<BookStoreScratchApplicationModule>();
        });
    }
}

從上面模組中你會發現一個問題,那就是該如何確保註冊 AutoMapper 服務完成後才註冊 AutoMapperProfile,否則順序不對會導致 錯誤的產生,這時候就該提到上面說到的 AddApplicationAsyncInitializeApplicationAsync 方法了,我們需要使用 AddApplicationAsync 方法確保模組會優先建立 DependsOn 所需要的模組,保證底層的模組都加載完成才加載高階的模組。

詳細的內容可以參考 AbpApplicationBase.cs 的原始碼,有更詳細的流程

public virtual async Task ConfigureServicesAsync()
{
    CheckMultipleConfigureServices();

    var context = new ServiceConfigurationContext(Services);
    Services.AddSingleton(context);

    foreach (var module in Modules)
    {
        if (module.Instance is AbpModule abpModule)
        {
            abpModule.ServiceConfigurationContext = context;
        }
    }

    //PreConfigureServices
    foreach (var module in Modules.Where(m => m.Instance is IPreConfigureServices))
    {
        try
        {
            await ((IPreConfigureServices)module.Instance).PreConfigureServicesAsync(context);
        }
        catch (Exception ex)
        {
            throw new AbpInitializationException($"An error occurred during {nameof(IPreConfigureServices.PreConfigureServicesAsync)} phase of the module {module.Type.AssemblyQualifiedName}. See the inner exception for details.", ex);
        }
    }

    var assemblies = new HashSet<Assembly>();

    //ConfigureServices
    foreach (var module in Modules)
    {
        if (module.Instance is AbpModule abpModule)
        {
            if (!abpModule.SkipAutoServiceRegistration)
            {
                var assembly = module.Type.Assembly;
                if (!assemblies.Contains(assembly))
                {
                    Services.AddAssembly(assembly);
                    assemblies.Add(assembly);
                }
            }
        }

        try
        {
            await module.Instance.ConfigureServicesAsync(context);
        }
        catch (Exception ex)
        {
            throw new AbpInitializationException($"An error occurred during {nameof(IAbpModule.ConfigureServicesAsync)} phase of the module {module.Type.AssemblyQualifiedName}. See the inner exception for details.", ex);
        }
    }

    //PostConfigureServices
    foreach (var module in Modules.Where(m => m.Instance is IPostConfigureServices))
    {
        try
        {
            await ((IPostConfigureServices)module.Instance).PostConfigureServicesAsync(context);
        }
        catch (Exception ex)
        {
            throw new AbpInitializationException($"An error occurred during {nameof(IPostConfigureServices.PostConfigureServicesAsync)} phase of the module {module.Type.AssemblyQualifiedName}. See the inner exception for details.", ex);
        }
    }

    foreach (var module in Modules)
    {
        if (module.Instance is AbpModule abpModule)
        {
            abpModule.ServiceConfigurationContext = null;
        }
    }

    _configuredServices = true;

    TryToSetEnvironment(Services);
}

另外 InitializeApplicationAsync 註冊邏輯也是一樣的,不同的是會透過各種 LifecycleContributor 來確保各個生命週期的程式能夠正確執行

// InternalServiceCollectionExtensions.cs
services.Configure<AbpModuleLifecycleOptions>(options =>
{
    options.Contributors.Add<OnPreApplicationInitializationModuleLifecycleContributor>();
    options.Contributors.Add<OnApplicationInitializationModuleLifecycleContributor>();
    options.Contributors.Add<OnPostApplicationInitializationModuleLifecycleContributor>();
    options.Contributors.Add<OnApplicationShutdownModuleLifecycleContributor>();
});
        
//ModuleManager.cs
public void InitializeModules(ApplicationInitializationContext context)
{
    foreach (var contributor in _lifecycleContributors)
    {
        foreach (var module in _moduleContainer.Modules)
        {
            try
            {
                contributor.Initialize(context, module.Instance);
            }
            catch (Exception ex)
            {
                throw new AbpInitializationException($"An error occurred during the initialize {contributor.GetType().FullName} phase of the module {module.Type.AssemblyQualifiedName}: {ex.Message}. See the inner exception for details.", ex);
            }
        }
    }

    _logger.LogInformation("Initialized all ABP modules.");
}

Host Layer

從上一段的內容中我們可以將 Host 專案的 Program.cs 進行改寫以符合 abp 需要的格式,另外建議是安裝 Autofac 來取代 Microsoft 預設的 DI 容器, 因為 abp 一些的功能是需要透過 Autofac 來實踐的因此官方預設樣板也是直接使用 Autofac,還有安裝 Abp 打包的 SqlServer EFCore 模組。

dotnet add package Volo.Abp.Autofac --version 7.2.2
dotnet add package Volo.Abp.EntityFrameworkCore.SqlServer --version 7.2.2

我們將 Program.cs 內非必要的內容先移除只留下必要的功能,接下來的目標是把剛剛移除的內容移動到 BookStoreScratchHttpApiHostModule 裡面

using BookStoreScratch;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

var builder = WebApplication.CreateBuilder(args);
await builder.AddApplicationAsync<BookStoreScratchHttpApiHostModule>();
builder.Host.UseAutofac();
var app = builder.Build();
await app.InitializeApplicationAsync();
await app.RunAsync();

BookStoreScratchHttpApiHostModule 因為 HttpApi 模組底層有依賴 AbpAspNetCoreMvcModule 因此 Host 模組可以省略許多設定,並且內部已經 呼叫了 AddMvc 等於是我們專案自動會添加 Controllers 與 Razor 相關功能,另外 UseConfiguredEndpoints 方法是 abp 自己定義的內部會去呼叫 常用的 app.UseEndpoints,要記得使用 UseEndpoints 方法之前需要先呼叫 UseRouting 方法。

using BookStoreScratch.EntityFrameworkCore;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.OpenApi.Models;
using Volo.Abp;
using Volo.Abp.Autofac;
using Volo.Abp.EntityFrameworkCore;
using Volo.Abp.EntityFrameworkCore.SqlServer;
using Volo.Abp.Modularity;

namespace BookStoreScratch;

[DependsOn(
    typeof(BookStoreScratchApplicationModule),
    typeof(BookStoreScratchEntityFrameworkCoreModule),
    typeof(BookStoreScratchHttpApiModule),
    typeof(AbpEntityFrameworkCoreSqlServerModule),
    typeof(AbpAutofacModule)
)]
public class BookStoreScratchHttpApiHostModule : AbpModule
{
    public override void ConfigureServices(ServiceConfigurationContext context)
    {
        base.ConfigureServices(context);

        Configure<AbpDbContextOptions>(options =>
        {
            options.UseSqlServer();
        });

        context.Services.AddSwaggerGen(
            options =>
            {
                options.SwaggerDoc("v1", new OpenApiInfo { Title = "BookStoreScratch API", Version = "v1" });
                options.DocInclusionPredicate((docName, description) => true);
                options.CustomSchemaIds(type => type.FullName);
            });
    }

    public override void OnApplicationInitialization(ApplicationInitializationContext context)
    {
        var app = context.GetApplicationBuilder();

        app.UseSwagger();
        app.UseSwaggerUI();
        
        app.UseHttpsRedirection();
        
        app.UseRouting();
        app.UseConfiguredEndpoints();
    }
}

設定完成後將專案運行起來瀏覽器會開啟 swagger 網址可以看到我們的 Book Curd 成功顯示

  • GET /api/book/
  • PUT /api/book/
  • DELETE /api/book/
  • GET /api/book
  • POST /api/book

先使用 POST /api/book 新增一筆書籍到資料庫內

{
  "name": "mybook",
  "type": 1,
  "publishDate": "2023-07-06",
  "price": 10
}

成功後會根據之前定義的 BookDto 回傳資料,我們可以拿回傳的 id 來進行查詢

{
  "name": "mybook",
  "type": 1,
  "publishDate": "2023-07-06T00:00:00",
  "price": 10,
  "id": "767085e2-a2c3-d3f5-c37b-3a0c42d2f44e"
}

使用 Get 方法可以正常回傳資料 https://localhost:7225/api/book/767085e2-a2c3-d3f5-c37b-3a0c42d2f44e

{
  "name": "mybook",
  "type": 1,
  "publishDate": "2023-07-06T00:00:00",
  "price": 10,
  "id": "767085e2-a2c3-d3f5-c37b-3a0c42d2f44e"
}

Summary

今天成功完成了 Host 專案,這種開發方式可以將各種功能獨立在自己的模組方案之中,並且可以在自己的模組中使用 Host 專案進行測試以避免 多個模組互相產生衝突,以上便是最基礎的模組開發方式正常情況我們可以直接使用 CLI 直接產生模組方案,但是預設產生的模組樣板會添加許多 不需要的模組到專案之內,所以重零開始建立自己的模組可以更清楚了解 abp 的運作原則。