實現領域驅動設計 - 使用ABP框架 - 存儲庫
存儲庫
Repository 是一個類似于集合的接口,領域層和應用程序層使用它來訪問數據持久性系統(數據庫),以讀寫業務對象(通常是聚合)
常見的存儲庫原則是:
- 在領域層定義一個存儲庫接口(因為它被用于領域層和應用層),在基礎設施層實現(啟動模板中的EntityFrameworkCore項目)
- 不要在存儲庫中包含業務邏輯。
- 存儲庫接口應該是獨立于數據庫提供者/ ORM的。例如,不要從存儲庫方法返回DbSet。DbSet是 EF Core 提供的一個對象
- 為聚合根創建存儲庫,而不是為所有實體。因為,子集合實體(聚合的)應該通過聚合根訪問
不要在存儲庫中包含領域邏輯
雖然這個規則在一開始看起來很明顯,但是很容易將業務邏輯泄露到存儲庫中
示例:從存儲庫中獲取不活躍的問題
public interface IIssueRepository : IRepository<Issue, Guid>
{
Task<List<Issue>> GetInActiveIssuesAsync();
}
IIssueRepository 擴展了標準 IRepository<...> 接口,添加GetInActiveIssuesAsync 方法。這個存儲庫使用這樣一個Issue類:
public class Issue : AggregateRoot<Guid>, IHasCreationTime
{
public bool IsClosed { get; private set; }
public Guid? AssignedUserId { get; private set; }
public DateTime CreationTime { get; private set; }
public DateTime? LastCommentTime { get; private set; }
}
(代碼只顯示了本例所需的屬性)
規則規定存儲庫不應該知道業務規則。這里的問題是 “什么是不活躍的問題? ”它是業務規則定義嗎?”
讓我們看看實現來理解它:
public class EfCoreIssueRepository :
EfCoreRepository<IssueTrackingDbContext, Issue, Guid>
IIssueRepository
{
public async Task<List<Issue>> GetInActiveIssuesAsync()
{
var daysAgo30 = DateTime.Now.Subtract(TimeSpan.FromDays(30));
var dbSet = await GetDbSetAsync();
return await dbSet.Where(i =>
//開放的
!i.IsClosed &&
//沒有分配給任何人
i.AssignedUserId == null &&
//30天前創建的
i.CreationTime < daysAgo30 &&
//最近30天內沒有任何評論
(i.LastCommentTime == null || i.LastCommentTime < daysAgo30)
).ToListAsync();
}
}
(使用EF Core實現。查看 EF Core集成文檔,了解如何使用 EF Core 創建自定義存儲庫。)
當我們檢查 GetInActiveIssuesAsync 的實現時,我們看到了一個業務規則,它給出了不活躍的問題的定義:該問題應該是開放的,沒有分配給任何人,30天前創建的,并且在最近30天內沒有任何評論
這是隱藏在存儲庫方法中的業務規則的隱式定義。當我們需要重用該業務邏輯時,就會出現問題
例如,假設我們想要在 Issue 實體上添加一個 bool IsInActive() 方法。這樣,當我們有 Issue 實體時,我們就可以檢查活躍度。
讓我們看看實現:
public class Issue : AggregateRoot<Guid>, IHasCreationTime
{
public bool IsClosed { get; private set; }
public Guid? AssignedUserId { get; private set; }
public DateTime CreationTime { get; private set; }
public DateTime? LastCommentTime { get; private set; }
public bool IsInActive()
{
var daysAgo30 = DateTime.Now.Subtract(TimeSpan.FromDays(30));
return
//開放的
!IsClosed &&
//沒有分配給任何人
AssignedUserId == null &&
//30天前創建的
CreationTime < daysAgo30 &&
//最近30天內沒有任何評論
(LastCommentTime == null || LastCommentTime < daysAgo30);
}
}
我們必須復制/粘貼/修改代碼。如果活動性的定義改變了呢?我們不應該忘記更新這兩個地方。這是業務邏輯的重復,這是非常危險的
這個問題的一個很好的解決方案是規范模式!
規范
規范是一個命名的、可重用的、可組合的和可測試的類,用于基于業務規則篩選領域對象
ABP框架提供了必要的基礎設施來輕松地創建規范類并在應用程序代碼中使用它們。讓我們將不活躍的問題過濾器實現為一個規范類:
public class InActiveIssueSpecification : Specification<Issue>
{
public override Expression<Func<Issue,bool>> ToExpression()
{
var daysAgo30 = DateTime.Now.Subtract(TimeSpan.FromDays(30));
return i =>
//開放的
!i.IsClosed &&
//沒有分配給任何人
i.AssignedUserId == null &&
//30天前創建的
i.CreationTime < daysAgo30 &&
//最近30天內沒有任何評論
(i.LastCommentTime == null || i.LastCommentTime < daysAgo30);
}
}
Specification<T> 基類通過定義表達式簡化了創建規范類的工作。只是將表達式從存儲庫移到這里
現在我們就可以在 Issue 實體和 EfCoreIssueRepository 類中復用 InActiveIssueSpecification 了
在實體中使用規范
Specification 類提供了一個 IsSatisfiedBy 方法,如果給定的對象(實體)滿足規范,該方法返回true。我們可以重寫這個 Issue。IsInActive 方法如下所示:
public class Issue : AggregateRoot<Guid>, IHasCreationTime
{
public bool IsInActive()
{
return new InActiveIssueSpecification().IsSatisfiedBy(this);
}
}
在存儲庫中使用規范
首先,從存儲庫接口開始:
public interface IIssueRepository : IRepository<Issue, Guid>
{
Task<List<Issue>> GetIssuesAsync(ISpecification<Issue> spec);
}
- 將 GetInActiveIssuesAsync 重命名為簡單的 GetIssuesAsync, 并接受一個規范對象
- 由于規范(過濾器)已經從存儲庫中移出,我們不再需要創建不同的方法來獲得不同條件下的問題(比如: GetAssignedIssues(...) , GetLockedIssues(...) 等等)
更新后的存儲庫實現可以像這樣:
public class EfCoreIssueRepository :
EfCoreRepository<IssueTrackingDbContext, Issue, Guid>
IIssueRepository
{
public async Task<List<Issue>> GetIssuesAsync(ISpecification<Issue> spec)
{
var dbSet = await GetDbSetAsync();
return await dbSet
.Where(spec.ToExpression())
.ToListAsync();
}
}
因為ToExpression()方法返回一個表達式,所以它可以直接傳遞給Where方法來過濾實體
- 最終,我們做到了業務邏輯的代碼復用,消除了安全隱患
使用默認的存儲庫
實際上,您不必創建自定義存儲庫才能使用規范。標準的IRepository已經擴展了IQueryable,所以你可以在上面使用標準的LINQ擴展方法:
public class IssueAppService : ApplicationService, IIssueAppService
{
public async Task<List<Issue>> GetInActiveIssuesAsync()
{
var queryable = await _issueRepository.GetQueryableAsync();
var issues = await AsyncExecuter.ToListAsync(
queryable.Where(new InActiveIssueSpecification())
);
}
}
AsyncExecuter 是ABP框架提供的一個實用工具,用于使用異步LINQ擴展方法(如這里的ToListAsync),而不依賴于EF Core NuGet包。有關更多信息,請參閱 Repositories文檔
組合規范
規范的一個強大的方面是它們是可組合的。假設我們有另一個規范,它只在問題位于里程碑時返回true
public class MilestoneSpecification : Specification<Issue>
{
public Guid MilestoneId { get; }
public override Expression<Func<Issue,bool>> ToExpression()
{
return i => i.MilestoneId == MilestoneId;
}
}
本規范是參數化的,與 InActiveIssueSpecification 有所不同。我們可以結合這兩個規范來獲得特定里程碑中的非活躍問題列表
public class IssueAppService : ApplicationService, IIssueAppService
{
public async Task<List<Issue>> GetInActiveIssuesWithinMilestoneAsync(Guid milestoneId)
{
var queryable = await _issueRepository.GetQueryableAsync();
var issues = await AsyncExecuter.ToListAsync(
queryable.Where(
new InActiveIssueSpecification()
.And(new MilestoneSpecification(milestoneId))
.ToExpression()
)
);
}
}
上面的示例使用And擴展方法來組合這些規范。還有更多的組合方法可用,比如 Or(…) 和 AndNot(…)
有關ABP框架提供的規范基礎架構的更多細節,請參閱 規范文檔。

浙公網安備 33010602011771號