介绍

不好意思这篇文章应该早点更新的,这几天在忙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

七、Abp vNext 基础篇丨文章聚合功能下的更多相关文章

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

    介绍 9月开篇讲,前面几章群里已经有几个小伙伴跟着做了一遍了,遇到的问题和疑惑也都在群里反馈和解决好了,9月咱们保持保持更新.争取10月份更新完基础篇. 另外番外篇属于 我在abp群里和日常开发的问题 ...

  2. 八、Abp vNext 基础篇丨标签聚合功能

    介绍 本章节先来把上一章漏掉的上传文件处理下,然后实现Tag功能. 上传文件 上传文件其实不含在任何一个聚合中,它属于一个独立的辅助性功能,先把抽象接口定义一下,在Bcvp.Blog.Core.App ...

  3. 九、Abp vNext 基础篇丨评论聚合功能

    介绍 评论本来是要放到标签里面去讲的,但是因为上一章东西有点多了,我就没放进去,这一章单独拿出来,内容不多大家自己写写就可以,也算是对前面讲解的一个小练习吧. 相关注释我也加在代码上面了,大家看看代码 ...

  4. 十一、Abp vNext 基础篇丨测试

    前言 祝大家国庆快乐,本来想国庆之前更新完的,结果没写完,今天把剩下的代码补了一下总算ok了. 本章节也是我们后端日常开发中最重要的一步就是测试,我们经常听到的单元测试.集成测试.UI测试.系统测试, ...

  5. 五、Abp vNext 基础篇丨博客聚合功能

    介绍 业务篇章先从客户端开始写,另外补充一下我给项目起名的时候没多想起的太随意了,结果后面有些地方命名冲突了需要通过手动using不过问题不大. 开工 应用层 根据第三章分层架构里面讲到的现在我们模型 ...

  6. Abp vNext 基础篇丨分层架构

    介绍 本章节对 ABP 框架进行一个简单的介绍,摘自ABP官方,后面会在使用过程中对各个知识点进行细致的讲解. 领域驱动设计 领域驱动设计(简称:DDD)是一种针对复杂需求的软件开发方法.将软件实现与 ...

  7. Abp vNext 基础篇丨领域构建

    介绍 我们将通过例⼦介绍和解释⼀些显式规则.在实现领域驱动设计时,应该遵循这些规则并将其应⽤到解决⽅案中. 领域划分 首先我们先对比下Blog.Core和本次重构设计上的偏差,可以看到多了一个博客管理 ...

  8. 十、Abp vNext 基础篇丨权限

    介绍 本章节来把接口的权限加一下 权限配置和使用 官方地址:https://docs.abp.io/en/abp/latest/Authorization 下面这种代码可能我们日常开发都写过,ASP. ...

  9. [Abp vNext 源码分析] - 文章目录

    一.简要介绍 ABP vNext 是 ABP 框架作者所发起的新项目,截止目前 (2019 年 2 月 18 日) 已经拥有 1400 多个 Star,最新版本号为 v 0.16.0 ,但还属于预览版 ...

随机推荐

  1. SSRF漏洞入门篇

    SSRF漏洞,又名服务端请求伪造漏洞. PHP中下列函数使用不当会导致SSRF: file_get_contents().fsockopen().curl_exec()函数(源码审计的时候注意点儿): ...

  2. mysql中的with rollup得到group by的汇总信息

    使用mysql中的with rollup可以得到每个分组的汇总级别的数据: 表如下: CREATE TABLE `test3` (  `id` int(5) unsigned NOT NULL AUT ...

  3. iptables中实现内外网互访,SNAT和DNAT

    目录 一.SNAT原理与应用 二.DNAT原理与应用 DNAT转换:发布内网web服务 DNAT转换:发布时修改目标端口 三.防火墙规则的备份和还原 四.linux抓包 一.SNAT原理与应用 ① S ...

  4. 面试官疯狂问我:char和varchar的区别 怎么办?愣着干嘛?进来白嫖啊!

    MySQL的修仙之路,图文谈谈如何学MySQL.如何进阶!(已发布) 面前突击!33道数据库高频面试题,你值得拥有!(已发布) 大家常说的基数是什么?(已发布) 讲讲什么是慢查!如何监控?如何排查?( ...

  5. 20分钟掌握Android Gradle

    目前国内对Android领域的探索已经越来越深,不少技术领域如插件化.热修复.构建系统等都对Gradle有迫切的需求,不懂Gradle将无法完成上述事情.所以Gradle必须要学习. Gradle 里 ...

  6. rollup 开发环境搭建

    rollup 开发环境搭建 初始化项目使用lerna管理项目 使用npm init 初始化项目 npm init -y 安装lerna并初始化项目 npm install lerna --save-d ...

  7. druid与知乎平台

    背景 知乎作为知名中文知识内容平台,业务增长和产品迭代速度很快,如何满足业务快速扩张中的灵活分析需求,是知乎数据平台组要面临的一大挑战. 知乎数据平台团队基于开源的 Druid 打造的业务自助式的数据 ...

  8. GraphQL API vs REST API

    REST是构建API的一种流行方法,而且比GraphQL应用更广泛,让我们看看GraphQL和REST的区别. Rest是一个概念 REST是一个事实上的架构标准,但它实际上没有规范,有大量的非官方定 ...

  9. Apache/Nginx/IIS 访问日志详解

    Apache日志详解 1.Apache日志文件名称及所在路径 日志文件一般都是保存在在apache/logs目录下,实际情况可以根据Apache的配置文件去查找日志文件所在的路径. 例如phpstud ...

  10. DVWA(四):Command Injection 全等级命令注入

    Command Injection : 命令注入(Command Injection),对一些函数的参数没有做好过滤而导致用户可以控制输入的参数,使其恶意执行系统命令或这cmd.bash指令的一种注入 ...