diff --git a/Directory.Packages.props b/Directory.Packages.props
index 755c2d8f..e233a4e8 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -20,6 +20,7 @@
+
diff --git a/src/LinkDotNet.Blog.Infrastructure/LinkDotNet.Blog.Infrastructure.csproj b/src/LinkDotNet.Blog.Infrastructure/LinkDotNet.Blog.Infrastructure.csproj
index 6e65f1be..4ec1166f 100644
--- a/src/LinkDotNet.Blog.Infrastructure/LinkDotNet.Blog.Infrastructure.csproj
+++ b/src/LinkDotNet.Blog.Infrastructure/LinkDotNet.Blog.Infrastructure.csproj
@@ -14,6 +14,7 @@
+
diff --git a/src/LinkDotNet.Blog.Infrastructure/Persistence/CachedRepository.cs b/src/LinkDotNet.Blog.Infrastructure/Persistence/CachedRepository.cs
index b5f7b14a..66cc62c1 100644
--- a/src/LinkDotNet.Blog.Infrastructure/Persistence/CachedRepository.cs
+++ b/src/LinkDotNet.Blog.Infrastructure/Persistence/CachedRepository.cs
@@ -1,10 +1,10 @@
-using System;
+using System;
using System.Collections.Generic;
using System.Linq.Expressions;
using System.Threading.Tasks;
using LinkDotNet.Blog.Domain;
-using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Diagnostics.HealthChecks;
+using ZiggyCreatures.Caching.Fusion;
namespace LinkDotNet.Blog.Infrastructure.Persistence;
@@ -12,22 +12,20 @@ public sealed class CachedRepository : IRepository
where T : Entity
{
private readonly IRepository repository;
- private readonly IMemoryCache memoryCache;
+ private readonly IFusionCache fusionCache;
- public CachedRepository(IRepository repository, IMemoryCache memoryCache)
+ public CachedRepository(IRepository repository, IFusionCache fusionCache)
{
this.repository = repository;
- this.memoryCache = memoryCache;
+ this.fusionCache = fusionCache;
}
public ValueTask PerformHealthCheckAsync() => repository.PerformHealthCheckAsync();
- public async ValueTask GetByIdAsync(string id) =>
- (await memoryCache.GetOrCreateAsync(id, async entry =>
+ public async ValueTask GetByIdAsync(string id) => await fusionCache.GetOrSetAsync(id, async c =>
{
- entry.SlidingExpiration = TimeSpan.FromDays(7);
return await repository.GetByIdAsync(id);
- }))!;
+ }, TimeSpan.FromDays(7));
public async ValueTask> GetAllAsync(Expression>? filter = null,
Expression>? orderBy = null,
@@ -53,14 +51,14 @@ public async ValueTask StoreAsync(T entity)
if (!string.IsNullOrEmpty(entity.Id))
{
- memoryCache.Remove(entity.Id);
+ await fusionCache.RemoveAsync(entity.Id);
}
}
public async ValueTask DeleteAsync(string id)
{
await repository.DeleteAsync(id);
- memoryCache.Remove(id);
+ await fusionCache.RemoveAsync(id);
}
public async ValueTask DeleteBulkAsync(IReadOnlyCollection ids) => await repository.DeleteBulkAsync(ids);
diff --git a/src/LinkDotNet.Blog.Web/Controller/SitemapController.cs b/src/LinkDotNet.Blog.Web/Controller/SitemapController.cs
index 03397a33..1221eeec 100644
--- a/src/LinkDotNet.Blog.Web/Controller/SitemapController.cs
+++ b/src/LinkDotNet.Blog.Web/Controller/SitemapController.cs
@@ -1,9 +1,9 @@
-using System;
-using System.Threading.Tasks;
using LinkDotNet.Blog.Web.Features.Admin.Sitemap.Services;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.RateLimiting;
-using Microsoft.Extensions.Caching.Memory;
+using System;
+using System.Threading.Tasks;
+using ZiggyCreatures.Caching.Fusion;
namespace LinkDotNet.Blog.Web.Controller;
@@ -13,28 +13,24 @@ public sealed class SitemapController : ControllerBase
{
private readonly ISitemapService sitemapService;
private readonly IXmlWriter xmlWriter;
- private readonly IMemoryCache memoryCache;
+ private readonly IFusionCache fusionCache;
public SitemapController(
ISitemapService sitemapService,
IXmlWriter xmlWriter,
- IMemoryCache memoryCache)
+ IFusionCache fusionCache)
{
this.sitemapService = sitemapService;
this.xmlWriter = xmlWriter;
- this.memoryCache = memoryCache;
+ this.fusionCache = fusionCache;
}
[ResponseCache(Duration = 3600)]
[HttpGet]
public async Task GetSitemap()
{
- var buffer = await memoryCache.GetOrCreateAsync("sitemap.xml", async e =>
- {
- e.AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(1);
- return await GetSitemapBuffer();
- })
- ?? throw new InvalidOperationException("Buffer is null");
+ var buffer = await fusionCache.GetOrSetAsync("sitemap.xml", async e => await GetSitemapBuffer(), o => o.SetDuration(TimeSpan.FromHours(1)))
+ ?? throw new InvalidOperationException("Buffer is null");
return File(buffer, "application/xml");
}
diff --git a/src/LinkDotNet.Blog.Web/Features/Home/Index.razor b/src/LinkDotNet.Blog.Web/Features/Home/Index.razor
index 8997b330..a4c8678b 100644
--- a/src/LinkDotNet.Blog.Web/Features/Home/Index.razor
+++ b/src/LinkDotNet.Blog.Web/Features/Home/Index.razor
@@ -1,4 +1,4 @@
-@page "/"
+@page "/"
@page "/{page:int}"
@using Markdig
@using LinkDotNet.Blog.Domain
@@ -8,7 +8,8 @@
@using LinkDotNet.Blog.Web.Features.Services
@using Microsoft.Extensions.Caching.Memory
@using Microsoft.Extensions.Primitives
-@inject IMemoryCache MemoryCache
+@using ZiggyCreatures.Caching.Fusion
+@inject IFusionCache FusionCache
@inject ICacheTokenProvider CacheTokenProvider
@inject IRepository BlogPostRepository
@inject IOptions Introduction
@@ -49,27 +50,24 @@
protected override async Task OnParametersSetAsync()
{
- const string firstPageCacheKey = "BlogPostList";
+ const string firstPageCacheKey = "BlogPostList";
if (Page is null or < 1)
{
Page = 1;
}
- // The hot path is that users land on the initial page which is the first page.
- // So we want to cache that page for a while to reduce the load on the database
- // and to speed up the page load.
- // That will lead to stale blog posts for x minutes (worst case) for the first page,
- // but I am fine with that (as publishing isn't super critical and not done multiple times per hour).
- // This cache can be manually invalidated in the Admin UI (settings)
- if (Page == 1)
- {
- currentPage = (await MemoryCache.GetOrCreateAsync(firstPageCacheKey, async entry =>
- {
- var cacheDuration = TimeSpan.FromMinutes(AppConfiguration.Value.FirstPageCacheDurationInMinutes);
- entry.AbsoluteExpirationRelativeToNow = cacheDuration;
- entry.AddExpirationToken(new CancellationChangeToken(CacheTokenProvider.Token));
- return await GetAllForPageAsync(1);
- }))!;
+ // The hot path is that users land on the initial page which is the first page.
+ // So we want to cache that page for a while to reduce the load on the database
+ // and to speed up the page load.
+ // That will lead to stale blog posts for x minutes (worst case) for the first page,
+ // but I am fine with that (as publishing isn't super critical and not done multiple times per hour).
+ // This cache can be manually invalidated in the Admin UI (settings)
+ if (Page == 1)
+ {
+ currentPage = await FusionCache.GetOrSetAsync(firstPageCacheKey, async e => await GetAllForPageAsync(1), o =>
+ {
+ o.SetDuration(TimeSpan.FromMinutes(AppConfiguration.Value.FirstPageCacheDurationInMinutes));
+ });
return;
}
diff --git a/src/LinkDotNet.Blog.Web/RegistrationExtensions/StorageProviderExtensions.cs b/src/LinkDotNet.Blog.Web/RegistrationExtensions/StorageProviderExtensions.cs
index 9498d37d..2d7f638b 100644
--- a/src/LinkDotNet.Blog.Web/RegistrationExtensions/StorageProviderExtensions.cs
+++ b/src/LinkDotNet.Blog.Web/RegistrationExtensions/StorageProviderExtensions.cs
@@ -1,9 +1,11 @@
-using System;
using LinkDotNet.Blog.Domain;
using LinkDotNet.Blog.Infrastructure.Persistence;
-using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
+using System;
+using ZiggyCreatures.Caching.Fusion;
+using ZiggyCreatures.Caching.Fusion.Locking;
+using ZiggyCreatures.Caching.Fusion.Locking.AsyncKeyed;
namespace LinkDotNet.Blog.Web.RegistrationExtensions;
@@ -13,7 +15,8 @@ public static IServiceCollection AddStorageProvider(this IServiceCollection serv
{
ArgumentNullException.ThrowIfNull(configuration);
- services.AddMemoryCache();
+ services.AddSingleton();
+ services.AddFusionCache().WithRegisteredMemoryLocker();
var provider = configuration["PersistenceProvider"] ?? throw new InvalidOperationException("No persistence provider configured");
var persistenceProvider = PersistenceProvider.Create(provider);
@@ -58,6 +61,6 @@ private static void RegisterCachedRepository(this IServiceCollection serv
services.AddScoped();
services.AddScoped>(provider => new CachedRepository(
provider.GetRequiredService(),
- provider.GetRequiredService()));
+ provider.GetRequiredService()));
}
}
diff --git a/tests/LinkDotNet.Blog.IntegrationTests/Infrastructure/Persistence/CachedRepositoryTests.cs b/tests/LinkDotNet.Blog.IntegrationTests/Infrastructure/Persistence/CachedRepositoryTests.cs
index d49703e0..4ac61380 100644
--- a/tests/LinkDotNet.Blog.IntegrationTests/Infrastructure/Persistence/CachedRepositoryTests.cs
+++ b/tests/LinkDotNet.Blog.IntegrationTests/Infrastructure/Persistence/CachedRepositoryTests.cs
@@ -1,9 +1,10 @@
-using System.Linq;
-using System.Threading.Tasks;
using LinkDotNet.Blog.Domain;
using LinkDotNet.Blog.Infrastructure.Persistence;
using LinkDotNet.Blog.TestUtilities;
-using Microsoft.Extensions.Caching.Memory;
+using System.Linq;
+using System.Threading.Tasks;
+using ZiggyCreatures.Caching.Fusion;
+using ZiggyCreatures.Caching.Fusion.Locking.AsyncKeyed;
namespace LinkDotNet.Blog.IntegrationTests.Infrastructure.Persistence;
@@ -19,7 +20,7 @@ public async Task ShouldNotCacheWhenDifferentQueries()
await Repository.StoreAsync(bp2);
await Repository.StoreAsync(bp3);
var searchTerm = "tag 1";
- var sut = new CachedRepository(Repository, new MemoryCache(new MemoryCacheOptions()));
+ var sut = new CachedRepository(Repository, new FusionCache(new FusionCacheOptions(), memoryLocker: new AsyncKeyedMemoryLocker()));
await sut.GetAllAsync(f => f.Tags.Any(t => t == searchTerm));
searchTerm = "tag 2";
@@ -34,7 +35,7 @@ public async Task ShouldResetOnDelete()
{
var bp1 = new BlogPostBuilder().WithTitle("1").Build();
var bp2 = new BlogPostBuilder().WithTitle("2").Build();
- var sut = new CachedRepository(Repository, new MemoryCache(new MemoryCacheOptions()));
+ var sut = new CachedRepository(Repository, new FusionCache(new FusionCacheOptions(), memoryLocker: new AsyncKeyedMemoryLocker()));
await sut.StoreAsync(bp1);
await sut.StoreAsync(bp2);
await sut.GetAllAsync();
@@ -50,7 +51,7 @@ public async Task ShouldResetOnSave()
{
var bp1 = new BlogPostBuilder().WithTitle("1").Build();
var bp2 = new BlogPostBuilder().WithTitle("2").Build();
- var sut = new CachedRepository(Repository, new MemoryCache(new MemoryCacheOptions()));
+ var sut = new CachedRepository(Repository, new FusionCache(new FusionCacheOptions(), memoryLocker: new AsyncKeyedMemoryLocker()));
await sut.StoreAsync(bp1);
await sut.GetAllAsync();
bp1.Update(bp2);
diff --git a/tests/LinkDotNet.Blog.IntegrationTests/Infrastructure/Persistence/Sql/BlogPostRepositoryTests.cs b/tests/LinkDotNet.Blog.IntegrationTests/Infrastructure/Persistence/Sql/BlogPostRepositoryTests.cs
index d2ed4a13..197fd4d4 100644
--- a/tests/LinkDotNet.Blog.IntegrationTests/Infrastructure/Persistence/Sql/BlogPostRepositoryTests.cs
+++ b/tests/LinkDotNet.Blog.IntegrationTests/Infrastructure/Persistence/Sql/BlogPostRepositoryTests.cs
@@ -1,10 +1,11 @@
-using System.Linq;
-using System.Threading.Tasks;
using LinkDotNet.Blog.Domain;
using LinkDotNet.Blog.Infrastructure.Persistence;
using LinkDotNet.Blog.TestUtilities;
using Microsoft.EntityFrameworkCore;
-using Microsoft.Extensions.Caching.Memory;
+using System.Linq;
+using System.Threading.Tasks;
+using ZiggyCreatures.Caching.Fusion;
+using ZiggyCreatures.Caching.Fusion.Locking.AsyncKeyed;
using TestContext = Xunit.TestContext;
namespace LinkDotNet.Blog.IntegrationTests.Infrastructure.Persistence.Sql;
@@ -175,7 +176,7 @@ public async Task ShouldDelete()
public async Task GivenBlogPostWithTags_WhenLoadingAndDeleting_ThenShouldBeUpdated()
{
var bp = new BlogPostBuilder().WithTags("tag 1").Build();
- var sut = new CachedRepository(Repository, new MemoryCache(new MemoryCacheOptions()));
+ var sut = new CachedRepository(Repository, new FusionCache(new FusionCacheOptions(), memoryLocker: new AsyncKeyedMemoryLocker()));
await sut.StoreAsync(bp);
var updateBp = new BlogPostBuilder().WithTags("tag 2").Build();
var bpFromCache = await sut.GetByIdAsync(bp.Id);
diff --git a/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/Home/IndexTests.cs b/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/Home/IndexTests.cs
index c88a72b3..33fba7b1 100644
--- a/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/Home/IndexTests.cs
+++ b/tests/LinkDotNet.Blog.IntegrationTests/Web/Features/Home/IndexTests.cs
@@ -10,6 +10,8 @@
using LinkDotNet.Blog.Web.Features.Services;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
+using ZiggyCreatures.Caching.Fusion;
+using ZiggyCreatures.Caching.Fusion.Locking.AsyncKeyed;
namespace LinkDotNet.Blog.IntegrationTests.Web.Features.Home;
@@ -225,5 +227,6 @@ private void RegisterComponents(BunitContext ctx, string? profilePictureUri = nu
ctx.Services.AddScoped(_ => Options.Create(CreateSampleAppConfiguration(profilePictureUri, useMultiAuthorMode).Introduction));
ctx.Services.AddScoped(_ => Substitute.For());
ctx.Services.AddScoped(_ => Substitute.For());
+ ctx.Services.AddScoped(_ => new FusionCache(new FusionCacheOptions(), memoryLocker: new AsyncKeyedMemoryLocker()));
}
}
diff --git a/tests/LinkDotNet.Blog.UnitTests/Infrastructure/Persistence/CachedRepositoryTests.cs b/tests/LinkDotNet.Blog.UnitTests/Infrastructure/Persistence/CachedRepositoryTests.cs
index fb3baa1d..777b6f95 100644
--- a/tests/LinkDotNet.Blog.UnitTests/Infrastructure/Persistence/CachedRepositoryTests.cs
+++ b/tests/LinkDotNet.Blog.UnitTests/Infrastructure/Persistence/CachedRepositoryTests.cs
@@ -1,11 +1,12 @@
-using System;
-using System.Linq.Expressions;
-using System.Threading.Tasks;
using LinkDotNet.Blog.Domain;
using LinkDotNet.Blog.Infrastructure;
using LinkDotNet.Blog.Infrastructure.Persistence;
using LinkDotNet.Blog.TestUtilities;
-using Microsoft.Extensions.Caching.Memory;
+using System;
+using System.Linq.Expressions;
+using System.Threading.Tasks;
+using ZiggyCreatures.Caching.Fusion;
+using ZiggyCreatures.Caching.Fusion.Locking.AsyncKeyed;
namespace LinkDotNet.Blog.UnitTests.Infrastructure.Persistence;
@@ -17,7 +18,7 @@ public sealed class CachedRepositoryTests
public CachedRepositoryTests()
{
repositoryMock = Substitute.For>();
- sut = new CachedRepository(repositoryMock, new MemoryCache(new MemoryCacheOptions()));
+ sut = new CachedRepository(repositoryMock, new FusionCache(new FusionCacheOptions(), memoryLocker: new AsyncKeyedMemoryLocker()));
}
[Fact]
@@ -138,4 +139,4 @@ private void SetupRepository()
Arg.Any(),
Arg.Any()).Returns(new PagedList([blogPost], 1, 1, 1));
}
-}
\ No newline at end of file
+}