目錄
建立空白方案 & 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
的架構分別設計出四個層級:
- Domain Layer(領域層): 包含整個應用程式的
Domain Objects(Business Objects)
以及Business Rules
。 - Application Layer(應用層): 透過建立各種應用服務(Application Services)來操作或協調
Domain Objects
來實現具體的應用程式Use Cases
。 - Presentation Layer(表現層): 負責用戶介面與應用程式的互動,由各種頁面或 UI 元件所組成。
- Infrastructure Layer (基礎設施層): 透過抽象的方式來提供其他層級使用基礎設施或第三方服務。
為了要讓我們程式碼符合 ABP 的規範,所以我們必須在每個層級中安裝對應的 ABP 框架模組,只要看到 ABP 專案名稱含有 DDD 就代表這個專案是與層級相關的框架模組, 目前這類型的專案總共有以下幾個:
Volo.Abp.Ddd.Application
Volo.Abp.Ddd.Application.Contracts
Volo.Abp.Ddd.Domain
Volo.Abp.Ddd.Domain.Shared
Volo.Abp.Ddd.Tests
扣掉最後一個與測試相關的專案,所以其實只有四個是與層級相關的框架模組,其中有兩個專案是屬於更細項的層級框架模組:
Volo.Abp.Ddd.Application.Contracts
: 存放應用服務(Application Services)的Interface
與相關的Data Transfer Object(DTO)
。Volo.Abp.Ddd.Domain.Shared
: 將Const
、Enum
、多語系 json
等等可以安全共享或者是跟層級無關的資料放在這一層。
仔細看原始碼會發現這兩個層級其實是可有可無的,因為 Volo.Abp.Ddd.Application
與 Volo.Abp.Ddd.Domain
會直接參考它們,
所以在建立專案的時候如果覺得不需要這兩個額外的層級是可以省略掉它們只保留兩個層級就夠了。
所有 ABP 常用的 Application Modules 應用模組,例如 Identity
、
Audit Logging
、Setting
等應用模組的對應層級都有安裝這些 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 則是先跳過等待底層處理完後處理。
- Domain Layer(領域層):
- BookStoreScratch.Domain
- BookStoreScratch.Domain.Shared
- Application Layer(應用層):
- BookStoreScratch.Application
- BookStoreScratch.Application.Contracts
- Infrastructure Layer(基礎設施層):
- BookStoreScratch.EntityFrameworkCore
ABP 在處理資料庫存取有三種方式,它們各自都有一個主要的基底模組:
- Entity Framework Core:
Volo.Abp.EntityFrameworkCore
- MongoDB:
Volo.Abp.MongoDB
- Dapper:
Volo.Abp.Dapper
其中最重要的就是 Entity Framework Core
,ABP 將具體的資料庫實做進行分離,等需要決定具體該用哪個資料庫時再來安裝即可,
ABP 官方提供以下幾種資料庫 package:
Volo.Abp.EntityFrameworkCore.MySQL
Volo.Abp.EntityFrameworkCore.Oracle
Volo.Abp.EntityFrameworkCore.Oracle.Devart
Volo.Abp.EntityFrameworkCore.PostgreSql
Volo.Abp.EntityFrameworkCore.Sqlite
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.Contracts
、Domain
、Domain.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
下一步就是建立各個專案之間的依賴關係,這裡根據目前整理出來的訊息列出以下幾的重點。
Domain.Shared
層級是用來放置跟層級無關的共享資料,所以每個層級都須要有這個Domain.Shared
層級參考。Application.Contracts
層級存放 Application Services 的Interface
與DTO
,所以代表Application
層級需要參考Application.Contracts
層級才有辦法建立應用服務。Application
層級則是負責建立各種應用服務來操作Domain Objects
,所以代表Application
層級還需要參考Domain
層級才有辦法建立應用服務。Domain
層級包含整個應用程式的Domain Objects
以及Business Rules
。EntityFrameworkCore
層級由於Domain Objects
是保存在Domain
層級,所以EntityFrameworkCore
層級需要參考Domain
層級,才能透過資料庫建立對應的 .NET 物件。
由上面的重點得知 Domain.Shared
層必須要在最底層給所有層級參考,Application
層必須同時參考 Application.Contracts
與 Domain
才有辦法建立應用服務,
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 的生命週期,一個模組會經歷以下過程
OnPreApplicationInitialization
: 初始化前執行OnApplicationInitialization
: 初始化時執行OnPostApplicationInitialization
: 初始化後執行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 有關的生命週期,所以總共有以下幾種生命週期:
PreConfigureServices
ConfigureServices
PostConfigureServices
OnPreApplicationInitialization
OnApplicationInitialization
OnPostApplicationInitialization
OnApplicationShutdown
所以你會發現 ABP 的註冊方法跟 Startup.cs
的寫法其實是差不多的,
在寫一般的 .Net 時我們會在這個 Startup.cs
檔案建立兩個方法 ConfigureServices
用來註冊服務到 DI 容器中,
Configure
用來設定設定 Middleware。
所以你可以把 Startup.cs
的 ConfigureServices
對應成 abp 中的 ConfigureServices
,
Configure
對應成 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