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

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

一.领域服务实践

接下来将聚合根 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 有 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 的自动对象映射:

猜你喜欢

转载自blog.csdn.net/weixin_70730532/article/details/125910686
今日推荐