什么是领域服务呢?领域服务就是领域对象本身的服务,通常是通过多个聚合以实现单个聚合无法处理的逻辑。

一.领域服务实践

接下来将聚合根Issue中的AssignToAsync()方法[将问题分配给用户],剥离到领域服务当中。如下:

// ABP当中的领域服务类通常都是以Manager结尾的
public class IssueManager : DomainService
{
private readonly IRepository<Issue,Guid> _issueRepository; // 在构造函数中注入需要的仓储
public IssueManager(IRepository<Issue,Guid> issueRepository)
{
_issueRepository = issueRepository;
} public async Task AssignToAsync(Issue issue, AppUser user)
{
// 通过仓储获取分配给该用户的,并且没有关闭的Issue的数量
var openIssueCount = await _issueRepository.CountAsync(i => i.AssignedUserId == user.id && !i.IsClosed); // 如果超过3个,那么抛出异常
if (openIssueCount > 3)
{
throw new BusinessException("IssueTracking:ConcurrentOpenIssueLimit");
} issue.AssignedUserId = user.Id;
}
}

需要说明的是通常不需要为领域服务IssueManager在创建一个接口IIssueManager。

二.应用服务实践

应用服务的输入和输出通常都是DTO,其中的难点是区分领域逻辑和应用逻辑,即哪些服务放在领域层实现,哪些服务放在应用层来实现。

namespace IssueTracking.Issues
{
public class IssueAppService :ApplicationService.IIssueAppService
{
private readonly IssueManager _issueManager;
private readonly IRepository<Issue,Guid> _issueRepository;
private readonly IRepository<AppUser,Guid> _userRepository;
public IssueAppService(
IssueManager issueManager,
IRepository<Issue,Guid> issueRepository,
IRepository<AppUser,Guid> userRepository
)
{
_issueManager=issueManager;
_issueRepository=issueRepository;
_userRepository=userRepository;
} [Authorize]
public async Task AssignAsync(IssueAssignDto input)
{
var issue=await _issueRepository.GetAsync(input.IssueId);
var user=await _userRepository.GetAsync(inpu.UserId);
await _issueManager.AssignToAsync(issue,user);
await _issueRepository.UpdateAsync(issue);
}
}
}

在上述代码中,为什么最后执行_issueRepository.UpdateAsync(issue)呢?其中有2层含义,第1层是Issue通过_issueManager.AssignToAsync(issue,user)发生了变化,需要进行更新操作(从下图可知Issue聚合根中包含AssignedUserId字段);第2层是EF Core中有状态变更跟踪,Update并不是必须的,但是还是建议显式调用Update,用来适配其它的数据库提供程序。

三.数据传输对象DTO实践

DTO的本质是在应用层和展示层传递状态数据,通常应用层的输入和输出都是DTO,这样做的最大好处就是不暴露实体的结构设计。

1.输入DTO实践

(1)不要重用输入DTO

不使用的属性不要定义在输入DTO中;不要重用输入DTO有2种方式:一种方式是为每个应用服务方法定义特定的输入DTO,另一种方式是不要使用DTO继承。下面是错误的输入DTO实践,理由详见注释:

public interface IUserAppService : IApplicationService
{
Task CreateAsync(UserDto input); //Id在该方法中没有用到
Task UpdateAsync(UserDto input); // Password在该方法中没有用到
Task ChangePasswordAsync(UserDto input); // CreationTime在该方法中没有用到
}
public class UserDto
{
public Guid Id { get; set; }
public string UserName { get; set; }
public string Email { get; set; }
public string Password { get; set; }
public DateTime CreateTime { get; set; }
}

下面是正确的输入DTO实践:

public interface IUserAppService : IApplicationService
{
Task CreateAsync(UserCreationDto input);
Task UpdateAsync(UserUpdateDto input);
Task ChangePasswordAsync(UserChangePasswordDto input);
}
public class UserCreationDto
{
public string UserName { get; set; }
public string Email { get; set; }
public string Password { get; set; }
}
public class UserUpdateDto
{
public Guid Id { get; set; }
public string UserName { get; set; }
public string Email { get; set; }
}
public class UserChangePasswordDto
{
public Guid Id { get; set; }
public string Password { get; set; }
}

(2)输入DTO中的验证逻辑

