由于软件系统中可能有着不同的数据库,不同的ORM,仓储思想的本质是解耦它们。在ABP中具体的实现仓储接口定义在领域层,实现在基础设施层。仓储接口被领域层(比如领域服务)和应用层用来访问数据库,操作聚合根,聚合根就是业务单元。这篇文章主要分析怎么通过规约将业务逻辑从仓储实现中剥离出来,从而让仓储专注于数据处理。

一.业务需求

还是以Issue聚合根为例,假如有个业务规则是:判断是否是未激活的Issue,条件是打开状态、未分配给任何人、创建超过30天、最近30天没有评论。Issue聚合根如下:

二.在仓储中实现业务逻辑

该业务规则在基础设施层中实现如下:

namespace IssueTracking.Issues
{
public class EfCoreIssueRepository : EfCoreRepository<IssueTrackingDbContext, IssueTracking, Guid>, IIssueRepository
{
// 构造函数
public EfCoreIssueRepository(IDbContextProvider<IssueTrackingDbContext> dbContextProvider) : base(dbContextProvider)
{
} // 判断是否是未激活的Issue
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();
}
}
}

根据DDD中仓储的实践原则,肯定是不能将业务逻辑放在仓储实现中的,接下来使用规约的方式来解决这个问题。

三.使用规约实现业务逻辑

规约就是一种约定,规范来讲:规约是一个命名的、可重用的、可组合的和可测试的类,用于根据业务规则来过滤领域对象。通过ABP中的Specification规约基类创建规约类,将判断Issue是否激活这个业务规则实现为一个规约类如下:

namespace IssueTracking.Issues
{
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)
}
}
}

接下来讲解在Issue实体和EfCoreIssueRepository类中如何使用InActiveIssueSpecification规约。

四.在实体中使用规约

规约是根据业务规则来过滤领域对象,Issue聚合根中的IsInActive()方法实现如下:

public class Issue : AggregateRoot<Guid>, IHasCreationTime
{
public bool IsClosed { get; private set; }
public Guid? AssignedUserId { get; private set; }
public DateTime CreateTime { get; private set; }
public DateTime? LastCommentTime { get; private set; } // 判断Issue是否未激活
public bool IsInActive()
{
return new InActiveIssueSpecification().IsSatisfiedBy(this);
}
}

创建一个InActiveIssueSpecification实例,使用它的IsSatisfiedBy()来进行规约验证。

五.在仓储中使用规约

领域层中的(自定义)仓储接口如下,GetIssuesAsync()接收一个规约对象参数:

public interface IIssueRepository : IRepository<Issue, Guid>
{
Task<List<Issue> GetIssuesAsync(ISpecification<Issue> spec);
}

基础设施层中的(自定义)仓储实现如下:

public class EfCoreIssueRepository: EfCoreRepository<IssueTrackingDbContext, EfCoreIssueRepository, Guid>, IIssueRepository
{
// 构造函数
public EfCoreIssueRepository(IDbContextProvider<IssueTrackingDbContext> dbContextProvider) : base(dbContextProvider)
{
} public async Task<List<Issue>> GetIssuesAsync(ISpecification<Issue> spec)
{
var dbSet = await GetDbSetAsync();
// 通过表达式实现Issue实体过滤
return await dbSet.Where(spec.ToExpression()).ToListAsync();
}
}
}

应用层使用规约如下,本质上就是新建一个规约实例,然后作为GetIssuesAsync()的参数:

public class IssueAppService : ApplicationService, IIssueAppService
{
private readonly IIssueRepository _issueRepository; // 构造函数
public IssueAppService(IIssueRepository issueRepository)
{
_issueRepository = issueRepository;
} public async Task DoItAsync()
{
// 在应用层通过仓储使用规约来过滤实体
var issues = await _issueRepository.GetIssuesAsync(new InActiveIssueSpecification());
}
}

六.在应用层中通过默认仓储来使用规约

上面是在应用层中通过自定义仓储来使用规约的,接下来讲解在应用层中通过默认仓储来使用规约:

