第三章:實戰應用程式開發流程
3.1 引言:標準化的開發節奏
在 ABP Framework 中,開發一個新功能通常遵循一個標準的節奏。這個節奏確保了程式碼的可維護性、可測試性以及與 DDD 原則的一致性。
標準開發流程 (The ABP Way):
本章將以 「圖書借閱 (Book Borrowing)」 功能為例,帶您走過這六個步驟。
3.2 步驟一:需求分析與領域建模
需求:
使用者可以借閱一本書。如果書已經被借出,則不能再借。借閱成功後,書籍狀態應變更為「已借出」。
領域層設計 (Domain Layer)
在 BookStore.Domain 專案中,我們需要定義實體 (Entity) 與業務規則。
定義列舉 (Enum): 在
BookStore.Domain.Shared中定義書籍狀態,以便前後端共用。csharp// src/BookStore.Domain.Shared/Books/BookStatus.cs public enum BookStatus { Available = 0, Borrowed = 1 }定義聚合根 (Aggregate Root): 在
BookStore.Domain中修改Book實體,加入業務行為。注意:請避免使用 Anemic Domain Model (貧血模型),應將邏輯封裝在實體中。csharp// src/BookStore.Domain/Books/Book.cs using Volo.Abp.Domain.Entities.Auditing; using Volo.Abp; namespace BookStore.Books { public class Book : AuditedAggregateRoot<Guid> { public string Name { get; private set; } public BookStatus Status { get; private set; } // 私有建構子供 ORM 使用 private Book() { } public Book(Guid id, string name) : base(id) { SetName(name); Status = BookStatus.Available; } public void SetName(string name) { Name = Check.NotNullOrWhiteSpace(name, nameof(name)); } // 核心業務邏輯:借書 public void Borrow() { if (Status == BookStatus.Borrowed) { throw new BusinessException("BookStore:BookAlreadyBorrowed"); } Status = BookStatus.Borrowed; } // 核心業務邏輯:還書 public void Return() { Status = BookStatus.Available; } } }
3.3 步驟二:資料庫整合 (Infrastructure Layer)
定義好領域模型後,我們需要設定 EF Core 對應。
設定 DbContext: 在
BookStore.EntityFrameworkCore專案的BookStoreDbContext.cs中加入DbSet。csharppublic DbSet<Book> Books { get; set; }設定實體對應 (Mapping): 在
OnModelCreating方法中設定資料表結構。csharpprotected override void OnModelCreating(ModelBuilder builder) { base.OnModelCreating(builder); builder.Entity<Book>(b => { b.ToTable(BookStoreConsts.DbTablePrefix + "Books", BookStoreConsts.DbSchema); b.ConfigureByConvention(); // 自動設定標準屬性 (如 CreationTime) b.Property(x => x.Name).IsRequired().HasMaxLength(128); b.HasIndex(x => x.Name); // 建立索引 }); }建立並執行遷移 (Migration): 使用 ABP CLI 或 EF Core Tool。
bashdotnet ef migrations add Added_Book_Status -p src/BookStore.EntityFrameworkCore -s src/BookStore.DbMigrator dotnet run --project src/BookStore.DbMigrator
3.4 步驟三:應用服務實作 (Application Layer)
應用層負責協調業務邏輯,並將 DTO 轉換為領域物件。
定義 DTOs: 在
BookStore.Application.Contracts中定義輸入與輸出。csharp// BookDto.cs public class BookDto : AuditedEntityDto<Guid> { public string Name { get; set; } public BookStatus Status { get; set; } } // CreateUpdateBookDto.cs public class CreateUpdateBookDto { [Required] [StringLength(128)] public string Name { get; set; } }定義介面:
csharppublic interface IBookAppService : ICrudAppService<BookDto, Guid, PagedAndSortedResultRequestDto, CreateUpdateBookDto> { Task BorrowAsync(Guid id); }實作 AppService: 在
BookStore.Application中實作邏輯。csharppublic class BookAppService : CrudAppService<Book, BookDto, Guid, PagedAndSortedResultRequestDto, CreateUpdateBookDto>, IBookAppService { public BookAppService(IRepository<Book, Guid> repository) : base(repository) { } public async Task BorrowAsync(Guid id) { // 1. 取得實體 var book = await Repository.GetAsync(id); // 2. 執行領域邏輯 book.Borrow(); // 3. 更新實體 (EF Core 會自動追蹤變更,但明確呼叫 UpdateAsync 是好習慣) await Repository.UpdateAsync(book); } }
3.5 步驟四:自動化測試 (Testing)
ABP 提供了強大的測試基礎設施。我們應該在開發過程中編寫測試,而不是事後補救。
領域層單元測試: 測試
Book實體的邏輯。csharp// test/BookStore.Domain.Tests/Books/Book_Tests.cs public class Book_Tests { [Fact] public void Should_Borrow_Available_Book() { var book = new Book(Guid.NewGuid(), "Test Book"); book.Borrow(); book.Status.ShouldBe(BookStatus.Borrowed); } [Fact] public void Should_Throw_Exception_When_Borrowing_Borrowed_Book() { var book = new Book(Guid.NewGuid(), "Test Book"); book.Borrow(); // First borrow Should.Throw<BusinessException>(() => book.Borrow()); } }應用層整合測試: 測試 AppService 與資料庫的互動。
csharp// test/BookStore.Application.Tests/Books/BookAppService_Tests.cs public class BookAppService_Tests : BookStoreApplicationTestBase { private readonly IBookAppService _bookAppService; public BookAppService_Tests() { _bookAppService = GetRequiredService<IBookAppService>(); } [Fact] public async Task Should_Borrow_Book() { // Arrange: 建立一本書 (通常透過 Data Seeder) var bookDto = await _bookAppService.CreateAsync(new CreateUpdateBookDto { Name = "New Book" }); // Act await _bookAppService.BorrowAsync(bookDto.Id); // Assert var updatedBook = await _bookAppService.GetAsync(bookDto.Id); updatedBook.Status.ShouldBe(BookStatus.Borrowed); } }
3.6 步驟五:API 與 UI 整合
API 驗證: 啟動
BookStore.Web,使用 Swagger 測試POST /api/app/book/{id}/borrow。前端整合 (以 MVC 為例): 在
Index.cshtml.cs中注入IBookAppService並呼叫。csharppublic class IndexModel : PageModel { private readonly IBookAppService _bookAppService; public List<BookDto> Books { get; set; } public IndexModel(IBookAppService bookAppService) { _bookAppService = bookAppService; } public async Task OnGetAsync() { var result = await _bookAppService.GetListAsync(new PagedAndSortedResultRequestDto()); Books = result.Items.ToList(); } }
3.7 實戰技巧與最佳實踐
1. 使用 ObjectMapping
不要手動將 Entity 轉換為 DTO。在 BookStoreApplicationAutoMapperProfile.cs 中設定映射:
CreateMap<Book, BookDto>();
CreateMap<CreateUpdateBookDto, Book>();2. 異常處理與本地化
不要在程式碼中寫死錯誤訊息。
- 在
Domain.Shared的BookStoreErrorCodes定義錯誤碼。 - 在
en.json/zh-Hant.json定義友善訊息。
"BookStore:BookAlreadyBorrowed": "這本書已經被借出了!"3. 資料種子 (Data Seeding)
在開發與測試時,使用 Data Seeder 預填資料。 實作 IDataSeedContributor 介面,並在 DbMigrator 或測試啟動時執行。
3.8 練習與挑戰
基礎練習
- 完整實作:依照本章步驟,完成
Book的借閱與歸還功能。 - 新增欄位:為
Book新增Price(decimal) 欄位,並更新 DTO、EF Core Mapping 與測試。
進階挑戰
- 借閱歷史:建立一個新的實體
BookRentalHistory,當書籍被借閱時,自動建立一筆歷史紀錄 (提示:使用領域事件Domain Event)。 - 併發控制:如果兩個人同時借同一本書怎麼辦?研究 ABP 的
Optimistic Concurrency(樂觀併發控制) 並實作之。
3.9 總結
本章展示了 ABP 的標準開發流程:
- Domain First:先思考業務規則與實體狀態。
- Infrastructure Second:處理資料庫儲存。
- Application Third:暴露 API 與協調邏輯。
- Test Always:全程伴隨測試。
遵循此流程,您將能寫出結構清晰、易於維護且高品質的程式碼。下一章,我們將探索 ABP 官方提供的豐富範例與解決方案,讓您不必從頭造輪子。
參考資源: