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


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

    一.领域服务实践

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

    // ABP当中的领域服务类通常都是以Manager结尾的
    public class IssueManager : DomainService
    {
        private readonly IRepository _issueRepository;
        
        // 在构造函数中注入需要的仓储
        public IssueManager(IRepository 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 _issueRepository;
            private readonly IRepository _userRepository;
            public IssueAppService(
                IssueManager issueManager,
                IRepository issueRepository,
                IRepository 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 GetUserNameAndEmail(Guid id);
        List GetRoles(Guid id);
        List GetList();
        UserCreateResultDto Create(UserCreationDto input);
        UserUpdateResultDto Update(UserUpdateDto input);
    }
    

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

    public interface IUserAppService:IApplicationService
    {
        UserDto Get(Guid id);
        List 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

  • 相关阅读:
    机器学习Sklearn的k近邻算法api初步使用
    Ubuntu MongoDB账户密码设置
    最全最小系统板原理图设计实操(涉及电容、电阻选型)
    网络参考模型与标准协议(一)
    Springboot毕设项目共享电单车管理系统3447i(java+VUE+Mybatis+Maven+Mysql)
    网络安全渗透测试工具AWVS14.6.2的安装与使用(激活)
    基于transformer的心脑血管心脏病疾病预测
    异步FIFO设计的仿真与综合技术(2)
    【布局优化】基于帝国企鹅算法求解潮流计算的电力系统总线优化问题附matlab代码
    设计模式之原型模式(Prototype)
  • 原文地址:https://www.cnblogs.com/shengshengwang/p/16500644.html