Merge pull request #8491 from abpframework/EngincanV/blogging-cache-invalidation

Blogging: Cache Invalidation
pull/8505/head
Halil İbrahim Kalkan 5 years ago committed by GitHub
commit d06d511ab2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -18,5 +18,7 @@ namespace Volo.Blogging.Admin.Blogs
Task<BlogDto> UpdateAsync(Guid id, UpdateBlogDto input);
Task DeleteAsync(Guid id);
Task ClearCacheAsync(Guid id);
}
}

@ -3,18 +3,22 @@ using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Volo.Abp.Application.Dtos;
using Volo.Abp.Caching;
using Volo.Blogging.Blogs;
using Volo.Blogging.Blogs.Dtos;
using Volo.Blogging.Posts;
namespace Volo.Blogging.Admin.Blogs
{
public class BlogManagementAppService : BloggingAdminAppServiceBase, IBlogManagementAppService
{
private readonly IBlogRepository _blogRepository;
public BlogManagementAppService(IBlogRepository blogRepository)
private readonly IDistributedCache<List<PostCacheItem>> _postsCache;
public BlogManagementAppService(IBlogRepository blogRepository, IDistributedCache<List<PostCacheItem>> postsCache)
{
_blogRepository = blogRepository;
_postsCache = postsCache;
}
public async Task<ListResultDto<BlogDto>> GetListAsync()
@ -63,5 +67,11 @@ namespace Volo.Blogging.Admin.Blogs
{
await _blogRepository.DeleteAsync(id);
}
[Authorize(BloggingPermissions.Blogs.ClearCache)]
public async Task ClearCacheAsync(Guid id)
{
await _postsCache.RemoveAsync(id.ToString());
}
}
}

@ -54,5 +54,12 @@ namespace Volo.Blogging.Admin
{
await _blogManagementAppService.DeleteAsync(id);
}
[HttpGet]
[Route("clear-cache/{id}")]
public async Task ClearCacheAsync(Guid id)
{
await _blogManagementAppService.ClearCacheAsync(id);
}
}
}

@ -52,6 +52,22 @@
});
},
},
{
text: l("ClearCache"),
visible: abp.auth.isGranted(
'Blogging.Blog.ClearCache'
),
confirmMessage: function (data) {
return l("ClearCacheConfirmationMessage");
},
action: function (data) {
volo.blogging.admin.blogManagement
.clearCache(data.record.id)
.then(function () {
_dataTable.ajax.reload();
})
}
}
],
},
},

@ -15,6 +15,7 @@ namespace Volo.Blogging
blogs.AddChild(BloggingPermissions.Blogs.Update, L("Permission:Edit"));
blogs.AddChild(BloggingPermissions.Blogs.Delete, L("Permission:Delete"));
blogs.AddChild(BloggingPermissions.Blogs.Create, L("Permission:Create"));
blogs.AddChild(BloggingPermissions.Blogs.ClearCache, L("Permission:ClearCache"));
var posts = bloggingGroup.AddPermission(BloggingPermissions.Posts.Default, L("Permission:Posts"));
posts.AddChild(BloggingPermissions.Posts.Update, L("Permission:Edit"));

@ -13,6 +13,7 @@ namespace Volo.Blogging
public const string Delete = Default + ".Delete";
public const string Update = Default + ".Update";
public const string Create = Default + ".Create";
public const string ClearCache = Default + ".ClearCache";
}
public static class Posts

@ -20,6 +20,13 @@ namespace Volo.Blogging
CreateMap<Post, PostWithDetailsDto>().Ignore(x=>x.Writer).Ignore(x=>x.CommentCount).Ignore(x=>x.Tags);
CreateMap<Comment, CommentWithDetailsDto>().Ignore(x => x.Writer);
CreateMap<Tag, TagDto>();
CreateMap<Post, PostCacheItem>().Ignore(x=>x.CommentCount).Ignore(x=>x.Tags);
CreateMap<PostCacheItem, PostWithDetailsDto>()
.IgnoreModificationAuditedObjectProperties()
.IgnoreDeletionAuditedObjectProperties()
.Ignore(x=>x.Writer)
.Ignore(x => x.CommentCount)
.Ignore(x => x.Tags);
}
}
}

