由于软件系统中可能有着不同的数据库,不同的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. 1.9 初学者应选择哪个Linux发行版?

    前面章节中,已经对几个常见的 Linux 发行版做了简单的介绍,那么对于初学者来说,选择哪个发行版的性价比更高呢? 通常情况下,初学者学习 Linux,是为了找一份和 Linux 相关的工作,那么问题 ...

  2. pycham的安装、优化、使用

    一.下载与安装 下载地址:https://confluence.jetbrains.com/display/PYH/Previous+PyCharm+Releases 注册:参考地址:http://i ...

  3. 操作系统实现-loader

    博客网址:www.shicoder.top 微信:18223081347 欢迎加群聊天 :452380935 大家好呀,终于我们到了操作系统的loader部分了,loader也是操作系统中最重要的一个 ...

  4. 使用DSVPN解决分支网络出口为ADSL场景下的内网互通

    背景 最近接到一个项目是一家机构总部与多个分支之间的内网互通,总部具有固定ip,分部是使用adsl动态获取的不固定公网ip,由于两端互联网ip不固定所以不能使用传统的GRE技术来实现,所以最后经过评估 ...

  5. vue项目引入TinyMCE

    1.安装 npm install @tinymce/tinymce-vue@3.0.1 -S 2.配置 <template> <!-- 富文本 --> <div> ...

  6. 200 行代码实现基于 Paxos 的 KV 存储

    前言 写完[paxos 的直观解释]之后,网友都说疗效甚好,但是也会对这篇教程中一些环节提出疑问(有疑问说明真的看懂了 ),例如怎么把只能确定一个值的 paxos 应用到实际场景中. 既然 Talk ...

  7. IPC机制与线程的操作

    目录 Queue模块 IPC机制(进程间通信) 生产者消费者模型 线程理论 创建线程的两种方式 线程实现TCP服务端的并发 线程join方法 线程数据共享 线程对象属性和方法 守护线程 GIL全局解释 ...

  8. .net6.0 中一个接口多个实现的服务注册与注入

    1.现有一个数据库操作接口 如下   它有两个数据操作实现 Sqlserver 和MySql的数据库操作实现类 现在我们需要 将这个两个类 注册到MVC中 注意这里注册的服务类型都是 IDataBas ...

  9. 记一次生产事故的排查与优化——Java服务假死

    一.现象 在服务器上通过curl命令调用一个Java服务的查询接口,半天没有任何响应.关于该服务的基本功能如下: 1.该服务是一个后台刷新指示器的服务,即该服务会将用户需要的指示器数据提前计算好,放入 ...

  10. Unity-自定义事件派发器的两次尝试

    一.前言: 在游戏开发的很多时候,需要引用其他类的方法,但是一旦类多起来了,相互引用会导致引用关系混乱,极其难以阅读. 以前初次做抖音小游戏时,和一位经验老道的cocos程序员合作,看到我写的代码他不 ...