public class IssueAppService : ApplicationService, IIssueAppService
{
private readonly IRepository<Issue, Guid> _issueRepository; // 构造函数
public IssueAppService(IRepository<Issue, Guid> issueRepository)
{
_issueRepository = issueRepository;
} public async Task DoItAsync()
{
var queryable = await _issueRepository.GetQueryableAsync();
// 简单理解,queryable就是查询出来的实体,然后根据规约进行过滤
var issues = AsyncExecuter.ToListAsync(queryable.Where(new InActiveIssueSpecification()));
}
}

说明:AsyncExecuter是ABP提供的一个工具类,用于使用异步LINQ拓展方法,而不依赖于EF Core NuGet包。

七.组合规约的使用

规约是可组合使用的,这样就变的很强大。比如,再定义一个规约,当Issue是指定里程碑是返回True。定义新的规约如下:

public class MilestoneSpecification : Specification<Issue>
{
public Guid MilestoneId { get; } // 构造函数
public MilestoneSpecification(Guid milestoneId)
{
MilestoneId = milestoneId;
} public override Expression<Func<Issue, bool>> ToExpression()
{
return x => x.MilestoneId == MilestoneId;
}
}

如果和上面定义的InActiveIssueSpecification规约组合,就可以实现业务逻辑:获取指定里程碑中未激活的Issue:

public class IssueAppService : ApplicationService, IIssueAppService
{
private readonly IRepository<Issue, Guid> _issueRepository; // 构造函数
public IssueAppService(IRepository<Issue, Guid> issueRepository)
{
_issueRepository = issueRepository;
} public async Task DoItAsync(Guid milestoneId)
{
var queryable = await _issueRepository.GetQueryableAsync();
// 组合规约的使用方法,除了Add扩展方法,还有Or()、And()、Not()等方法
var issues = AsyncExecuter.ToListAsync(
queryable.Where(new InActiveIssueSpecification()
.Add(new MilestoneSpecification(milestoneId))
.ToExpression()
)
);
}
}

参考文献:

[1]基于ABP Framework实现领域驱动设计:https://url39.ctfile.com/f/2501739-616007877-f3e258?p=2096 (访问密码: 2096)