@ -6,6 +6,7 @@ using System.Collections.Generic;
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.Caching.Distributed;
using Volo.Abp.Caching;
using Volo.Abp.EventBus.Local;
using Volo.Blogging.Comments;
using Volo.Blogging.Tagging;
using Volo.Blogging.Tagging.Dtos;
@ -20,41 +21,92 @@ namespace Volo.Blogging.Posts
private readonly IPostRepository _postRepository;
private readonly ITagRepository _tagRepository;
private readonly ICommentRepository _commentRepository;
private readonly IDistributedCache<ListResultDto<PostWithDetailsDto>> _postsCache;
public PostAppService(IPostRepository postRepository, ITagRepository tagRepository, ICommentRepository commentRepository, IBlogUserLookupService userLookupService, IDistributedCache<ListResultDto<PostWithDetailsDto>> postsCache)
private readonly IDistributedCache<List<PostCacheItem>> _postsCache;
private readonly ILocalEventBus _localEventBus;
public PostAppService(
IPostRepository postRepository,
ITagRepository tagRepository,
ICommentRepository commentRepository,
IBlogUserLookupService userLookupService,
IDistributedCache<List<PostCacheItem>> postsCache,
ILocalEventBus localEventBus
)
{
UserLookupService = userLookupService;
_postRepository = postRepository;
_tagRepository = tagRepository;
_commentRepository = commentRepository;
_postsCache = postsCache;
_localEventBus = localEventBus;
}
public async Task<ListResultDto<PostWithDetailsDto>> GetListByBlogIdAndTagName(Guid id, string tagName)
{
var cacheKey = id.ToString() + "-" + tagName;
var posts = await _postRepository.GetPostsByBlogId(id);
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));
return await _postsCache.GetOrAddAsync(
cacheKey,
async () => await GetPostsByBlogIdAndTagName(id, tagName),
() => new DistributedCacheEntryOptions
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)
{
AbsoluteExpiration = DateTimeOffset.Now.AddHours(6)
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>> GetTimeOrderedListAsync(Guid blogId)
{
return await _postsCache.GetOrAddAsync(
var postCacheItems = await _postsCache.GetOrAddAsync(
blogId.ToString(),
async () => await GetTimeOrderedPostsAsync(blogId),
() => new DistributedCacheEntryOptions
{
AbsoluteExpiration = DateTimeOffset.Now.AddHours(6)
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<BlogUser, BlogUserDto>(creatorUser);
}
}
}
return new ListResultDto<PostWithDetailsDto>(postsWithDetails);
}
public async Task<PostWithDetailsDto> GetForReadingAsync(GetPostInput input)
@ -107,6 +159,7 @@ namespace Volo.Blogging.Posts
await _commentRepository.DeleteOfPost(id);
await _postRepository.DeleteAsync(id);
await PublishPostChangedEventAsync(post.BlogId);
}
[Authorize(BloggingPermissions.Posts.Update)]
@ -128,6 +181,7 @@ namespace Volo.Blogging.Posts
var tagList = SplitTags(input.Tags);
await SaveTags(tagList, post);
await PublishPostChangedEventAsync(post.BlogId);
return ObjectMapper.Map<Post, PostWithDetailsDto>(post);
}
@ -153,66 +207,16 @@ namespace Volo.Blogging.Posts
var tagList = SplitTags(input.Tags);
await SaveTags(tagList, post);
await PublishPostChangedEventAsync(post.BlogId);
return ObjectMapper.Map<Post, PostWithDetailsDto>(post);
}
private async Task<ListResultDto<PostWithDetailsDto>> GetPostsByBlogIdAndTagName(Guid id, string tagName)
{
var posts = await _postRepository.GetPostsByBlogId(id);
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));
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<BlogUser, BlogUserDto>(creatorUser);
}
}
if (userDictionary.ContainsKey(postDto.CreatorId.Value))
{
postDto.Writer = userDictionary[(Guid)postDto.CreatorId];
}
}
}
return new ListResultDto<PostWithDetailsDto>(postDtos);
}
private async Task<ListResultDto<PostWithDetailsDto>> GetTimeOrderedPostsAsync(Guid blogId)
private async Task<List<PostCacheItem>> GetTimeOrderedPostsAsync(Guid blogId)
{
var posts = await _postRepository.GetOrderedList(blogId);
var postDtos = new List<PostWithDetailsDto>(ObjectMapper.Map<List<Post>, List<PostWithDetailsDto>>(posts));
foreach (var postDto in postDtos)
{
var creatorUser = await UserLookupService.FindByIdAsync(postDto.CreatorId.Value);
if (creatorUser != null)
{
postDto.Writer = ObjectMapper.Map<BlogUser, BlogUserDto>(creatorUser);
}
}
return new ListResultDto<PostWithDetailsDto>(postDtos);
return ObjectMapper.Map<List<Post>, List<PostCacheItem>>(posts);
}
private async Task<string> RenameUrlIfItAlreadyExistAsync(Guid blogId, string url, Post existingPost = null)
@ -300,5 +304,14 @@ namespace Volo.Blogging.Posts
return Task.FromResult(filteredPostDtos);
}
private async Task PublishPostChangedEventAsync(Guid blogId)
{
await _localEventBus.PublishAsync(
new PostChangedEvent
{
BlogId = blogId
});
}
}
}