主要是在DTO内部通过数据注解特性、FluentValidation,或者实现IValidatableObject接口等方式来执行简单的验证。需要注意的是不要在DTO中执行领域验证,比如检测用户名是否唯一的验证等。下面在输入DTO中使用数据注解特性:

namespace IssueTracking.Users
{
public class UserCreationDto
{
[Required]
[StringLength(UserConsts.MaxUserNameLength)]
public string UserName {get;set;}
[Required]
[EmailAddress]
[StringLength(UserConsts.MaxEmailLength)]
public string Email{get;set;}
[Required]
[StringLength(UserConsts.MaxEmailLength,MinimumLength=UserConsts.MinPasswordLength)]
public string Password{get;set;}
}
}

ABP会自动验证输入DTO中的注解,如果验证失败,那么抛出AbpValidationException异常,并且返回400状态码。个人建议使用FluentValidation方式进行验证,而不是声明式的数据注解,这样做的优点是将验证规则和DTO类彻底分离开

2.输出DTO实践

输出DTO最佳实践:主要是尽可能的复用输出DTO,但是切记不能把输入DTO作为输出DTO;输出DTO可以包含更多的属性;Create和Update方法返回DTO。下面是错误的输出DTO实践:

public interface IUserAppService:IApplicationService
{
UserDto Get(Guid id);
List<UserNameAndEmailDto> GetUserNameAndEmail(Guid id);
List<string> GetRoles(Guid id);
List<UserListDto> GetList();
UserCreateResultDto Create(UserCreationDto input);
UserUpdateResultDto Update(UserUpdateDto input);
}

下面是正确的输出DTO实践:

public interface IUserAppService:IApplicationService
{
UserDto Get(Guid id);
List<UserDto> GetList();
UserDto Create(UserCreationDto input);
UserDto Update(UserUpdateDto input);
}
public class UserDto
{
public Guid Id{get;set;}
public string UserName{get;set;}
public string Email{get;set;}
public DateTiem CreationTime{get;set;}
public List<string> Roles{get;set;}
}

说明:删除GetUserNameAndEmail()和GetRoles()方法,因为它们与Get()方法重复了,即它们的功能都可以通过Get()方法来实现。

3.对象映射工具

  为什么需要对象映射工具呢?由于实体和DTO具有相同或者相似的属性,如果手工处理实体和DTO间的转换,那么效率是非常低的,因此需要对象映射工具高效的完成实体和DTO间的转换。

  在ABP中使用的对象映射框架是AutoMapper,官方的建议是:仅对实体到输出DTO做自动对象映射,不建议输入DTO到实体做自动对象映射。因为DTO是实体的部分或者全部字段,自己推测前者是比较确定的,而由于复杂的业务规则让后者的映射充满了不确定性。具体为什么不使用输入DTO到实体做自动对象映射的原因参考[1]。

自动对象映射在应用服务层中实现,该类需要继承自Profile类:



虽然官方不建议输入DTO到实体做自动对象映射,但是在通常的实践中还是较多使用CreateOrUpdateXXXDto到实体XXX的自动对象映射:



关于FluentValidation和AutoMapper这2个库就不单独在这里展开讲了,后面单独文章进行讲解操作和原理。

参考文献:

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

[2]FluentValidation官方文档:https://docs.fluentvalidation.net/en/latest/

[3]FluentValidation GitHub:https://github.com/FluentValidation/FluentValidation/blob/main/docs/index.rst

[4]AutoMapper官方文档:http://automapper.org/

[5]AutoMapper GitHub:https://github.com/AutoMapper/AutoMapper

