目錄


建立空白方案 & ABP 框架模組介紹

這個系列會來逐步分解 ABP 背後的設計與各項模組背後的原理,建議做完 ABP 官方 Book Store 教學 後才繼續觀看。

這次我們將從空白方案開始一步一步以模組化的方式建立出各個層級專案,再次完成 10 回的教學,並且對比較細項的內容進行補充,基本上適用於任何 ABP 版本。

首先我們建立一個新的空的方案 BookStoreScratch.sln

dotnet new sln -o BookStoreScratch
cd BookStoreScratch
git init
dotnet new gitignore

ABP 是根據 DDD 的設計原則與參考 Clean Architecture 的架構分別設計出四個層級:

  1. Domain Layer(領域層): 包含整個應用程式的 Domain Objects(Business Objects) 以及 Business Rules
  2. Application Layer(應用層): 透過建立各種應用服務(Application Services)來操作或協調 Domain Objects 來實現具體的應用程式 Use Cases
  3. Presentation Layer(表現層): 負責用戶介面與應用程式的互動,由各種頁面或 UI 元件所組成。
  4. Infrastructure Layer (基礎設施層): 透過抽象的方式來提供其他層級使用基礎設施或第三方服務。

為了要讓我們程式碼符合 ABP 的規範,所以我們必須在每個層級中安裝對應的 ABP 框架模組,只要看到 ABP 專案名稱含有 DDD 就代表這個專案是與層級相關的框架模組, 目前這類型的專案總共有以下幾個:

  1. Volo.Abp.Ddd.Application
  2. Volo.Abp.Ddd.Application.Contracts
  3. Volo.Abp.Ddd.Domain
  4. Volo.Abp.Ddd.Domain.Shared
  5. Volo.Abp.Ddd.Tests

扣掉最後一個與測試相關的專案,所以其實只有四個是與層級相關的框架模組,其中有兩個專案是屬於更細項的層級框架模組:

  1. Volo.Abp.Ddd.Application.Contracts: 存放應用服務(Application Services)的 Interface 與相關的 Data Transfer Object(DTO)
  2. Volo.Abp.Ddd.Domain.Shared: 將 ConstEnum多語系 json 等等可以安全共享或者是跟層級無關的資料放在這一層。

仔細看原始碼會發現這兩個層級其實是可有可無的,因為 Volo.Abp.Ddd.ApplicationVolo.Abp.Ddd.Domain 會直接參考它們, 所以在建立專案的時候如果覺得不需要這兩個額外的層級是可以省略掉它們只保留兩個層級就夠了。

所有 ABP 常用的 Application Modules 應用模組,例如 IdentityAudit LoggingSetting 等應用模組的對應層級都有安裝這些 APB 定義的 DDD 的框架模組。

例如下面 Setting 應用模組就是將細項的層級分開並安裝對應的 DDD 層級框架模組: Volo.Abp.SettingManagement.Application 安裝 Volo.Abp.Ddd.Application Volo.Abp.SettingManagement.Application.Contracts 安裝 Volo.Abp.Ddd.Application.Contracts Volo.Abp.SettingManagement.Domain 安裝 Volo.Abp.Ddd.Domain Volo.Abp.SettingManagement.Domain.Shared 安裝 Volo.Abp.Ddd.Domain.Shared




BookStoreScratch 專案層級設計 & 安裝與調整相關依賴

接下來根據上一節的分層設計的建議,我們開始設計自己的 BookStoreScratch 應用模組, 本文章選擇了 Entity Framework Core 作為 Database Provider,UI 則是先跳過等待底層處理完後處理。

  1. Domain Layer(領域層):
    • BookStoreScratch.Domain
    • BookStoreScratch.Domain.Shared
  2. Application Layer(應用層):
    • BookStoreScratch.Application
    • BookStoreScratch.Application.Contracts
  3. Infrastructure Layer(基礎設施層):
    • BookStoreScratch.EntityFrameworkCore

ABP 在處理資料庫存取有三種方式,它們各自都有一個主要的基底模組:

  1. Entity Framework Core: Volo.Abp.EntityFrameworkCore
  2. MongoDB: Volo.Abp.MongoDB
  3. Dapper: Volo.Abp.Dapper

