六、Abp vNext 基础篇丨文章聚合功能上

介绍

9月开篇讲,前面几章群里已经有几个小伙伴跟着做了一遍了,遇到的问题和疑惑也都在群里反馈和解决好了,9月咱们保持保持更新。争取10月份更新完基础篇。

另外番外篇属于 我在abp群里和日常开发的问题记录,如果各位在使用abp的过程中发现什么问题也可以及时反馈给我。

上一章已经把所有实体的迁移都做好了,这一章我们进入到文章聚合,文章聚合涉及接口比较多。

开工

先来看下需要定义那些应用层接口,Dto我也在下面定义好了,关于里面的BlogUserDto这个是作者目前打算采用ABP Identtiy中的User来做到时候通过权限控制,另外就是TagDto属于Posts领域的Dto.

六、Abp vNext 基础篇丨文章聚合功能上

    public interface IPostAppService : IApplicationService
    {
        Task<ListResultDto<PostWithDetailsDto>> GetListByBlogIdAndTagName(Guid blogId, string tagName);

        Task<ListResultDto<PostWithDetailsDto>> GetTimeOrderedListAsync(Guid blogId);

        Task<PostWithDetailsDto> GetForReadingAsync(GetPostInput input);

        Task<PostWithDetailsDto> GetAsync(Guid id);

        Task DeleteAsync(Guid id);

        Task<PostWithDetailsDto> CreateAsync(CreatePostDto input);

        Task<PostWithDetailsDto> UpdateAsync(Guid id, UpdatePostDto input);
    }



    public class BlogUserDto : EntityDto<Guid>
    {
        public Guid? TenantId { get; set; }

        public string UserName { get; set; }

        public string Email { get; set; }

        public bool EmailConfirmed { get; set; }

        public string PhoneNumber { get; set; }

        public bool PhoneNumberConfirmed { get; set; }

        public Dictionary<string, object> ExtraProperties { get; set; }
    }




    public class CreatePostDto
    {
        public Guid BlogId { get; set; }

        [Required]
        [DynamicStringLength(typeof(PostConsts), nameof(PostConsts.MaxTitleLength))]
        public string Title { get; set; }

        [Required]
        public string CoverImage { get; set; }

        [Required]
        [DynamicStringLength(typeof(PostConsts), nameof(PostConsts.MaxUrlLength))]
        public string Url { get; set; }

        [Required]
        [DynamicStringLength(typeof(PostConsts), nameof(PostConsts.MaxContentLength))]
        public string Content { get; set; }

        public string Tags { get; set; }

        [DynamicStringLength(typeof(PostConsts), nameof(PostConsts.MaxDescriptionLength))]
        public string Description { get; set; }

    }



    public class GetPostInput
    {
        [Required]
        public string Url { get; set; }

        public Guid BlogId { get; set; }
    }



    public class UpdatePostDto
    {
        public Guid BlogId { get; set; }

        [Required]
        public string Title { get; set; }

        [Required]
        public string CoverImage { get; set; }

        [Required]
        public string Url { get; set; }

        [Required]
        public string Content { get; set; }

        public string Description { get; set; }

        public string Tags { get; set; }
    }



    public class PostWithDetailsDto : FullAuditedEntityDto<Guid>
    {
        public Guid BlogId { get; set; }

        public string Title { get; set; }

        public string CoverImage { get; set; }

        public string Url { get; set; }

        public string Content { get; set; }

        public string Description { get; set; }

        public int ReadCount { get; set; }

        public int CommentCount { get; set; }

        [CanBeNull]
        public BlogUserDto Writer { get; set; }

        public List<TagDto> Tags { get; set; }
    }




    public class TagDto : FullAuditedEntityDto<Guid>
    {
        public string Name { get; set; }

        public string Description { get; set; }

        public int UsageCount { get; set; }
    }


根据上上面的接口我想,就应该明白ABP自带的仓储无法满足我们业务需求,我们需要自定义仓储,在大多数场景下我们不会采用ABP提供的泛型仓储,除非业务足够简单泛型仓储完全满足(个人意见)。