@ -12,6 +12,7 @@
"Permission:Posts": "Posts",
"Permission:Tags": "Tags",
"Permission:Comments": "Comments",
"Permission:ClearCache": "Clear cache",
"Title": "Title",
"Delete": "Delete",
"Reply": "Reply",
@ -53,6 +54,8 @@
"Blogs": "Blogs",
"Tags": "Tags",
"ShareOn": "Share on",
"TitleLengthWarning": "Keep your title size under 60 characters to be SEO friendly!"
"TitleLengthWarning": "Keep your title size under 60 characters to be SEO friendly!",
"ClearCache": "Clear cache",
"ClearCacheConfirmationMessage": "Are you sure you want to clear the cache?"
}
}

@ -12,6 +12,7 @@
"Permission:Posts": "Yazılar",
"Permission:Tags": "Etiketler",
"Permission:Comments": "Yorumlar",
"Permission:ClearCache": "Önbelleği temizle",
"Title": "Başlık",
"Delete": "Sil",
"Reply": "Yanıtla",
@ -53,6 +54,8 @@
"Blogs": "Bloglar",
"Tags": "Etiketler",
"ShareOn": "Paylaş",
"TitleLengthWarning": "Başlığınınz SEO dostu olabilmesi için 60 karakterden az olmasını sağlayın!"
"TitleLengthWarning": "Başlığınınz SEO dostu olabilmesi için 60 karakterden az olmasını sağlayın!",
"ClearCache": "Önbelleği temizle",
"ClearCacheConfirmationMessage": "Önbelleği temizlemek istediğinizden emin misiniz?"
}
}

@ -15,6 +15,7 @@
<ProjectReference Include="..\..\..\users\src\Volo.Abp.Users.Domain\Volo.Abp.Users.Domain.csproj" />
<ProjectReference Include="..\..\..\..\framework\src\Volo.Abp.AutoMapper\Volo.Abp.AutoMapper.csproj" />
<ProjectReference Include="..\..\..\..\framework\src\Volo.Abp.Ddd.Domain\Volo.Abp.Ddd.Domain.csproj" />
<ProjectReference Include="..\..\..\..\framework\src\Volo.Abp.Caching\Volo.Abp.Caching.csproj" />
</ItemGroup>
</Project>

@ -1,5 +1,6 @@
using Microsoft.Extensions.DependencyInjection;
using Volo.Abp.AutoMapper;
using Volo.Abp.Caching;
using Volo.Abp.Domain;
using Volo.Abp.Domain.Entities.Events.Distributed;
using Volo.Abp.Modularity;
@ -13,7 +14,9 @@ namespace Volo.Blogging
[DependsOn(
typeof(BloggingDomainSharedModule),
typeof(AbpDddDomainModule),
typeof(AbpAutoMapperModule))]
typeof(AbpAutoMapperModule),
typeof(AbpCachingModule)
)]
public class BloggingDomainModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)

@ -0,0 +1,23 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Volo.Abp.Caching;
using Volo.Abp.DependencyInjection;
using Volo.Abp.EventBus;
namespace Volo.Blogging.Posts
{
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());
}
}
}

@ -0,0 +1,38 @@
using System;
using System.Collections.Generic;
using JetBrains.Annotations;
using Volo.Abp.Auditing;
using Volo.Abp.Domain.Entities;
using Volo.Blogging.Tagging;
using Volo.Blogging.Users;
namespace Volo.Blogging.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; }
}
}

@ -0,0 +1,9 @@
using System;
namespace Volo.Blogging.Posts
{
public class PostChangedEvent
{
public Guid BlogId { get; set; }
}
}
Loading…
Cancel
Save