其中最重要的就是 Entity Framework Core,ABP 將具體的資料庫實做進行分離,等需要決定具體該用哪個資料庫時再來安裝即可, ABP 官方提供以下幾種資料庫 package:

  1. Volo.Abp.EntityFrameworkCore.MySQL
  2. Volo.Abp.EntityFrameworkCore.Oracle
  3. Volo.Abp.EntityFrameworkCore.Oracle.Devart
  4. Volo.Abp.EntityFrameworkCore.PostgreSql
  5. Volo.Abp.EntityFrameworkCore.Sqlite
  6. Volo.Abp.EntityFrameworkCore.SqlServer

這裡要注意如果你是做為一個模組開發者,那麼應該把資料庫的選擇權交給模組的使用者,所以這裡直接安裝 Volo.Abp.EntityFrameworkCore 就足夠了, 避免依賴到具體實做,影響使用我們模組的人。

還有 Volo.Abp.Dapper 底層其實也是依賴 Volo.Abp.EntityFrameworkCore,這是因為 ABP 官方事建議混用兩種模式,你需要高性能查詢可以選擇使用 Dapper,其他情況使用 EFCore 即可,所以 Volo.Abp.Dapper 封裝了幾個方便的方法,讓我們能從 DbContext 直接獲取到 IDbConnection 並發出 SQL 語句。

接下來根據設計後建立以下專案。

mkdir src
dotnet new classlib -o src/BookStoreScratch.Domain
dotnet new classlib -o src/BookStoreScratch.Domain.Shared
dotnet new classlib -o src/BookStoreScratch.Application
dotnet new classlib -o src/BookStoreScratch.Application.Contracts
dotnet new classlib -o src/BookStoreScratch.EntityFrameworkCore

dotnet sln add src/BookStoreScratch.Domain
dotnet sln add src/BookStoreScratch.Domain.Shared
dotnet sln add src/BookStoreScratch.Application
dotnet sln add src/BookStoreScratch.Application.Contracts
dotnet sln add src/BookStoreScratch.EntityFrameworkCore

rm src/BookStoreScratch.Domain/Class1.cs
rm src/BookStoreScratch.Domain.Shared/Class1.cs
rm src/BookStoreScratch.Application/Class1.cs
rm src/BookStoreScratch.Application.Contracts/Class1.cs
rm src/BookStoreScratch.EntityFrameworkCore/Class1.cs

調整所有專案的 RootNamespace 與刪除 ImplicitUsings

<PropertyGroup>
  <TargetFramework>net9.0</TargetFramework>
  <Nullable>enable</Nullable>
  <RootNamespace/>
</PropertyGroup>

如果有需要支援舊版本的程式建議是調整 Application.ContractsDomainDomain.Shared 這些與具體實做沒關係的專案的支援 netstandard

<TargetFrameworks>netstandard2.0;netstandard2.1;net9.0</TargetFrameworks>

安裝必要的 ABP Package。

dotnet add src/BookStoreScratch.Application package Volo.Abp.Ddd.Application
dotnet add src/BookStoreScratch.Application.Contracts package Volo.Abp.Ddd.Application.Contracts
dotnet add src/BookStoreScratch.Domain package Volo.Abp.Ddd.Domain
dotnet add src/BookStoreScratch.Domain.Shared package Volo.Abp.Ddd.Domain.Shared
dotnet add src/BookStoreScratch.EntityFrameworkCore package Volo.Abp.EntityFrameworkCore

下一步就是建立各個專案之間的依賴關係,這裡根據目前整理出來的訊息列出以下幾的重點。

  1. Domain.Shared 層級是用來放置跟層級無關的共享資料,所以每個層級都須要有這個 Domain.Shared 層級參考。
  2. Application.Contracts 層級存放 Application Services 的 InterfaceDTO,所以代表 Application 層級需要參考 Application.Contracts 層級才有辦法建立應用服務。
  3. Application 層級則是負責建立各種應用服務來操作 Domain Objects,所以代表 Application 層級還需要參考 Domain 層級才有辦法建立應用服務。
  4. Domain 層級包含整個應用程式的 Domain Objects 以及 Business Rules
  5. EntityFrameworkCore 層級由於 Domain Objects 是保存在 Domain 層級,所以 EntityFrameworkCore 層級需要參考 Domain 層級,才能透過資料庫建立對應的 .NET 物件。

