介紹

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