Skip to content

第三章:實戰應用程式開發流程

3.1 引言:標準化的開發節奏

在 ABP Framework 中,開發一個新功能通常遵循一個標準的節奏。這個節奏確保了程式碼的可維護性、可測試性以及與 DDD 原則的一致性。

標準開發流程 (The ABP Way)

本章將以 「圖書借閱 (Book Borrowing)」 功能為例,帶您走過這六個步驟。


3.2 步驟一:需求分析與領域建模

需求

使用者可以借閱一本書。如果書已經被借出,則不能再借。借閱成功後,書籍狀態應變更為「已借出」。

領域層設計 (Domain Layer)

BookStore.Domain 專案中,我們需要定義實體 (Entity) 與業務規則。

  1. 定義列舉 (Enum): 在 BookStore.Domain.Shared 中定義書籍狀態,以便前後端共用。

    csharp
    // src/BookStore.Domain.Shared/Books/BookStatus.cs
    public enum BookStatus
    {
        Available = 0,
        Borrowed = 1
    }
  2. 定義聚合根 (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 對應。

  1. 設定 DbContext: 在 BookStore.EntityFrameworkCore 專案的 BookStoreDbContext.cs 中加入 DbSet

    csharp
    public DbSet<Book> Books { get; set; }
  2. 設定實體對應 (Mapping): 在 OnModelCreating 方法中設定資料表結構。

    csharp
    protected 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); // 建立索引
        });
    }
  3. 建立並執行遷移 (Migration): 使用 ABP CLI 或 EF Core Tool。

    bash
    dotnet 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 轉換為領域物件。

  1. 定義 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; }
    }
  2. 定義介面

    csharp
    public interface IBookAppService :
        ICrudAppService<BookDto, Guid, PagedAndSortedResultRequestDto, CreateUpdateBookDto>
    {
        Task BorrowAsync(Guid id);
    }
  3. 實作 AppService: 在 BookStore.Application 中實作邏輯。

    csharp
    public 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 提供了強大的測試基礎設施。我們應該在開發過程中編寫測試,而不是事後補救。

  1. 領域層單元測試: 測試 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());
        }
    }
  2. 應用層整合測試: 測試 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 整合

  1. API 驗證: 啟動 BookStore.Web,使用 Swagger 測試 POST /api/app/book/{id}/borrow

  2. 前端整合 (以 MVC 為例): 在 Index.cshtml.cs 中注入 IBookAppService 並呼叫。

    csharp
    public 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 中設定映射:

csharp
CreateMap<Book, BookDto>();
CreateMap<CreateUpdateBookDto, Book>();

2. 異常處理與本地化

不要在程式碼中寫死錯誤訊息。

  • Domain.SharedBookStoreErrorCodes 定義錯誤碼。
  • en.json / zh-Hant.json 定義友善訊息。
json
"BookStore:BookAlreadyBorrowed": "這本書已經被借出了!"

3. 資料種子 (Data Seeding)

在開發與測試時,使用 Data Seeder 預填資料。 實作 IDataSeedContributor 介面,並在 DbMigrator 或測試啟動時執行。


3.8 練習與挑戰

基礎練習

  1. 完整實作:依照本章步驟,完成 Book 的借閱與歸還功能。
  2. 新增欄位:為 Book 新增 Price (decimal) 欄位,並更新 DTO、EF Core Mapping 與測試。

進階挑戰

  1. 借閱歷史:建立一個新的實體 BookRentalHistory,當書籍被借閱時,自動建立一筆歷史紀錄 (提示:使用領域事件 Domain Event)。
  2. 併發控制:如果兩個人同時借同一本書怎麼辦?研究 ABP 的 Optimistic Concurrency (樂觀併發控制) 並實作之。

3.9 總結

本章展示了 ABP 的標準開發流程:

  1. Domain First:先思考業務規則與實體狀態。
  2. Infrastructure Second:處理資料庫儲存。
  3. Application Third:暴露 API 與協調邏輯。
  4. Test Always:全程伴隨測試。

遵循此流程,您將能寫出結構清晰、易於維護且高品質的程式碼。下一章,我們將探索 ABP 官方提供的豐富範例與解決方案,讓您不必從頭造輪子。


參考資源

Released under the MIT License.