存储库

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框架提供的规范基础架构的更多细节,请参阅 规范文档

实现领域驱动设计 - 使用ABP框架 - 存储库的更多相关文章

  1. .net core +codefirst(.net core 基础入门,适合这方面的小白阅读) 【我们一起写框架】领域驱动设计的CodeFirst框架(一)—序篇

    .net core +codefirst(.net core 基础入门,适合这方面的小白阅读)   前言 .net core mvc和 .net mvc开发很相似,比如 视图-模型-控制器结构.所以. ...

  2. 【我们一起写框架】领域驱动设计的CodeFirst框架(一)—序篇

    前言 领域驱动设计,其实已经是一个很古老的概念了,但它的复杂度依旧让学习的人头疼不已. 互联网关于领域驱动的文章有很多,每一篇写的都很好,理解领域驱动设计的人都看的懂. 不过,这些文章对于那些初学者而 ...

  3. 【DDD】使用领域驱动设计思想实现业务系统

    最近新接了一个业务系统——社区服务系统,为了快速熟悉和梳理老系统的业务逻辑和代码,同时对老系统代码做一些优化,于是打算花上一个月时间不间断地对老系统服务进行重构.同时,考虑到社区业务的复杂性,想起了之 ...

  4. .NET领域驱动设计—初尝(一:疑问、模式、原则、工具、过程、框架、实践)

     .NET领域驱动设计—初尝(一:疑问.模式.原则.工具.过程.框架.实践) 2013-04-07 17:35:27 标签:.NET DDD 驱动设计 原创作品,允许转载,转载时请务必以超链接形式标明 ...

  5. 如何使用ABP进行软件开发(2) 领域驱动设计和三层架构的对比

    简述 上一篇简述了ABP框架中的一些基础理论,包括ABP前后端项目的分层结构,以及后端项目中涉及到的知识点,例如DTO,应用服务层,整洁架构,领域对象(如实体,聚合,值对象)等. 笔者也曾经提到,AB ...

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

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

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

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

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

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

  9. 基于ABP落地领域驱动设计-00.目录和小结

    <实现领域驱动设计> -- 基于 ABP Framework 实现领域驱动设计实用指南 翻译缘由 自 ABP vNext 1.0 开始学习和使用该框架,被其优雅的设计和实现吸引,适逢 AB ...

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

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

随机推荐

  1. [原]CentOS7部署PostGis

    转载请注明原作者(think8848)和出处(http://think8848.cnblogs.com) 本文参考了<An almost idiot's guide to install Pos ...

  2. php ffmpeg

    可以合成视频,音频视频合成等 $aurl = '../Uploads/movie/zzz.mp4'; $vurl = '../Uploads/mp3/dmd.mp3'; // exec("c ...

  3. python deep copy and shallow copy

    Python中对于对象的赋值都是引用,而不是拷贝对象(Assignment statements in Python do not copy objects, they create bindings ...

  4. 获取jQuery对象的第N个DOM元素 &amp;&amp; table常用css样式

    获取jQuery对象的第N个DOM元素 1.$(selector).get(N-1) 2.$(selector)[N-1] 注意:.index()方法返回的是一个数,相当于C#中的IndexOf() ...

  5. sping注解原理

    持续更新中.. spring注解用的是java注解,用到的是java反射机制. 参考文档如下: http://zxf-noimp.iteye.com/blog/1071765 对应spring源码如下 ...

  6. ASP.NET中共有哪几种类型的控件?其中,HTML控件、HTML服务器控件和WEB服务器控件之间有什么区别

    ASP.NET的控件包括WEB服务器控件.WEB用户控件.WEB自定义控件.HTML服务器控件和HTML控件.HTML控件.HTML服务器控件和WEB服务器控件之间的区别如下所示.q      HTM ...

  7. pwnable.kr fb

    fb-1 pt 连接到服务器,发现 有三个文件,fd脚本,fd.c脚本的源程序,flag是要看的东西,无权限 来我们分析一下源码 如果只传进去一个值,print” pass argv[1] a num ...

  8. .netcore 整合 log4net

    1.背景 前两天,曾经的一个同事咨询我,怎样将log4net以中间件的形式整合到core里边去.我不假思索的回答,这种问题应该有人做过吧,他说没有.于是,我去博客园搜了下,发现还真没有,全部都是传统. ...

  9. [Model] AlexNet

    CaffeNet - a variant of AlexNet Ref: Classification: Instant Recognition with Caffe This is caffeNet ...

  10. C#调用haskell遭遇Attempted to read or write protected memory

    1. Haskell的代码如下: 上面的代码中readMarkdown与writeHtmlString是pandoc中的函数,newString的作用是将String转换为IO CString. 2. ...