首页 .Net 六、Abp vNext 基础篇丨文章聚合功能上

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

系列文章列表,点击展示/隐藏

系列教程一目录:知识点源码解析

系列教程二目录:Bcvp AbpVnext讲解

系列教程三目录:单个知识点讲解

系列教程四目录:分库分表(日志系统案例讲解)

本文梯子

    正文

    介绍

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

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

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

    开工

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

    项目结构

        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 >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 >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 >GetPostInput
        {
            [Required]
            public string Url { get; set; }
    
            public Guid BlogId { get; set; }
        }
    
    
    
        public >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 >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 >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 >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 >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

    公众号

    特别声明:本站部分内容收集于互联网是出于更直观传递信息的目的。该内容版权归原作者所有,并不代表本站赞同其观点和对其真实性负责。如该内容涉及任何第三方合法权利,请及时与824310991@qq.com联系,我们会及时反馈并处理完毕。