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

七、Abp vNext 基础篇丨文章聚合功能下

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

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

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

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

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

本文梯子

    正文

    介绍

    不好意思这篇文章应该早点更新的,这几天在忙CICD的东西没顾得上,等后面整好了CICD我也发2篇文章讲讲,咱们进入正题,这一章来补全剩下的 2个接口和将文章聚合进行完善。

    开工

    上一章大部分业务都完成了,这一章专门讲删除和修改,首先是删除,文章被删除评论肯定也要同步被删掉掉,另外评论因为也会存在子集所以也要同步删除。

    业务接口

    首先根据上面的分析创建评论自定义仓储接口。

        public interface ICommentRepository : IBasicRepository<Comment, Guid>
        {
            Task DeleteOfPost(Guid id, CancellationToken cancellationToken = default);
        }
    
    
        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);
    
            // 新加入的
            Task DecreaseUsageCountOfTagsAsync(List<Guid> id, CancellationToken cancellationToken = default);
        }
    

    完成删除业务接口

    
      public async Task DeleteAsync(Guid id)
            {
                // 查找文章
                var post = await _postRepository.GetAsync(id);
                // 根据文章获取Tags
                var tags = await GetTagsOfPost(id);
                // 减少Tag引用数量
                await _tagRepository.DecreaseUsageCountOfTagsAsync(tags.Select(t => t.Id).ToList());
                // 删除评论
                await _commentRepository.DeleteOfPost(id);
                // 删除文章
                await _postRepository.DeleteAsync(id);
    
            }
    
    

    上面的删除接口完成后,就剩下修改接口了。

            public async Task<PostWithDetailsDto> UpdateAsync(Guid id, UpdatePostDto input)
            {
                var post = await _postRepository.GetAsync(id);
    
                input.Url = await RenameUrlIfItAlreadyExistAsync(input.BlogId, input.Url, post);
    
                post.SetTitle(input.Title);
                post.SetUrl(input.Url);
                post.Content = input.Content;
                post.Description = input.Description;
                post.CoverImage = input.CoverImage;
    
                post = await _postRepository.UpdateAsync(post);
    
                var tagList = SplitTags(input.Tags);
                await SaveTags(tagList, post);
    
                return ObjectMapper.Map<Post, PostWithDetailsDto>(post);
            }
    
    

    整体效果

    补充

    文章整体业务完成了,现在需要把其他使用到的仓储接口实现补全一下了。

        public >EfCoreBlogRepository : EfCoreRepository<CoreDbContext, Blog, Guid>, IBlogRepository
        {
            public EfCoreBlogRepository(IDbContextProvider<CoreDbContext> dbContextProvider)
                : base(dbContextProvider)
            {
    
            }
    
            public async Task<Blog> FindByShortNameAsync(string shortName, CancellationToken cancellationToken = default)
            {
                return await (await GetDbSetAsync()).FirstOrDefaultAsync(p => p.ShortName == shortName, GetCancellationToken(cancellationToken));
            }
        }
    
    
        public >EfCoreTagRepository : EfCoreRepository<CoreDbContext, Tag, Guid>, ITagRepository
        {
            public EfCoreTagRepository(IDbContextProvider<CoreDbContext> dbContextProvider) : base(dbContextProvider)
            {
            }
    
            public async Task<List<Tag>> GetListAsync(Guid blogId, CancellationToken cancellationToken = default)
            {
                return await (await GetDbSetAsync()).Where(t => t.BlogId == blogId).ToListAsync(GetCancellationToken(cancellationToken));
            }
    
            public async Task<Tag> GetByNameAsync(Guid blogId, string name, CancellationToken cancellationToken = default)
            {
                return await (await GetDbSetAsync()).FirstAsync(t => t.BlogId == blogId && t.Name == name, GetCancellationToken(cancellationToken));
            }
    
            public async Task<Tag> FindByNameAsync(Guid blogId, string name, CancellationToken cancellationToken = default)
            {
                return await (await GetDbSetAsync()).FirstOrDefaultAsync(t => t.BlogId == blogId && t.Name == name, GetCancellationToken(cancellationToken));
            }
    
            public async Task DecreaseUsageCountOfTagsAsync(List<Guid> ids, CancellationToken cancellationToken = default)
            {
                var tags = await (await GetDbSetAsync())
                    .Where(t => ids.Any(id => id == t.Id))
                    .ToListAsync(GetCancellationToken(cancellationToken));
    
                foreach (var tag in tags)
                {
                    tag.DecreaseUsageCount();
                }
            }
        }
    
    
    

    缓存与事件

    GetTimeOrderedListAsync的文章列表数据用缓存处理。

    缓存用法可以直接参照官方文档:https://docs.abp.io/en/abp/latest/Caching,另外缓存如果你开启了redis就是我们第二章讲的那么数据就会进入redis,如果没开启就是内存。

    ICreationAuditedObject我们会在中级篇进行讲解,名字一看就懂创建对象审核。

    整体效果

        private readonly IDistributedCache<List<PostCacheItem>> _postsCache;
    
    
        public async Task<ListResultDto<PostWithDetailsDto>> GetTimeOrderedListAsync(Guid blogId)
            {
                var postCacheItems = await _postsCache.GetOrAddAsync(
                    blogId.ToString(),
                    async () => await GetTimeOrderedPostsAsync(blogId),
                    () => new DistributedCacheEntryOptions
                    {
                        AbsoluteExpiration = DateTimeOffset.Now.AddHours(1)
                    }
                );
    
                var postsWithDetails = ObjectMapper.Map<List<PostCacheItem>, List<PostWithDetailsDto>>(postCacheItems);
    
                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);
    
            }
    
            private async Task<List<PostCacheItem>> GetTimeOrderedPostsAsync(Guid blogId)
            {
                var posts = await _postRepository.GetOrderedList(blogId);
    
                return ObjectMapper.Map<List<Post>, List<PostCacheItem>>(posts);
            }
    
    
    
    
    
        [Serializable]
        public >PostCacheItem : ICreationAuditedObject
        {
            public Guid Id { get; set; }
    
            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; }
    
            public List<Tag> Tags { get; set; }
    
            public Guid? CreatorId { get; set; }
    
            public DateTime CreationTime { get; set; }
        }
    
    
    
    

    我们现在将根据时间排序获取文章列表接口的文章数据缓存了,但是如果文章被删除或者修改或者创建了新的文章,怎么办,这个时候我们缓存中的数据是有问题的,我们需要刷新缓存内容。

    领域事件使用文档:https://docs.abp.io/en/abp/latest/Local-Event-Bus

    引入ILocalEventBus并新增PublishPostChangedEventAsync方法发布事件,在删除、新增、修改接口上调用发布事件方法。

    PostChangedEvent放在领域层,可以参考第三章的架构讲解

    调用发布事件

    
            private readonly ILocalEventBus _localEventBus;
    
    
            private async Task PublishPostChangedEventAsync(Guid blogId)
            {
                await _localEventBus.PublishAsync(
                    new PostChangedEvent
                    {
                        BlogId = blogId
                    });
            }
    
    
        public >PostChangedEvent
        {
            public Guid BlogId { get; set; }
        }
    
    

    事件发布出去了,谁来处理呢,这个业务是文章的肯定文章自己处理,LocalEvent属于本地事件和接口属于同一个事务单元,可以去上面的文档说明。

        public >PostCacheInvalidator : ILocalEventHandler<PostChangedEvent>, ITransientDependency
        {
            protected IDistributedCache<List<PostCacheItem>> Cache { get; }
    
            public PostCacheInvalidator(IDistributedCache<List<PostCacheItem>> cache)
            {
                Cache = cache;
            }
    
            public virtual async Task HandleEventAsync(PostChangedEvent post)
            {
                await Cache.RemoveAsync(post.BlogId.ToString());
            }
        }
    

    领域事件

    结语

    本节知识点:

    • 1.业务开发方式
    • 2.ABP缓存的使用
    • 3.领域事件的使用

    最麻烦的文章聚合算是讲完了,有一个ABP如何做文件上传没讲那就是那个文章封面下期再说,还剩下Comment、Tag,另外我说下我这边写代码暂时不给演示测试效果,大家也是先学ABP用法和DDD理论实践。

    后面单元测试的时候我在把接口一把梭,加油请持续关注。

    联系作者:加群:867095512 @MrChuJiu

    公众号

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