基于ABP实现DDD--仓储实践的更多相关文章

  1. 基于ABP落地领域驱动设计-02.聚合和聚合根的最佳实践和原则

    目录 前言 聚合 聚合和聚合根原则 包含业务原则 单个单元原则 事务边界原则 可序列化原则 聚合和聚合根最佳实践 只通过ID引用其他聚合 用于 EF Core 和 关系型数据库 保持聚合根足够小 聚合 ...

  2. 基于ABP落地领域驱动设计-03.仓储和规约最佳实践和原则

    目录 系列文章 仓储 仓储的通用原则 仓储中不包含领域逻辑 规约 在实体中使用规约 在仓储中使用规约 组合规约 学习帮助 围绕DDD和ABP Framework两个核心技术,后面还会陆续发布核心构件实 ...

  3. 基于ABP落地领域驱动设计-04.领域服务和应用服务的最佳实践和原则

    目录 系列文章 领域服务 应用服务 学习帮助 系列文章 基于ABP落地领域驱动设计-00.目录和前言 基于ABP落地领域驱动设计-01.全景图 基于ABP落地领域驱动设计-02.聚合和聚合根的最佳实践 ...

  4. 基于ABP落地领域驱动设计-05.实体创建和更新最佳实践

    目录 系列文章 数据传输对象 输入DTO最佳实践 不要在输入DTO中定义不使用的属性 不要重用输入DTO 输入DTO中验证逻辑 输出DTO最佳实践 对象映射 学习帮助 系列文章 基于ABP落地领域驱动 ...

  5. 基于ABP实现DDD--领域服务、应用服务和DTO实践

      什么是领域服务呢?领域服务就是领域对象本身的服务,通常是通过多个聚合以实现单个聚合无法处理的逻辑. 一.领域服务实践 接下来将聚合根Issue中的AssignToAsync()方法[将问题分配给用 ...

  6. 基于ABP实现DDD--聚合和聚合根实践

      在下面的例子中涉及Repository.Issue.Label.User这4个聚合根,接下来以Issue聚合为例进行分析,其中Issue聚合是由Issue[聚合根].Comment[实体].Iss ...

  7. ABP(现代ASP.NET样板开发框架)系列之11、ABP领域层——仓储(Repositories)

    点这里进入ABP系列文章总目录 基于DDD的现代ASP.NET开发框架--ABP系列之11.ABP领域层——仓储(Repositories) ABP是“ASP.NET Boilerplate Proj ...

  8. ABP领域层——仓储(Repositories)

    ABP领域层——仓储(Repositories) 点这里进入ABP系列文章总目录 基于DDD的现代ASP.NET开发框架--ABP系列之11.ABP领域层——仓储(Repositories) ABP是 ...

  9. 基于ABP落地领域驱动设计-01.全景图

    什么是领域驱动设计? 领域驱动设计(简称:DDD)是一种针对复杂需求的软件开发方法.将软件实现与不断发展的模型联系起来,专注于核心领域逻辑,而不是基础设施细节.DDD适用于复杂领域和大规模应用,而不是 ...

随机推荐

  1. Linux学习教程 | 全文目录

    本教程最大的特点是通俗易懂,并且非常详细,花费 7 天时间即可快速了解 Linux. 第一章 Linux简介 1.1 操作系统是什么,操作系统概述 1.2 Linux是什么,有哪些特点? 1.3 Li ...

  2. 文本框字符限制、focus光标定位

    一.为一个元素的所有子元素设置统一样式:.className * { color: #6666 } 二.正则表达式: 1.去除所有HTML标签只保留文字: /<\/?.+?\/?>/2.去 ...

  3. Spring Framework 远程命令执行漏洞(CVE-2022-22965)

    Spring Framework 远程命令执行漏洞 (CVE-2022-22965) 近日,Spring 官方 GitHub issue中提到了关于 Spring Core 的远程命令执行漏洞,该漏洞 ...

  4. Yapi Docker 部署

    参考 https://github.com/Ryan-Miao/docker-yapi , 并使用该代码的脚本构建yapi image. 部署mongodb docker run \ --name m ...

  5. 基于DEM的坡度坡向分析

    坡度坡向分析方法 坡度(slope)是地面特定区域高度变化比率的量度.坡度的表示方法有百分比法.度数法.密位法和分数法四种,其中以百分比法和度数法较为常用.本文计算的为坡度百分比数据.如当角度为45度 ...

  6. 四、针对redis容灾切换导致"脑裂"的情况

    网上参考到别人博客说,redis容灾切换的时候,有几率出现脑裂的情况. 什么是脑裂: sentinel判断master宕机,切换slave为新master的过程中,业务数据还在持续往原master写入 ...

  7. postman 脚本和变量

    背景 后端接口有登录或鉴权验证,通过 swagger 调用比较费劲,并且 java 的 swagger 库(不够自动化,嵌套类支持需要各种配置才能正常显示 schema)个人感觉也没有 .net co ...

  8. 详细剖析pyecharts大屏的Page函数配置文件:chart_config.json

    目录 一.问题背景 二.揭开json文件神秘面纱 三.巧用json文件 四.关于Table图表 五.同步讲解视频 5.1 讲解json的视频 5.2 讲解全流程大屏的视频 5.3 讲解全流程大屏的文章 ...

  9. 企业应用架构研究系列二十六:信号量SemaphoreSlim与Semaphore

    在进行多线程程序的开发和设计的过程中,不可避免的需要引入semaphore信号量这个组件,这是.net框架提供的一个对多线程计数互斥的方案,就是允许指定的线程个数访问特定的资源而增加的 一个" ...

  10. 目标检测复习之Faster RCNN系列

    目标检测之faster rcnn系列 paper blogs1: 一文读懂Faster RCNN Faster RCNN理论合集 code: mmdetection Faster rcnn总结: 网络 ...