APB vNext 集成微服务实战 丨业务接口

前言

首先非常感谢老哥提出的问题

@落叶子
IdentityServer、用户、角色、组织 你都没用到 然后生成那么多没用的表,这点感觉不怎么好

首先回答一下上一章,大家提出的疑问,首先我本次搭建的这个项目,在安排上其实需要用到以上的东西。
但是如果说你在用的时候发现abp默认生成的很多表你不想要你该怎么办呢。

针对IdentityServer你可以移除掉改成Jwt

针对组织架构,这一点这里强调说明,在我们开发项目的时候,组织架构是需要我们根据项目要求做出调整的,在真实业务场景是需要重写的,abp提供的组织模块仅供参考。

我最近又思考了一下,我觉得上次的设计在后面会给我自己挖坑,我只是做技术教程,不想写太多业务上的代码,所以我将Entity做出了一点点的调整,将创建问答和文章的支持匿名,相应的问答就不存在采纳,而是改为赞同。

对应的也省掉了修改和删除的接口,我真是太机智了!

APB vNext 集成微服务实战 丨业务接口

开始

上一节 我们简单的创建了下Entity 和 实体映射配置,这一节来吧问答业务写一下。

创捷接口抽象和Dto

根据Abp的规范,我们先要在 Application.Contracts 层创建(Contracts翻译过来就是合约,用于约束具体业务的实现行为),接口定义和Dto

创建 Questions 文件夹 内部创建 IQuestionAppService 接口


    public  interface IQuestionAppService : IApplicationService
    {
        /// <summary>
        /// 获取所有问答
        /// </summary>
        /// <returns></returns>
        Task<ListResultDto<QuestionDto>> GetListAllAsync();

        /// <summary>
        /// 获取问答列表
        /// </summary>
        /// <param name="input"></param>
        /// <returns></returns>
        Task<PagedResultDto<QuestionDto>> GetListAsync(GetQuestionInputDto input);

        /// <summary>
        /// 获取问答详情
        /// </summary>
        /// <param name="id"></param>
        /// <returns></returns>
        Task<QuestionDetailsDto> GetAsync(Guid id);

        /// <summary>
        /// 创建问答
        /// </summary>
        /// <param name="input"></param>
        /// <returns></returns>
        Task CreateAsync(CreateQuestionInputDto input);
        
    } 

并创建 CreateQuestionInputDto、GetQuestionInputDto、QuestionDetailsDto、QuestionDto 这里因为我们没有修改所以没写修改的Dto,根据我目前的使用经验
一个Entity业务基本上对应以上 5个 Dto 就能够满足正常的CRUD业务需要。

    public class CreateQuestionInputDto
    {
        /// <summary>
        /// 标题
        /// </summary>
        public string Title { get; set; }
        /// <summary>
        /// 内容
        /// </summary>
        public string Content { get; set; }
        /// <summary>
        /// 类别
        /// </summary>
        public string Tag { get; set; }
    }
    public  class GetQuestionInputDto : PagedAndSortedResultRequestDto
    {
        public string Filter { get; set; }

    }
    public class QuestionDetailsDto : FullAuditedEntityDto<Guid>
    {
        /// <summary>
        /// 标题
        /// </summary>
        public string Title { get; set; }
        /// <summary>
        /// 内容
        /// </summary>
        public string Content { get; set; }
        /// <summary>
        /// 类别
        /// </summary>
        public string Tag { get; set; }
        /// <summary>
        /// 访问量
        /// </summary>
        public int Traffic { get; set; }

        /// <summary>
        /// 回答
        /// </summary>

        public List<QuestionCommentDto> QuestionComment = new List<QuestionCommentDto>();

    }
    public class QuestionDto : FullAuditedEntityDto<Guid>
    {
        /// <summary>
        /// 标题
        /// </summary>
        public string Title { get; set; }
        /// <summary>
        /// 内容
        /// </summary>
        public string Content { get; set; }
        /// <summary>
        /// 类别
        /// </summary>
        public string Tag { get; set; }
        /// <summary>
        /// 访问量
        /// </summary>
        public int Traffic { get; set; }
        /// <summary>
        /// 回答数量
        /// </summary>
        public int QuestionCommentsCount { get; set; }
    }

另外因为问答有个评论,我们在内部创建一个 Comments 文件夹,创建 CreateQuestionCommentInputDto、QuestionCommentDto 2个Dto 这里因为我们回答不牵扯修改和指定查询动作所以就2个Dto

    public class CreateQuestionCommentInputDto
    {
        /// <summary>
        /// 内容
        /// </summary>
        public string Content { get; set; }
    }
    public class QuestionCommentDto : FullAuditedEntityDto<Guid>
    {
        /// <summary>
        /// 内容
        /// </summary>
        public string Content { get; set; }

        /// <summary>
        /// 赞同数
        /// </summary>
        public bool ApproveOf { get; set; }
    }

完成后的结构如下图所示

APB vNext 集成微服务实战 丨业务接口

实现接口业务

