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

介绍

不好意思这篇文章应该早点更新的,这几天在忙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 class 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 class 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 class 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 class PostChangedEvent
    {
        public Guid BlogId { get; set; }
    }

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

    public class 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

公众号

posted @ 2021-09-06 15:38  初久的私房菜  阅读(1231)  评论(0编辑  收藏  举报
作者:初久的私房菜
好好学习,天天向上
返回顶部小火箭
好友榜:
如果愿意,把你的博客地址放这里
张弛:https://blog.zhangchi.fun/