实现领域驱动设计 - 使用ABP框架 - 创建实体
用例演示 - 创建实体
本节将演示一些示例用例并讨论可选场景。
创建实体
从实体/聚合根类创建对象是实体生命周期的第一步。聚合/聚合根规则和最佳实践部分建议为Entity类创建一个主构造函数,以保证创建一个有效的实体。因此,无论何时我们需要创建实体的实例,我们都应该使用那个构造函数
参见下面的问题聚合根类:
public class Issue : AggregateRoot<Guid>
{
public Guid RepositoryId { get; private set; }
public string Title { get; private set; }
public string Text { get; set; }
public Guid? AssignedUserId { get; private set; }
public Issue(
Guid id,
Guid repositoryId,
string title,
string text = null
) : base(id)
{
RepositoryId = repositoryId;
Title = Check.NotNullOrWhiteSpace(title, nameof(title));
Text = text; // 允许空值
}
private Issue() { //为ORM保留的空构造函数 }
public void SetTitle(string title)
{
Title = Check.NotNullOrWhiteSpace(title, nameof(title));
}
}
该类保证通过其构造函数创建有效的实体。
如果你需要更改标题,你需要使用 SetTitle 方法保证标题在一个有效状态
如果您想将这个问题分配给用户,您需要使用 IssueManager (它在分配之前实现了一些业务规则, 请参阅我之前关于 领域服务 的文章)。
Text 属性有一个公共setter,因为它也接受null值,并且这个示例没有任何验证规则。它在构造函数中也是可选的
让我们看看用于创建问题的Application Service方法:
public class IssueAppService : ApplicationService, IIssueAppService
{
//省略了Repository和DomainService的依赖注入
[Authorize]
public async Task<IssueDto> CreateAsync(IssueCreationDto input)
{
//创建一个有效的问题实体
var issue = new Issue(
GuidGenerator.Create(),
input.RepositoryId,
input.Title,
input.Text
);
//如果传入了被分配人,则把该问题法分配给这个用户
if(input.AssignedUserId.HasValue)
{
var user = await _userRepository.GetAsync(input.AssignedUserId.Value);
await _issueManager.AssignToAsync(issue, user);
}
// 把问题实体保存到数据库
await _issueRepository.InsertAsync(issue);
//返回表示这个新的问题的DTO
return ObjectMapper.Map<Issue, IssueDto>(issue);
}
}
CreateAsync 方法:
- 使用 Issue 构造函数创建有效的问题。它使用 IGuidGenerator 服务传递Id。这里不使用自动对象映射
- 如果客户端希望在对象创建时将这个问题分配给用户,它会使用IssueManager 来完成,允许 IssueManager 在分配之前执行必要的检查。
- 保存实体到数据库
- 最后使用 IObjectMapper 返回一个 IssueDto ,该 IssueDto 是通过映射从新的 Issue 实体自动创建的
使用领域规则创建实体
上述示例, Issue 没有关于实体创建的业务规则,除了在构造函数中进行一些形式的验证。但是,在某些情况下,实体创建应该检查一些额外的业务规则
例如,假设您不希望在完全相同的标题已经存在问题的情况下创建问题。在哪里实现这个规则? 在 Application Service 中实现此规则是不合适的,因为它是一个应该始终检查的 核心业务(领域)规则
该规则应该在 领域服务 (在本例中是 IssueManager )中实现。因此,我们需要强制应用层总是使用 IssueManager 来创建一个新的 Issue
首先,我们可以将 Issue 构造函数设置为 internal ,而不是 public:
public class Issue : AggregateRoot<Guid>
{
internal Issue(
Guid id,
Guid repositoryId,
string title,
string text = null
) : base(id)
{
//...
}
}
这阻止了应用服务直接使用构造函数,所以它们将使用 IssueManager 。然后我们可以在 IssueManager 中添加一个 CreateAsync 方法:
public class IssueManager : DomainService
{
//省略了依赖注入
public async Task<IssueDto> CreateAsync(
Guid repositoryId,
string title,
string text = null
)
{
//如果存在相同标题的问题,直接抛错
if(await _issueRepository.AnyAsync(i => i.Title == title))
{
throw new BusinessException("IssueTracking:IssueWithSameTitleExists");
}
//创建一个有效的问题实体
return new Issue(
GuidGenerator.Create(),
repositoryId,
title,
text
);
}
}
CreateAsync方法检查相同标题是否已经存在问题,并在这种情况下抛出业务异常- 如果没有重复,则创建并返回一个新的Issue
为了使用上述方法,IssueAppService 被修改如下:
public class IssueAppService : ApplicationService, IIssueAppService
{
//省略了依赖注入
public async Task<IssueDto> CreateAsync(IssueCreationDto input)
{
//★修改为通过领域服务创建有效的问题实体, 而不是直接new
var issue = await _issueManager.CreateAsync(
GuidGenerator.Create(),
input.RepositoryId,
input.Title,
input.Text
);
//如果传入了被分配人,则把该问题法分配给这个用户
if(input.AssignedUserId.HasValue)
{
var user = await _userRepository.GetAsync(input.AssignedUserId.Value);
await _issueManager.AssignToAsync(issue, user);
}
// 把问题实体保存到数据库
await _issueRepository.InsertAsync(issue);
//返回表示这个新的问题的DTO
return ObjectMapper.Map<Issue, IssueDto>(issue);
}
}
讨论:为什么问题没有在 IssueManager 中保存到数据库?
你可能会问 “为什么 IssueManager 不把问题保存到数据库中?” 我们认为这是应用服务的责任
因为,在保存问题对象之前,应用程序服务可能需要对其进行额外的更改/操作。如果领域服务保存它,则保存操作将重复
- 两次数据库往返会导致性能损失
- 需要显式的数据库事务来包含这两个操作
- 如果由于业务规则的原因,其他操作取消了实体创建,则应该在数据库中回滚事务
当你检查 IssueAppService 时,你会看到在 IssueManager.CreateAsync 中不保存 Issue 到数据库的好处。否则,我们将需要执行一次插入(在 IssueManager 中)和一次更新(在分配问题之后)
讨论:为什么不在应用程序服务中实现重复标题检查?
我们可以简单地说 “因为它是一个核心领域逻辑,应该在领域层中实现”。然而,这带来了一个新的问题: “您如何判断它是核心领域逻辑,而不是应用程序逻辑?” (稍后我们将详细讨论其中的差异)
对于这个例子,一个简单的问题可以帮助我们做出决定: “如果我们有另一种方法(用例)来创建一个问题,我们是否仍然应用相同的规则?” 你可能会想 “为什么我们有第二种制造问题的方式?” 然而,在现实生活中,你有:
- 应用程序的最终用户可能会在应用程序的标准UI中创建问题(比如在github的网页端创建问题)
- 您可能有第二个后台应用程序,由您自己的员工使用,您可能希望提供一种创建问题的方法(在本例中可能使用不同的授权规则)
- 您可能有一个对第三方客户端开放的HTTP API,他们会创建问题。
- 您可能有一个 background worker service,如果它检测到一些故障,它会做一些事情并创建问题。这样,它将在没有任何用户交互的情况下(可能没有任何标准的授权检查)创建问题。
- 您甚至可以在UI上设置一个按钮,将某些内容 (例如,讨论) 转换为问题
综上所述,不同的应用程序始终遵循这样的规则:新问题的标题不能与任何现有问题的标题相同!他们与应用层无关! 这就是为什么该逻辑是核心领域逻辑,应该位于领域层中,而不应该在应用程序服务中实现为重复的代码。
实现领域驱动设计 - 使用ABP框架 - 创建实体的更多相关文章
- 实现领域驱动设计 - 使用ABP框架 - 什么是领域驱动设计?
前言: 最近看到ABP官网的一本电子书,感觉写的很好,翻译出来,一起学习下 (Implementing Domain Driven Design) https://abp.io/books DDD简介 ...
- 实现领域驱动设计 - 使用ABP框架 - 通用准则
在进入细节之前,让我们看看一些总体的 DDD 原则 数据库提供者 / ORM 无关性 领域和应用程序层应该与 ORM / 数据库提供程序 无关.它们应该只依赖于 Repository 接口,而 Rep ...
- 实现领域驱动设计 - 使用ABP框架 - 解决方案概览
.NET解决方案的分层 下图显示了使用ABP的 应用启动模板 创建的Visual Studio解决方案: 解决方案名称为问题跟踪,它由多个项目组成.通过考虑DDD原则以及开发和部署实践,该解决方案是分 ...
- 实现领域驱动设计 - 使用ABP框架 - 存储库
存储库 Repository 是一个类似于集合的接口,领域层和应用程序层使用它来访问数据持久性系统(数据库),以读写业务对象(通常是聚合) 常见的存储库原则是: 在领域层定义一个存储库接口(因为它被用 ...
- .net core +codefirst(.net core 基础入门,适合这方面的小白阅读) 【我们一起写框架】领域驱动设计的CodeFirst框架(一)—序篇
.net core +codefirst(.net core 基础入门,适合这方面的小白阅读) 前言 .net core mvc和 .net mvc开发很相似,比如 视图-模型-控制器结构.所以. ...
- 【我们一起写框架】领域驱动设计的CodeFirst框架(一)—序篇
前言 领域驱动设计,其实已经是一个很古老的概念了,但它的复杂度依旧让学习的人头疼不已. 互联网关于领域驱动的文章有很多,每一篇写的都很好,理解领域驱动设计的人都看的懂. 不过,这些文章对于那些初学者而 ...
- DDD 领域驱动设计-“臆想”中的实体和值对象
其他博文: DDD 领域驱动设计-三个问题思考实体和值对象 DDD 领域驱动设计-三个问题思考实体和值对象(续) 以下内容属于博主"臆想",如有不当,请别当真. 扯淡开始: 诺兰的 ...
- (转)EntityFramework之领域驱动设计实践
EntityFramework之领域驱动设计实践 - 前言 EntityFramework之领域驱动设计实践 (一):从DataTable到EntityObject EntityFramework之领 ...
- EntityFramework之领域驱动设计实践
EntityFramework之领域驱动设计实践 - 前言 EntityFramework之领域驱动设计实践 (一):从DataTable到EntityObject EntityFramework之领 ...
随机推荐
- what 的页面制作
1. html结构 <!-- section: what we do --> <section id="what" class="bg-light py ...
- Apache Flink系列-④有状态函数
有状态函数:独立于平台的有状态无服务器堆栈 这是一种在现代基础设施上创建高效.可扩展且一致的应用程序的简单方法,无论规模大小. 有状态函数是一种API,它通过为无服务器架构构建的运行时简化了分 ...
- k8s入门之Ingress(七)
Ingress 的功能其实很容易理解:所谓 Ingress,就是 Service 的"Service",代理不同后端 Service 而设置的负载均衡服务. 一.安装ingress ...
- ps、top命令查找不到进程的解决方案
netstat -anpt发现一个奇怪的连接,但是ps和top命令确查不到此进程,这很可能是因为因为ps和top命令被替换了导致这些进程被过滤掉了.因此我这里有个脚本专门查找出来隐藏的进程 #!/us ...
- 记一次sql注入的解决方案
点赞再看,养成习惯,微信搜索「小大白日志」关注这个搬砖人. 本文在公众号文章已同步,还有各种一线大厂面试原题.我的学习系列笔记. 今天业务提了个模糊查询,一听就知道这种问题有坑,肯定涉及到sql注入, ...
- 2.SSH协议常见问题排错
一.SSH登录linux服务器密码验证很慢 现象:ssh登录服务器后,输入密码时,验证要等10秒左右,很慢.登录上去后速度正常,这种情况主要有两种可能的原因: 1. DNS反向解析的问题 OpenSS ...
- 一文详解 FTP、FTPS 与 SFTP 的原理
开源Linux 长按二维码加关注~ 上一篇:2020年MySQL数据库面试题总结 无论是网盘还是云存储,上传都是一项很简单的操作.那些便捷好用的上传整理工具所用的 FTP 协议到底是什么意义,繁杂的模 ...
- 【面试普通人VS高手系列】说说缓存雪崩和缓存穿透的理解,以及如何避免?
听说10个人去互联网公司面试,有9个人会被问到缓存雪崩和缓存穿透的问题. 听说,这9个人里面,至少有8个人回答得不完整. 而这8个人里面,全都是在网上找的各种面试资料去应付的,并没有真正理解. 当然, ...
- web安全之自己写一个扫描器
web安全之自己写一个扫描器 自己来写一个简单的目录扫描器,了解扫描器的运转机制和原理,因为python写脚本比较容易所以用python写一个网站目录扫描器. 第一步:我们需要导入所需要的库 1 im ...
- 关闭BottomSheetDialogFragment从后台返回后的动画
问题 显示BottomSheetDialogFragment后.将当前应用放于后台,切换到其他APP,然后再返回当前应用.此时会看到BottomSheetDialogFragment从下而上的动画再次 ...