在 Application 创建 QuestionAppService 继承 AbpvNextAppService 和 刚才创建的 IQuestionAppService 接口。

先列出一个我们最初级的写法,当我们开始用Abp的时候,你不会写你就算用下面的写法你的代码也是很优秀的,因为它可以随着你对Abp的深入了解,非常简单的就能改造完成,
用我经常说的一句话,你已经在这么优秀的框架上进行开发了,你写的代码在烂又能烂到哪去。

 public class QuestionAppService: AbpvNextAppService, IQuestionAppService
    {
        protected readonly IRepository<Question> _questionsRepository;
        protected readonly IRepository<QuestionComment> _questionCommentsRepository;

        public QuestionAppService(IRepository<Question> questionsRepository, IRepository<QuestionComment> questionCommentsRepository)
        {
            _questionsRepository = questionsRepository;
            _questionCommentsRepository = questionCommentsRepository;
        }

        /// <summary>
        /// 获取所有问答
        /// </summary>
        /// <returns></returns>
        public async Task<ListResultDto<QuestionDto>> GetListAllAsync()
        {
            var entityList =  await  _questionsRepository.GetListAsync();

            return new ListResultDto<QuestionDto>(ObjectMapper.Map<List<Question>, List<QuestionDto>>(entityList));

        }


        /// <summary>
        /// 获取问答列表
        /// </summary>
        /// <param name="input"></param>
        /// <returns></returns>
        public async Task<PagedResultDto<QuestionDto>> GetListAsync(GetQuestionInputDto input)
        {
            // .WhereIf(!input.Filter.IsNullOrEmpty(), x => x.Title.Contains(input.Filter) || x.Content.Contains(input.Filter))
            var entityList = await _questionsRepository.GetPagedListAsync(input.SkipCount, input.MaxResultCount, input.Sorting);

            var result = ObjectMapper.Map<List<Question>, List<QuestionDto>>(entityList);

            return new PagedResultDto<QuestionDto>(100, result);
        }


        /// <summary>
        /// 获取问答详情
        /// </summary>
        /// <param name="id"></param>
        /// <returns></returns>
        public async Task<QuestionDetailsDto> GetAsync(Guid id)
        {
           var entity = await _questionsRepository.FindAsync(x=>x.Id == id);
           return ObjectMapper.Map<Question, QuestionDetailsDto>(entity);
        }

        /// <summary>
        /// 创建问答
        /// </summary>
        /// <param name="input"></param>
        /// <returns></returns>
        public async Task CreateAsync(CreateQuestionInputDto input)
        {
            var  entity = ObjectMapper.Map<CreateQuestionInputDto, Question>(input);
            await _questionsRepository.InsertAsync(entity);
        }
    }

改造

上面的代码你会看到我把筛选条件给注释了,因为Abp提供的GetPagedListAsync 并不是基于IQueryable而是封装的仓储方法,当然你也可以通过_questionsRepository.WithDetailsAsync()来写linq 完成,但是Abp提供了另一种方案自定义仓储

自定义仓储

在领域层的问答文件夹新建仓储接口 IQuestionRepository,这里传递一些开发经验Abp本身的那个PageList有个非常不好的点,它官方的写法是通过调用2次接口一次返回数据,一次返回Count,对于这么简单的类型返回其实用元组也可以,但是我比较偏向于定于类型所以我封装了一个EntityPageCount来数据,另外根据经验我的仓储都会出现这3个接口,如果业务需要做字段验证我会外加一个 ExistsAsync,如果还需要根据某某编码或者特定字段查询我会加一个 GetByXXXAsync。

 public  interface  IQuestionRepository : IBasicRepository<Question, Guid>
    {
        Task<EntityPageCount<Question>> GetListAsync(string filter,
            bool includeDetails = true, string sorting = null,
            int maxResultCount = Int32.MaxValue, int skipCount = 0, CancellationToken cancellationToken = default);

        Task<Question> GetByIdAsync(Guid id, bool includeDetails = true,
            CancellationToken cancellationToken = default);


        Task<List<Question>> GetListByIdsAsync(List<Guid> ids, bool includeDetails = true,
            CancellationToken cancellationToken = default);

    }

