介绍

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 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 基础篇丨文章聚合功能上的更多相关文章

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

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

  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. POJ1417 True Liars 题解

    通过读题,容易发现,当回答为yes时 \(x,y\) 必属于同类,当回答为no时二者必为异类(并且当 \(x=y\) 时,回答必为yes,不过这题不用这个性质). 于是先按关系维护连通块,然后求出每个 ...

  2. Tbase读写分离与分库分表

    一.读写分离 1.1 what 读写分离 读写分离,基本的原理是让主数据库处理事务性增.改.删操作(INSERT.UPDATE.DELETE),而从数据库处理SELECT查询操作.数据库复制被用来把事 ...

  3. js 正序、倒序、按字段排序方法

    js 基础--sort方法: arrayObject.sort(sortby); 参数:定义排序规则(正序.倒序.按字段排序)的函数: 返回值:对数组的引用.请注意,数组在原数组上进行排序,不生成副本 ...

  4. 解决pip下载速度慢的问题

    解决链接:https://blog.csdn.net/u013901768/article/details/82343512 感谢这位博主的分享. 博客内容如下: 1. 安装时强制使用国内源进行安装, ...

  5. Python基础之用PyQt5界面代码分离以及自定义一个槽函数

    最近开发一个项目,需要用到界面,遇到界面不能实时更新的问题,看到网上很多用槽函数,但是大多都是些button的,并不是我需要的,要么就是整数的,后来自己进行尝试,写了一个自定义的槽函数处理treewi ...

  6. 如何用C++自己实现mysql数据库的连接池?

    为什么是mysql? 现在几乎所有的后台应用都要用到数据库,什么关系型的.非关系型的:正当关系的,不正当关系的:主流的和非主流的, 大到Oracle,小到sqlite,以及包括现在逐渐流行的基于物联网 ...

  7. 记录21.07.21 —— ES6基础

    学习目录 课件地址: ES6核心技术课件 1.let关键字 1.1 let与var的区别 ①let不能重复定义 ②块作用域的区别 ③变量声明之前let不能使用,var可以 ④ 课件代码 <htm ...

  8. SQL语句(二)数据排序和单行函数

    目录 一.排序查询 1. 基本排序 2. 多条件排序 二.单行函数 调用方法 字符函数 ①LENGTH函数 ②CONCAT函数 ③upper 和 lower ④substr ⑤instr ⑥trim ...

  9. 解决 OnDropFiles 可能无响应的问题【转】

    大多数程序都有接收拖放文件的功能,即是用鼠标把文件拖放到程序窗口上方,符合格式的文件就会自动被程序打开.最近自己对编写的程序增加了一个拖放文件的功能,在 Windows XP.Windows Serv ...

  10. I类HDACs是乳酸化修饰“eraser”

    赖氨酸酰化修饰 (lysine acylation) 是一种广泛存在的.进化上高度保守的蛋白质翻译后修饰 (post-translational modifications, PTMs) 类型,通过表 ...