由上面的重點得知 Domain.Shared 層必須要在最底層給所有層級參考,Application 層必須同時參考 Application.ContractsDomain 才有辦法建立應用服務, Domain 層目前沒有特殊要求,EntityFrameworkCore 層級需要參考 Domain 層級,有這寫資訊後就可以來建立專案關聯了。

dotnet add src/BookStoreScratch.Domain reference src/BookStoreScratch.Domain.Shared
dotnet add src/BookStoreScratch.Application.Contracts reference src/BookStoreScratch.Domain.Shared
dotnet add src/BookStoreScratch.Application reference src/BookStoreScratch.Application.Contracts
dotnet add src/BookStoreScratch.Application reference src/BookStoreScratch.Domain
dotnet add src/BookStoreScratch.EntityFrameworkCore reference src/BookStoreScratch.Domain




建立 AbpModule

接下來根據 ABP 的規範建立每一個層級的 Module 檔案,要注意由於目前的 RootNamespace 已經被我們調整過, 所以現在命名空間是參考資料夾結構來命名的,另外 ABP 官方建議要使用 CompanyName.ModuleName 的格式來命名模組, 例如 Volo.Abp.SettingManagement 就是代表 Volo 公司 ABP 專案的 SettingManagement 模組。

這篇文章就省略掉了 CompanyName 直接用 ModuleName 來進行命名,也就是說我們的每個專案都要在建立一個 BookStoreScratch 資料夾來 辨別命名空間,並且之後開發的程式都要在這個資料夾底下命名空間才會正確。

EntityFrameworkCore 層級額外建立了一個 EntityFrameworkCore 資料夾來避免污染命名空間。

mkdir -p src/BookStoreScratch.Application/BookStoreScratch && touch "$_"/BookStoreScratchApplicationModule.cs
mkdir -p src/BookStoreScratch.Application.Contracts/BookStoreScratch && touch "$_"/BookStoreScratchApplicationContractsModule.cs
mkdir -p src/BookStoreScratch.Domain/BookStoreScratch && touch "$_"/BookStoreScratchDomainModule.cs
mkdir -p src/BookStoreScratch.Domain.Shared/BookStoreScratch && touch "$_"/BookStoreScratchDomainSharedModule.cs
mkdir -p src/BookStoreScratch.EntityFrameworkCore/BookStoreScratch/EntityFrameworkCore && touch "$_"/BookStoreScratchEntityFrameworkCoreModule.cs

建立完成後就能開始撰寫 ABP 程式了,首先 ABP 底層是透過 ModuleManager 來管理所有 Module 的生命週期,一個模組會經歷以下過程

  1. OnPreApplicationInitialization: 初始化前執行
  2. OnApplicationInitialization: 初始化時執行
  3. OnPostApplicationInitialization: 初始化後執行
  4. OnApplicationShutdown: 結束應用程式時執行

ModuleManager 會使用 foreach 的方法來執行每個有實做 IAbpModule 介面的模組內的 OnApplicationInitializationAsync 方法, 來初始化應用程式,其它的生命週期也是同樣的執行流程,下面是相關的原始碼。