在持久化层 增加 EFCoreRepository 文件夹实现自定义仓储接口,这里有2点经验我说一下,首先是ApplyFilterForGetAll方法该方法应做到所有参数都可以传递null,方便提供给其他方法进行调用,这个可能需要一个复杂业务才能体会到。第二点就是 IncludeDetails 我个人不推荐在使用EF的时候开启懒加载,所以我会扩展一个IncludeDetails方法,通过传递参数来选择是否加载子表
(什么你问我多个子表怎么办?你不会吧bool改成Flag枚举嘛)

    public class EfCoreQuestionRepository : EfCoreRepository<AbpvNextDbContext, Question, Guid>, IQuestionRepository
    {
        public EfCoreQuestionRepository(IDbContextProvider<AbpvNextDbContext> dbContextProvider) : base(dbContextProvider)
        {
        }

        public async Task<EntityPageCount<Question>> GetListAsync(string filter, bool includeDetails = true,
            string sorting = null, int maxResultCount = Int32.MaxValue, int skipCount = 0,
            CancellationToken cancellationToken = default)
        {
            IQueryable<Question> query = ApplyFilterForGetAll(await GetDbSetAsync(), filter);
            query = query.OrderBy(string.IsNullOrWhiteSpace(sorting) ? nameof(Question.CreationTime) : sorting).AsNoTracking();
            var count = query.Count();
            var entityList = await query.IncludeDetails(includeDetails).PageBy(skipCount, maxResultCount).ToListAsync(cancellationToken);
            return new EntityPageCount<Question>(count, entityList);
        }

        public async Task<Question> GetByIdAsync(Guid id, bool includeDetails = true, CancellationToken cancellationToken = default)
        {
            return await(await GetDbSetAsync()).Where(x => x.Id == id).IncludeDetails(includeDetails).FirstOrDefaultAsync(GetCancellationToken(cancellationToken));
        }

        public async Task<List<Question>> GetListByIdsAsync(List<Guid> ids, bool includeDetails = true, CancellationToken cancellationToken = default)
        {
            return await(await GetDbSetAsync()).Where(x => ids.Contains(x.Id)).IncludeDetails(includeDetails).ToListAsync(GetCancellationToken(cancellationToken));
        }

        protected virtual IQueryable<Question> ApplyFilterForGetAll(IQueryable<Question> query, string filter)
        {
            return query.Include(x => x.QuestionComments)
                .WhereIf(!filter.IsNullOrEmpty(), x => x.Title.Contains(filter) || x.Content.Contains(filter));
        }
    }

采用自定义仓储改完之后接口

public class QuestionAppService: AbpvNextAppService, IQuestionAppService
    {
        protected readonly IQuestionRepository _questionsRepository;

        public QuestionAppService(IQuestionRepository questionsRepository)
        {
            _questionsRepository = questionsRepository;
        }

        /// <summary>
        /// 获取所有问答
        /// </summary>
        /// <returns></returns>
        public async Task<ListResultDto<QuestionDto>> GetListAllAsync()
        {
            var entityList =  await  _questionsRepository.GetListAsync();

            return new ListResultDto<QuestionDto>(ObjectMapper.Map<List<Question>, List<QuestionDto>>(entityList));

        }


        /// <summary>
        /// 获取问答列表
        /// </summary>
        /// <param name="input"></param>
        /// <returns></returns>
        public async Task<PagedResultDto<QuestionDto>> GetListAsync(GetQuestionInputDto input)
        {
            var entityList = await _questionsRepository.GetListAsync(input.Filter, false, input.Sorting, input.MaxResultCount, input.SkipCount);

            var result = ObjectMapper.Map<List<Question>, List<QuestionDto>>(entityList.EntityList);

            return new PagedResultDto<QuestionDto>(entityList.Count, result);
        }


        /// <summary>
        /// 获取问答详情
        /// </summary>
        /// <param name="id"></param>
        /// <returns></returns>
        public async Task<QuestionDetailsDto> GetAsync(Guid id)
        {
           var entity = await _questionsRepository.FindAsync(id);
           return ObjectMapper.Map<Question, QuestionDetailsDto>(entity);
        }

        /// <summary>
        /// 创建问答
        /// </summary>
        /// <param name="input"></param>
        /// <returns></returns>
        public async Task CreateAsync(CreateQuestionInputDto input)
        {
            var  entity = ObjectMapper.Map<CreateQuestionInputDto, Question>(input);
            await _questionsRepository.InsertAsync(entity);
        }
    }

AutoMap映射

接口改完了,接下来需要把数据映射搞一下在 Application 层有一个 xxxApplicationAutoMapperProfile类

    public class AbpvNextApplicationAutoMapperProfile : Profile
    {
        public AbpvNextApplicationAutoMapperProfile()
        {
            /* You can configure your AutoMapper mapping configuration here.
             * Alternatively, you can split your mapping configurations
             * into multiple profile classes for a better organization. */

            // 问答
            CreateMap<Question, QuestionDto>();
            CreateMap<Question, QuestionDetailsDto>();

        }
    }

结语

本节知识点:
1.业务接口
2.泛型仓储介绍
3.如何自定义仓储
4.配置AutoMap映射

就先讲到这里吧,运行代码你会发现创建接口报错,因为我们没有加AutoMap映射,这里不写主要是偷懒不想写多余的代码,因为下一节我们讲DDD实践,现在这样写代码太糙了。

请各位一起指出文章中错误的讲解点 谢谢,如果你也在使用Abp有好的设计思路和想法希望你能共享给我,我们一起交流 加油!

该项目存放仓库在 https://github.com/BaseCoreVueProject/Blog.Core.AbpvNext

联系作者:加群:867095512 @MrChuJiu

APB vNext 集成微服务实战 丨业务接口

上一篇:【Abp VNext】实战入门基本操作 —— 如何修改用户账号密码及其他信息


下一篇:[.Net]使用Soa库+Abp搭建微服务项目框架(四):微服务原理