什么是畛域服务呢?畛域服务就是畛域对象自身的服务,通常是通过多个聚合以实现单个聚合无奈解决的逻辑。
一. 畛域服务实际
接下来将聚合根 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/25… (拜访明码: 2096)
[2]FluentValidation 官网文档:https://docs.fluentvalidation…
[3]FluentValidation GitHub:https://github.com/FluentVali…
[4]AutoMapper 官网文档:http://automapper.org/
[5]AutoMapper GitHub:https://github.com/AutoMapper…
本文由 mdnice 多平台公布