public virtual async Task InitializeModulesAsync(ApplicationInitializationContext context)
{
    foreach (var contributor in _lifecycleContributors)
    {
        foreach (var module in _moduleContainer.Modules)
        {
            try
            {
                await contributor.InitializeAsync(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.");
}

也就是說我們現在需要確保剛剛建立的所有 BookStoreScratch Module 都有實做 IAbpModule 介面才能將我們的模組接入到 ABP 系統內, 這一點我們直接繼承 AbpModule 就能轉換成 ABP 模組了,但是這樣還不夠,因為模組有層層依賴的關係,所以需要有一個機制負責處理我們模組內部依賴的底層模組才能確保我們的程式能正常運作, ABP 則是透過 DependsOn Attribute 來自動註冊底層模組,設定後 ABP 會自動處理模組之間的載入優先順序。

接下來就開始處理所有 BookStoreScratch Module。

展開模組詳細設定
// BookStoreScratchDomainSharedModule.cs
using Volo.Abp.Modularity;

namespace BookStoreScratch;

public class BookStoreScratchDomainSharedModule : AbpModule
{
}
// BookStoreScratchDomainModule.cs
using Volo.Abp.Domain;
using Volo.Abp.Modularity;

namespace BookStoreScratch;

[DependsOn(typeof(AbpDddDomainModule))]
[DependsOn(typeof(BookStoreScratchDomainSharedModule))]
public class BookStoreScratchDomainModule : AbpModule
{
}
// BookStoreScratchApplicationContractsModule.cs
using Volo.Abp.Application;
using Volo.Abp.Modularity;

namespace BookStoreScratch;

[DependsOn(typeof(AbpDddApplicationContractsModule))]
[DependsOn(typeof(BookStoreScratchDomainSharedModule))]
public class BookStoreScratchApplicationContractsModule : AbpModule
{
}
// BookStoreScratchApplicationModule.cs
using Volo.Abp.Application;
using Volo.Abp.Modularity;

namespace BookStoreScratch;

[DependsOn(typeof(AbpDddApplicationModule))]
[DependsOn(
    typeof(BookStoreScratchApplicationContractsModule),
    typeof(BookStoreScratchDomainModule)
)]
public class BookStoreScratchApplicationModule : AbpModule
{
}
// BookStoreScratchEntityFrameworkCoreModule.cs
using Volo.Abp.EntityFrameworkCore;
using Volo.Abp.Modularity;

namespace BookStoreScratch.EntityFrameworkCore;

[DependsOn(typeof(AbpEntityFrameworkCoreModule))]
[DependsOn(typeof(BookStoreScratchDomainModule))]
public class BookStoreScratchEntityFrameworkCoreModule : AbpModule
{
}




測試 Module & 生命週期

專案雛型到這裡就完成了,接下來可以來看看 ABP 是怎麼加載模組的。

首先建立一個 Console 專案並安裝 Volo.Abp.Core,這個 Package 是 APB 最核心的專案,所有最基礎的功能都會放在這個專案裡面, 例如之前提到負責管理模組加載的 ModuleManager 以及 DependsOn Attribute,另外安裝 Serilog 紀錄日誌。

mkdir app
dotnet new worker -o app/BookStoreScratch.Console
dotnet sln add app/BookStoreScratch.Console
rm app/BookStoreScratch.Console/Worker.cs

dotnet add app/BookStoreScratch.Console package Volo.Abp.Core
dotnet add app/BookStoreScratch.Console package Serilog.Extensions.Hosting
dotnet add app/BookStoreScratch.Console package Serilog.Extensions.Logging
dotnet add app/BookStoreScratch.Console package Serilog.Sinks.Async
dotnet add app/BookStoreScratch.Console package Serilog.Sinks.Console

同樣也是建立 BookStoreScratchConsoleModule 以及依賴 AbpModule

mkdir -p app/BookStoreScratch.Console && touch "$_"/BookStoreScratchConsoleModule.cs
using Volo.Abp.Modularity;

namespace BookStoreScratch.Console;

public class BookStoreScratchConsoleModule : AbpModule
{
}

最後是調整 Program.cs 的內容。

using System;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Serilog;
using Serilog.Events;

namespace BookStoreScratch.Console;

public class Program
{
    public static async Task<int> Main(string[] args)
    {
        Log.Logger = new LoggerConfiguration()
#if DEBUG
            .MinimumLevel.Debug()
#else
            .MinimumLevel.Information()
#endif
            .MinimumLevel.Override("Microsoft", LogEventLevel.Information)
            .Enrich.FromLogContext()
            .WriteTo.Async(c => c.Console())
            .CreateLogger();

        try
        {
            Log.Information("Starting console host.");

            var builder = Host.CreateApplicationBuilder(args);

            builder.Logging.ClearProviders().AddSerilog();

            await builder.Services.AddApplicationAsync<BookStoreScratchConsoleModule>();

            var host = builder.Build();

            await host.InitializeAsync();

            await host.RunAsync();

            return 0;
        }
        catch (Exception ex)
        {
            if (ex is HostAbortedException)
            {
                throw;
            }

            Log.Fatal(ex, "Host terminated unexpectedly!");
            return 1;
        }
        finally
        {
            Log.CloseAndFlush();
        }
    }
}

重要的內容只有 AddApplicationAsync 這一行,他是讓一般的 .Net 應用程式變成 ABP 應用程式的關鍵, 我們在文章一開始有提到 ABP 提供了四種生命週期,實際上還有三種跟 Dependency Injection 有關的生命週期,所以總共有以下幾種生命週期:

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

所以你會發現 ABP 的註冊方法跟 Startup.cs 的寫法其實是差不多的, 在寫一般的 .Net 時我們會在這個 Startup.cs 檔案建立兩個方法 ConfigureServices 用來註冊服務到 DI 容器中, Configure 用來設定設定 Middleware。

所以你可以把 Startup.csConfigureServices 對應成 abp 中的 ConfigureServicesConfigure 對應成 abp 中的 OnApplicationInitialization 方法。

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

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

運行專案後會看到終端輸出以下內容:

[19:40:15 INF] Starting console host.
[19:40:15 INF] Loaded ABP modules:
[19:40:15 INF] - BookStoreScratch.Console.BookStoreScratchConsoleModule
[19:40:15 INF] Initialized all ABP modules.
[19:40:15 INF] Application started. Press Ctrl+C to shut down.

如果有看到 Loaded ABP modules 這一行內容就代表成功了,ABP 會自動處理模組的加載順序,由於我們目前只有添加一個 BookStoreScratchConsoleModule 並且沒有其他依賴,所以下面的內容就只有載入這個模組而已。

接下來可以試試看把我們之前寫的層級模組的參考加入進來,要注意這種寫法只適合測試用,實做上還需要另外分層。

dotnet add app/BookStoreScratch.Console reference src/BookStoreScratch.Domain.Shared
dotnet add app/BookStoreScratch.Console reference src/BookStoreScratch.Application.Contracts
dotnet add app/BookStoreScratch.Console reference src/BookStoreScratch.Application
dotnet add app/BookStoreScratch.Console reference src/BookStoreScratch.Domain
dotnet add app/BookStoreScratch.Console reference src/BookStoreScratch.EntityFrameworkCore

添加完參考後也要記得在 DependsOn 添加需要依賴的模組。

[DependsOn(
    typeof(BookStoreScratchApplicationModule),
    typeof(BookStoreScratchApplicationContractsModule),
    typeof(BookStoreScratchDomainModule),
    typeof(BookStoreScratchDomainSharedModule),
    typeof(BookStoreScratchEntityFrameworkCoreModule)
)]
public class BookStoreScratchConsoleModule : AbpModule
{
}

完成後直接運行會看到加載了非常多的模組,這些大部分是 ABP 的框架模組,每個框架模組是按照功能來區分並建立出對應的模組。

[19:53:18 INF] Starting console host.
[19:53:18 INF] Loaded ABP modules:
[19:53:18 INF] - BookStoreScratch.Console.BookStoreScratchConsoleModule
[19:53:18 INF]   - BookStoreScratch.BookStoreScratchApplicationModule
[19:53:18 INF]     - Volo.Abp.Application.AbpDddApplicationModule
[19:53:18 INF]       - Volo.Abp.Domain.AbpDddDomainModule
[19:53:18 INF]         - Volo.Abp.Auditing.AbpAuditingModule
[19:53:18 INF]           - Volo.Abp.Data.AbpDataModule
[19:53:18 INF]             - Volo.Abp.ObjectExtending.AbpObjectExtendingModule
[19:53:18 INF]               - Volo.Abp.Localization.AbpLocalizationAbstractionsModule
[19:53:18 INF]               - Volo.Abp.Validation.AbpValidationAbstractionsModule
[19:53:18 INF]             - Volo.Abp.Uow.AbpUnitOfWorkModule
[19:53:18 INF]             - Volo.Abp.EventBus.Abstractions.AbpEventBusAbstractionsModule
[19:53:18 INF]           - Volo.Abp.Json.AbpJsonModule
[19:53:18 INF]             - Volo.Abp.Json.SystemTextJson.AbpJsonSystemTextJsonModule
[19:53:18 INF]               - Volo.Abp.Json.AbpJsonAbstractionsModule
[19:53:18 INF]               - Volo.Abp.Timing.AbpTimingModule
[19:53:18 INF]                 - Volo.Abp.Localization.AbpLocalizationModule
[19:53:18 INF]                   - Volo.Abp.VirtualFileSystem.AbpVirtualFileSystemModule
[19:53:18 INF]                   - Volo.Abp.Settings.AbpSettingsModule
[19:53:18 INF]                     - Volo.Abp.Security.AbpSecurityModule
[19:53:18 INF]                   - Volo.Abp.Threading.AbpThreadingModule
[19:53:18 INF]           - Volo.Abp.MultiTenancy.AbpMultiTenancyModule
[19:53:18 INF]             - Volo.Abp.MultiTenancy.AbpMultiTenancyAbstractionsModule
[19:53:18 INF]           - Volo.Abp.Auditing.AbpAuditingContractsModule
[19:53:18 INF]         - Volo.Abp.EventBus.AbpEventBusModule
[19:53:18 INF]           - Volo.Abp.Guids.AbpGuidsModule
[19:53:18 INF]           - Volo.Abp.BackgroundWorkers.AbpBackgroundWorkersModule
[19:53:18 INF]           - Volo.Abp.DistributedLocking.AbpDistributedLockingAbstractionsModule
[19:53:18 INF]         - Volo.Abp.ObjectMapping.AbpObjectMappingModule
[19:53:18 INF]         - Volo.Abp.ExceptionHandling.AbpExceptionHandlingModule
[19:53:18 INF]         - Volo.Abp.Specifications.AbpSpecificationsModule
[19:53:18 INF]         - Volo.Abp.Caching.AbpCachingModule
[19:53:18 INF]           - Volo.Abp.Serialization.AbpSerializationModule
[19:53:18 INF]         - Volo.Abp.Domain.AbpDddDomainSharedModule
[19:53:18 INF]       - Volo.Abp.Application.AbpDddApplicationContractsModule
[19:53:18 INF]       - Volo.Abp.Validation.AbpValidationModule
[19:53:18 INF]       - Volo.Abp.Authorization.AbpAuthorizationModule
[19:53:18 INF]         - Volo.Abp.Authorization.AbpAuthorizationAbstractionsModule
[19:53:18 INF]       - Volo.Abp.Http.AbpHttpAbstractionsModule
[19:53:18 INF]       - Volo.Abp.Features.AbpFeaturesModule
[19:53:18 INF]       - Volo.Abp.GlobalFeatures.AbpGlobalFeaturesModule
[19:53:18 INF]     - BookStoreScratch.BookStoreScratchApplicationContractsModule
[19:53:18 INF]       - BookStoreScratch.BookStoreScratchDomainSharedModule
[19:53:18 INF]     - BookStoreScratch.BookStoreScratchDomainModule
[19:53:18 INF]   - BookStoreScratch.EntityFrameworkCore.BookStoreScratchEntityFrameworkCoreModule
[19:53:18 INF]     - Volo.Abp.EntityFrameworkCore.AbpEntityFrameworkCoreModule
[19:53:18 INF] Initialized all ABP modules.
[19:53:18 INF] Application started. Press Ctrl+C to shut down.
[19:53:18 INF] Hosting environment: Production

如果到目前都還理解的話,應該早就發現 DependsOn 其實不用添加這麼多模組,例如 Application 層級早就在他自己的 BookStoreScratchApplicationModule 添加依賴的模組了,所以這裡其實不用再次添加,不過添加也沒關係 ABP 也會自動處理 底層的模組依賴,所以把程式碼改成下面這樣並運行,你會發現輸出一模一樣的結果。

[DependsOn(
    typeof(BookStoreScratchApplicationModule),
    typeof(BookStoreScratchEntityFrameworkCoreModule)
)]
public class BookStoreScratchConsoleModule : AbpModule
{
}

本次文章進度 Github