另外我们重写了WithDetailsAsync通过扩展IncludeDetails方法实现Include包含?集合对象,其实这个也可以作为可选参数我们可以在使用ABP提供的泛型仓储GetAsync方法中看到他有一个可选参数includeDetails,来指明查询是否包含?集合对象。

    public interface IPostRepository : IBasicRepository<Post, Guid>
    {
        Task<List<Post>> GetPostsByBlogId(Guid id, CancellationToken cancellationToken = default);

        Task<bool> IsPostUrlInUseAsync(Guid blogId, string url, Guid? excludingPostId = null, CancellationToken cancellationToken = default);

        Task<Post> GetPostByUrl(Guid blogId, string url, CancellationToken cancellationToken = default);

        Task<List<Post>> GetOrderedList(Guid blogId, bool descending = false, CancellationToken cancellationToken = default);
    }




  public class EfCorePostRepository : EfCoreRepository<CoreDbContext, Post, Guid>, IPostRepository
    {
        public EfCorePostRepository(IDbContextProvider<CoreDbContext> dbContextProvider)
            : base(dbContextProvider)
        {

        }

        public async Task<List<Post>> GetPostsByBlogId(Guid id, CancellationToken cancellationToken = default)
        {
            return await (await GetDbSetAsync()).Where(p => p.BlogId == id).OrderByDescending(p => p.CreationTime).ToListAsync(GetCancellationToken(cancellationToken));
        }

        public async Task<bool> IsPostUrlInUseAsync(Guid blogId, string url, Guid? excludingPostId = null, CancellationToken cancellationToken = default)
        {
            var query = (await GetDbSetAsync()).Where(p => blogId == p.BlogId && p.Url == url);

            if (excludingPostId != null)
            {
                query = query.Where(p => excludingPostId != p.Id);
            }

            return await query.AnyAsync(GetCancellationToken(cancellationToken));
        }

        public async Task<Post> GetPostByUrl(Guid blogId, string url, CancellationToken cancellationToken = default)
        {
            var post = await (await GetDbSetAsync()).FirstOrDefaultAsync(p => p.BlogId == blogId && p.Url == url, GetCancellationToken(cancellationToken));

            if (post == null)
            {
                throw new EntityNotFoundException(typeof(Post), nameof(post));
            }

            return post;
        }

        public async Task<List<Post>> GetOrderedList(Guid blogId, bool descending = false, CancellationToken cancellationToken = default)
        {
            if (!descending)
            {
                return await (await GetDbSetAsync()).Where(x => x.BlogId == blogId).OrderByDescending(x => x.CreationTime).ToListAsync(GetCancellationToken(cancellationToken));
            }
            else
            {
                return await (await GetDbSetAsync()).Where(x => x.BlogId == blogId).OrderBy(x => x.CreationTime).ToListAsync(GetCancellationToken(cancellationToken));
            }

        }

        public override async Task<IQueryable<Post>> WithDetailsAsync()
        {
            return (await GetQueryableAsync()).IncludeDetails();
        }
    }




    public static class CoreEntityFrameworkCoreQueryableExtensions
    {
        public static IQueryable<Post> IncludeDetails(this IQueryable<Post> queryable, bool include = true)
        {
            if (!include)
            {
                return queryable;
            }

            return queryable
                .Include(x => x.Tags);
        }
    }


应用层

新建PostAppService继承IPostAppService然后开始第一个方法GetListByBlogIdAndTagName该方法根据blogId 和 tagName 查询相关的文章数据。我们有IPostRepositoryGetPostsByBlogId方法可以根据blogId获取文章,那么如何在根据tagName筛选呢,这里就需要我们新增一个ITagRepository,先不着急先实现先把业务逻辑跑通。

 public interface ITagRepository : IBasicRepository<Tag, Guid>
    {

        Task<List<Tag>> GetListAsync(Guid blogId, CancellationToken cancellationToken = default);

        Task<Tag> FindByNameAsync(Guid blogId, string name, CancellationToken cancellationToken = default);

        Task<List<Tag>> GetListAsync(IEnumerable<Guid> ids, CancellationToken cancellationToken = default);

    }

现在进行下一步,文章已经查询出来了,文章上的作者和Tag还没处理,下面代码我写了注释代码意思应该都能看明白,这里可能会比较疑问的事这样写代码for循环去跑数据库是不是不太合理,因为Tags这个本身就不会存在很多数据,这块如果要调整其实完全可以讲TagName存在Tasg值对象中。

   public async Task<ListResultDto<PostWithDetailsDto>> GetListByBlogIdAndTagName(Guid id, string tagName)
        {
            // 根据blogId查询文章数据
            var posts = await _postRepository.GetPostsByBlogId(id);
            var postDtos = new List<PostWithDetailsDto>(ObjectMapper.Map<List<Post>, List<PostWithDetailsDto>>(posts));

            // 根据tagName筛选tag
            var tag = tagName.IsNullOrWhiteSpace() ? null : await _tagRepository.FindByNameAsync(id, tagName);

            // 给文章Tags赋值
            foreach (var postDto in postDtos)
            {
                postDto.Tags = await GetTagsOfPost(postDto.Id);
            }

            // 筛选掉不符合要求的文章
            if (tag != null)
            {
                postDtos = await FilterPostsByTag(postDtos, tag);
            }

        }

        private async Task<List<TagDto>> GetTagsOfPost(Guid id)
        {
            var tagIds = (await _postRepository.GetAsync(id)).Tags;

            var tags = await _tagRepository.GetListAsync(tagIds.Select(t => t.TagId));

            return ObjectMapper.Map<List<Tag>, List<TagDto>>(tags);
        }

        private Task<List<PostWithDetailsDto>> FilterPostsByTag(IEnumerable<PostWithDetailsDto> allPostDtos, Tag tag)
        {
            var filteredPostDtos = allPostDtos.Where(p => p.Tags?.Any(t => t.Id == tag.Id) ?? false).ToList();

            return Task.FromResult(filteredPostDtos);
        }


