ABP.IO 新手教學 No.07 開發教學 第 6 部分 作者:領域層 {id="ABP-IO-Tutorial-No-07-Part-6-Authors-Domain-Layer"}
使用一些 DDD 最佳實踐來實作 Author 的領域層
Web 應用程序開發教程 - 第 6 部分:作者:領域層
關於本教程
在本系列教程中,您將構建一個名為Acme.BookStore. 此應用程序用於管理書籍及其作者的列表。它是使用以下技術開發的:
本教程分為以下幾個部分;
下載源代碼
本教程根據您的UI和數據庫首選項有多個版本。我們準備了幾個要下載的源代碼組合:
介紹
在前面的部分中,我們已經使用 ABP 基礎架構輕鬆構建了一些服務;
對於“作者”部分;
作者實體
Authors在Acme.BookStore.Domain項目中創建一個文件夾(命名空間)並在其中添加一個Author類:
using System;
using JetBrains.Annotations;
using Volo.Abp;
using Volo.Abp.Domain.Entities.Auditing;
namespace Acme.BookStore.Authors
{
public class Author : FullAuditedAggregateRoot<Guid>
{
public string Name { get; private set; }
public DateTime BirthDate { get; set; }
public string ShortBio { get; set; }
private Author()
{
/* This constructor is for deserialization / ORM purpose */
}
internal Author(
Guid id,
[NotNull] string name,
DateTime birthDate,
[CanBeNull] string shortBio = null)
: base(id)
{
SetName(name);
BirthDate = birthDate;
ShortBio = shortBio;
}
internal Author ChangeName([NotNull] string name)
{
SetName(name);
return this;
}
private void SetName([NotNull] string name)
{
Name = Check.NotNullOrWhiteSpace(
name,
nameof(name),
maxLength: AuthorConsts.MaxNameLength
);
}
}
}
繼承自FullAuditedAggregateRoot<Guid>which 使實體軟刪除 (這意味著當您刪除它時,它不會在數據庫中刪除,而只是標記為已刪除)具有所有審計屬性。
private set因為Name屬性限制從這個類中設置這個屬性。有兩種設置名稱的方法(在這兩種情況下,我們都驗證名稱):
在構造函數中,同時創建一個新作者。
ChangeName稍後使用該方法更新名稱。
的constructor和ChangeName方法是internal迫使僅在域層使用這些方法,使用AuthorManager將在後面說明。
Checkclass 是一個 ABP 框架實用程序類,可幫助您檢查方法參數(它會引發ArgumentException無效情況)。

AuthorConsts是一個簡單的類,位於項目的Authors命名空間(文件夾)下Acme.BookStore.Domain.Shared:
namespace Acme.BookStore.Authors
{
public static class AuthorConsts
{
public const int MaxNameLength = 64;
}
}
在Acme.BookStore.Domain.Shared項目內部創建了這個類,因為我們稍後將在數據傳輸對象 (DTO)上重用它。

AuthorManager:領域服務
Author構造函數和ChangeName方法是internal ,因此它們只能在領域層中使用。
在 Acme.BookStore.Domain 專案的Authors文件夾(命名空間)中創建一個類 AuthorManager:
using System;
using System.Threading.Tasks;
using JetBrains.Annotations;
using Volo.Abp;
using Volo.Abp.Domain.Services;
namespace Acme.BookStore.Authors
{
public class AuthorManager : DomainService
{
private readonly IAuthorRepository _authorRepository;
public AuthorManager(IAuthorRepository authorRepository)
{
_authorRepository = authorRepository;
}
public async Task<Author> CreateAsync(
[NotNull] string name,
DateTime birthDate,
[CanBeNull] string shortBio = null)
{
Check.NotNullOrWhiteSpace(name, nameof(name));
var existingAuthor = await _authorRepository.FindByNameAsync(name);
if (existingAuthor != null)
{
throw new AuthorAlreadyExistsException(name);
}
return new Author(
GuidGenerator.Create(),
name,
birthDate,
shortBio
);
}
public async Task ChangeNameAsync(
[NotNull] Author author,
[NotNull] string newName)
{
Check.NotNull(author, nameof(author));
Check.NotNullOrWhiteSpace(newName, nameof(newName));
var existingAuthor = await _authorRepository.FindByNameAsync(newName);
if (existingAuthor != null && existingAuthor.Id != author.Id)
{
throw new AuthorAlreadyExistsException(newName);
}
author.ChangeName(newName);
}
}
}

兩種方法都會檢查是否已經存在具有給定名稱的作者並拋出一個特殊的業務異常, AuthorAlreadyExistsException在Acme.BookStore.Domain項目(Authors文件夾中)中定義,如下所示:
using Volo.Abp;
namespace Acme.BookStore.Authors
{
public class AuthorAlreadyExistsException : BusinessException
{
public AuthorAlreadyExistsException(string name)
: base(BookStoreDomainErrorCodes.AuthorAlreadyExists)
{
WithData("name", name);
}
}
}
BusinessException是一種特殊的異常類型。在需要時拋出域相關的異常是一個很好的做法。它由 ABP 框架自動處理,並且可以輕鬆本地化。
WithData(...)方法用於向異常對象提供附加數據,這些數據稍後將用於本地化消息或用於其他目的。

BookStoreDomainErrorCodes在Acme.BookStore.Domain.Shared項目中打開,修改如下圖:
namespace Acme.BookStore
{
public static class BookStoreDomainErrorCodes
{
public const string AuthorAlreadyExists = "BookStore:00001";
}
}

這是一個唯一的字符串,代表您的應用程序拋出的錯誤代碼,可由客戶端應用程序處理。對於用戶,您可能希望對其進行本地化。
打開項目Localization/BookStore/en.json內部Acme.BookStore.Domain.Shared並添加以下條目:
"BookStore:00001": "There is already an author with the same name: {name}"
無論何時拋出AuthorAlreadyExistsException ,最終用戶都會在 UI 上看到一條很好的錯誤消息。

IAuthorRepository
// 這個自訂倉儲的所定義的方法其實直接使用內建的通用倉儲就可以完成,這邊只是為了示範自訂倉儲該如何一步步實現,如果用不到沒必要多此一舉。
AuthorManager注入IAuthorRepository ,所以我們需要定義它。在 Acme.BookStore.Domain 項目的Authors文件夾(命名空間)中創建這個新接口:
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Volo.Abp.Domain.Repositories;
namespace Acme.BookStore.Authors
{
public interface IAuthorRepository : IRepository<Author, Guid>
{
Task<Author> FindByNameAsync(string name);
Task<List<Author>> GetListAsync(
int skipCount,
int maxResultCount,
string sorting,
string filter = null
);
}
}
IAuthorRepository擴展了標準IRepository<Author, Guid>接口,因此所有標準存儲庫方法也可用於IAuthorRepository.
FindByNameAsync用於AuthorManager按姓名查詢作者。
GetListAsync 將在應用程序層中用於獲取列出、排序和過濾的作者列表以顯示在 UI 上。

我們將在下一部分實現這個存儲庫。
// 這邊由於 DDD 領域層不會相依基礎設施層,所以在這邊只定義了介面,至於實作部分因為會使用到 DbContext,所以會放到基礎設施層來進行實作的部分。
結論
這部分涵蓋了書店應用程序作者功能的領域層。在此部分中創建/更新的主要文件在下圖中突出顯示:


下一部分
請參閱本教程的下一部分。

PS5
本文章從點部落遷移至 Writerside
14 October 2025