基于ABP实现DDD--领域服务、应用服务和DTO实践的更多相关文章

  1. 基于事件驱动的DDD领域驱动设计框架分享(附源代码)

    原文:基于事件驱动的DDD领域驱动设计框架分享(附源代码) 补充:现在再回过头来看这篇文章,感觉当初自己偏激了,呵呵.不过没有以前的我,怎么会有现在的我和现在的enode框架呢?发现自己进步了真好! ...

  2. ABP理论学习之领域服务

    返回总目录 本篇目录 介绍 IDomainService接口和DomainService类 样例 创建一个接口 服务实现 调用应用服务 一些讨论 何不只使用应用服务 如何强制使用领域服务 介绍 领域服 ...

  3. 如何运用DDD - 领域服务

    目录 如何运用DDD - 领域服务 概述 什么是领域服务 从实际场景下手 更贴近现实 领域服务VS应用服务 扩展上面的需求 最常见的认证授权是领域服务吗 使用领域服务 不要过多的使用领域服务 不要将过 ...

  4. DDD~领域服务的规约模式

    回到目录 规 约(Specification)模式:第一次看到这东西是在microsoft NLayer项目中,它是微软对DDD的解说,就像petshop告诉了我们MVC如何使用一样,这个规约模式最重 ...

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

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

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

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

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

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

  8. 基于ABP落地领域驱动设计-06.正确区分领域逻辑和应用逻辑

    目录 系列文章 领域逻辑和应用逻辑 多应用层 示例:正确区分应用逻辑和领域逻辑 学习帮助 系列文章 基于ABP落地领域驱动设计-00.目录和前言 基于ABP落地领域驱动设计-01.全景图 基于ABP落 ...

  9. ABP入门系列(18)—— 使用领域服务

    ABP入门系列目录--学习Abp框架之实操演练 源码路径:Github-LearningMpaAbp 1.引言 自上次更新有一个多月了,发现越往下写,越不知如何去写.特别是当遇到DDD中一些概念术语的 ...

随机推荐

  1. socket编程实现tcp服务器_C/C++

    1. 需求分析 实现一个回声服务器的C/S(客户端client/服务器server)程序,功能为客户端连接到服务器后,发送一串字符串,服务器接受信息后,返回对应字符串的大写形式给客户端显示. 例如: ...

  2. python爬取豆瓣电影Top250(附完整源代码)

    初学爬虫,学习一下三方库的使用以及简单静态网页的分析.就跟着视频写了一个爬取豆瓣Top250排行榜的爬虫. 网页分析 我个人感觉写爬虫最重要的就是分析网页,找到网页的规律,找到自己需要内容所在的地方, ...

  3. 攻防世界web进阶题—bug

    攻防世界web进阶题-bug 1.打开题目看一下源码,没有问题 2.扫一下目录,没有问题 3.查一下网站的组成:php+Apache+Ubuntu 只有登录界面 这里可以可以想到:爆破.万能密码.进行 ...

  4. java高级用法之:JNA中的回调

    目录 简介 JNA中的Callback callback的应用 callback的定义 callback的获取和应用 在多线程环境中使用callback 总结 简介 什么是callback呢?简单点说 ...

  5. windbg的时间旅行实现对 C# 程序的终极调试

    一:什么是时间旅行 简而言之就是把程序的执行流拍成vlog,这样就可以对 vlog 快进或者倒退,还可以分享给别人做进一步的分析,是不是想都不敢想. 很开心的是 windbg preview 版本中已 ...

  6. 【多线程】JUC版的CopyOnWriteArrayList

    CopyOnWriteArrayList CopyOnWriteArrayList适合于多线程场景下使用,其采用读写分离的思想,读操作不上锁,写操作上锁,且写操作效率较低. CopyOnWriteAr ...

  7. 今天才发现ThinkPad的触摸板简直好用!傻乎乎的自己

    最近经常看到这句话,天下武功,唯快不破. 今天才发现ThinkPad的触摸板简直好用! 今天才发现,傻乎乎的自己. 前一阵鼠标不好用了,没有滚轮,一直傻瓜式操作点击浏览器的滚动条... 遇到没有滚动条 ...

  8. 个人冲刺(四阶段)——体温上报app(一阶段)

    任务:完成了后台数据库的类模块 MyDBHelper.java package com.example.helloworld; import android.content.Context; impo ...

  9. sqlserver2008 数据库中查询存储过程的的创建修改和执行时间,以及比较常见的系统视图和存储过程

    因为各种原因数据库中存在大量无用的存储过程,想查询存储过程的最后执行情况,处理长期不使用的存储过程 下面这条语句可以查询存储过程创建 修改和执行的最后时间: SELECT a.name AS 存储过程 ...

  10. HDD线上沙龙·创新开发专场:多元服务融合,助力应用创新开发

    5月24日,由华为开发者联盟主办的HUAWEI Developer Day(华为开发者日,简称HDD)线上沙龙·创新开发专场在华为开发者学堂及各大直播平台与广大开发者见面.直播内容主要聚焦Harmon ...