继续向下就是赋值作者信息,对应上面Tasg最多十几个,但是系统有多少用户就不好说了所以这里使用userDictionary就是省掉重复查询数据。

 public async Task<ListResultDto<PostWithDetailsDto>> GetListByBlogIdAndTagName(Guid id, string tagName)
        {

            // 前面的代码就不重复粘贴了

            var userDictionary = new Dictionary<Guid, BlogUserDto>();
            // 赋值作者信息
            foreach (var postDto in postDtos)
            {
                if (postDto.CreatorId.HasValue)
                {
                    if (!userDictionary.ContainsKey(postDto.CreatorId.Value))
                    {
                        var creatorUser = await UserLookupService.FindByIdAsync(postDto.CreatorId.Value);
                        if (creatorUser != null)
                        {
                            userDictionary[creatorUser.Id] = ObjectMapper.Map<BlogUser, BlogUserDto>(creatorUser);
                        }
                    }

                    if (userDictionary.ContainsKey(postDto.CreatorId.Value))
                    {
                        postDto.Writer = userDictionary[(Guid)postDto.CreatorId];
                    }
                }
            }

            return new ListResultDto<PostWithDetailsDto>(postDtos);

        }

目前删除和修改接口做不了因为这里牵扯评论的部分操作,除去这两个,其他的接口直接看代码应该都没有什么问题,这一章的东西已经很多了剩下的我们下集。

 public async Task<ListResultDto<PostWithDetailsDto>> GetListByBlogIdAndTagName(Guid id, string tagName)
        {
            // 根据blogId查询文章数据
            var posts = await _postRepository.GetPostsByBlogId(id);
            // 根据tagName筛选tag
            var tag = tagName.IsNullOrWhiteSpace() ? null : await _tagRepository.FindByNameAsync(id, tagName);
            var userDictionary = new Dictionary<Guid, BlogUserDto>();
            var postDtos = new List<PostWithDetailsDto>(ObjectMapper.Map<List<Post>, List<PostWithDetailsDto>>(posts));

            // 给文章Tags赋值
            foreach (var postDto in postDtos)
            {
                postDto.Tags = await GetTagsOfPost(postDto.Id);
            }
            // 筛选掉不符合要求的文章
            if (tag != null)
            {
                postDtos = await FilterPostsByTag(postDtos, tag);
            }

            // 赋值作者信息
            foreach (var postDto in postDtos)
            {
                if (postDto.CreatorId.HasValue)
                {
                    if (!userDictionary.ContainsKey(postDto.CreatorId.Value))
                    {
                        var creatorUser = await UserLookupService.FindByIdAsync(postDto.CreatorId.Value);
                        if (creatorUser != null)
                        {
                            userDictionary[creatorUser.Id] = ObjectMapper.Map<IdentityUser, BlogUserDto>(creatorUser);
                        }
                    }

                    if (userDictionary.ContainsKey(postDto.CreatorId.Value))
                    {
                        postDto.Writer = userDictionary[(Guid)postDto.CreatorId];
                    }
                }
            }

            return new ListResultDto<PostWithDetailsDto>(postDtos);

        }

        public async Task<ListResultDto<PostWithDetailsDto>> GetTimeOrderedListAsync(Guid blogId)
        {
            var posts = await _postRepository.GetOrderedList(blogId);

            var postsWithDetails = ObjectMapper.Map<List<Post>, List<PostWithDetailsDto>>(posts);

            foreach (var post in postsWithDetails)
            {
                if (post.CreatorId.HasValue)
                {
                    var creatorUser = await UserLookupService.FindByIdAsync(post.CreatorId.Value);
                    if (creatorUser != null)
                    {
                        post.Writer = ObjectMapper.Map<IdentityUser, BlogUserDto>(creatorUser);
                    }
                }
            }

            return new ListResultDto<PostWithDetailsDto>(postsWithDetails);

        }

        public async Task<PostWithDetailsDto> GetForReadingAsync(GetPostInput input)
        {
            var post = await _postRepository.GetPostByUrl(input.BlogId, input.Url);

            post.IncreaseReadCount();

            var postDto = ObjectMapper.Map<Post, PostWithDetailsDto>(post);

            postDto.Tags = await GetTagsOfPost(postDto.Id);

            if (postDto.CreatorId.HasValue)
            {
                var creatorUser = await UserLookupService.FindByIdAsync(postDto.CreatorId.Value);

                postDto.Writer = ObjectMapper.Map<IdentityUser, BlogUserDto>(creatorUser);
            }

            return postDto;
        }

        public async Task<PostWithDetailsDto> GetAsync(Guid id)
        {
            var post = await _postRepository.GetAsync(id);

            var postDto = ObjectMapper.Map<Post, PostWithDetailsDto>(post);

            postDto.Tags = await GetTagsOfPost(postDto.Id);

            if (postDto.CreatorId.HasValue)
            {
                var creatorUser = await UserLookupService.FindByIdAsync(postDto.CreatorId.Value);

                postDto.Writer = ObjectMapper.Map<IdentityUser, BlogUserDto>(creatorUser);
            }

            return postDto;
        }


        public async Task<PostWithDetailsDto> CreateAsync(CreatePostDto input)
        {
            input.Url = await RenameUrlIfItAlreadyExistAsync(input.BlogId, input.Url);

            var post = new Post(
                id: GuidGenerator.Create(),
                blogId: input.BlogId,
                title: input.Title,
                coverImage: input.CoverImage,
                url: input.Url
            )
            {
                Content = input.Content,
                Description = input.Description
            };

            await _postRepository.InsertAsync(post);

            var tagList = SplitTags(input.Tags);
            await SaveTags(tagList, post);
            

            return ObjectMapper.Map<Post, PostWithDetailsDto>(post);
        }

        private async Task<string> RenameUrlIfItAlreadyExistAsync(Guid blogId, string url, Post existingPost = null)
        {
            if (await _postRepository.IsPostUrlInUseAsync(blogId, url, existingPost?.Id))
            {
                return url + "-" + Guid.NewGuid().ToString().Substring(0, 5);
            }

            return url;
        }

        private async Task SaveTags(ICollection<string> newTags, Post post)
        {
            await RemoveOldTags(newTags, post);

            await AddNewTags(newTags, post);
        }

        private async Task RemoveOldTags(ICollection<string> newTags, Post post)
        {
            foreach (var oldTag in post.Tags.ToList())
            {
                var tag = await _tagRepository.GetAsync(oldTag.TagId);

                var oldTagNameInNewTags = newTags.FirstOrDefault(t => t == tag.Name);

                if (oldTagNameInNewTags == null)
                {
                    post.RemoveTag(oldTag.TagId);

                    tag.DecreaseUsageCount();
                    await _tagRepository.UpdateAsync(tag);
                }
                else
                {
                    newTags.Remove(oldTagNameInNewTags);
                }
            }
        }

        private async Task AddNewTags(IEnumerable<string> newTags, Post post)
        {
            var tags = await _tagRepository.GetListAsync(post.BlogId);

            foreach (var newTag in newTags)
            {
                var tag = tags.FirstOrDefault(t => t.Name == newTag);

                if (tag == null)
                {
                    tag = await _tagRepository.InsertAsync(new Tag(GuidGenerator.Create(), post.BlogId, newTag, 1));
                }
                else
                {
                    tag.IncreaseUsageCount();
                    tag = await _tagRepository.UpdateAsync(tag);
                }

                post.AddTag(tag.Id);
            }
        }

        private List<string> SplitTags(string tags)
        {
            if (tags.IsNullOrWhiteSpace())
            {
                return new List<string>();
            }
            return new List<string>(tags.Split(",").Select(t => t.Trim()));
        }

        private async Task<List<TagDto>> GetTagsOfPost(Guid id)
        {
            var tagIds = (await _postRepository.GetAsync(id)).Tags;

            var tags = await _tagRepository.GetListAsync(tagIds.Select(t => t.TagId));

            return ObjectMapper.Map<List<Tag>, List<TagDto>>(tags);
        }

        private Task<List<PostWithDetailsDto>> FilterPostsByTag(IEnumerable<PostWithDetailsDto> allPostDtos, Tag tag)
        {
            var filteredPostDtos = allPostDtos.Where(p => p.Tags?.Any(t => t.Id == tag.Id) ?? false).ToList();

            return Task.FromResult(filteredPostDtos);
        }

结语

本节知识点:

  • 1.我们梳理了一个聚合的开发过程

因为该聚合东西太多了我们就拆成2章来搞一章的话太长了

联系作者:加群:867095512 @MrChuJiu

六、Abp vNext 基础篇丨文章聚合功能上

上一篇:10个CSS简写/优化技巧


下一篇:一名测试的第一一天