實現領域驅動設計 – 使用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框架提供的規範基礎架構的更多細節,請參閱 規範文檔