Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
<PackageVersion Include="Pomelo.EntityFrameworkCore.MySql" Version="9.0.0" />
<PackageVersion Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" />
<PackageVersion Include="RavenDB.Client" Version="7.1.4" />
<PackageVersion Include="ZiggyCreatures.FusionCache.Locking.AsyncKeyed" Version="2.4.0" />
</ItemGroup>
<ItemGroup Label="Web">
<PackageVersion Include="AspNetCore.HealthChecks.UI.Client" Version="9.0.0" />
Expand Down Expand Up @@ -58,4 +59,4 @@
<PackageVersion Include="CommandLineParser" Version="2.9.1" />
<PackageVersion Include="Microsoft.Playwright" Version="1.56.0" />
</ItemGroup>
</Project>
</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,14 @@

<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" />
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions" />
<PackageReference Include="MongoDB.Driver" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL"/>
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" />
<PackageReference Include="RavenDB.Client" />
<PackageReference Include="ZiggyCreatures.FusionCache.Locking.AsyncKeyed" />
</ItemGroup>

<ItemGroup>
Expand Down
22 changes: 11 additions & 11 deletions src/LinkDotNet.Blog.Infrastructure/Persistence/CachedRepository.cs
Original file line number Diff line number Diff line change
@@ -1,33 +1,33 @@
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;

public sealed class CachedRepository<T> : IRepository<T>
where T : Entity
{
private readonly IRepository<T> repository;
private readonly IMemoryCache memoryCache;
private readonly IFusionCache fusionCache;

public CachedRepository(IRepository<T> repository, IMemoryCache memoryCache)
public CachedRepository(IRepository<T> repository, IFusionCache fusionCache)
{
this.repository = repository;
this.memoryCache = memoryCache;
this.fusionCache = fusionCache;
}

public ValueTask<HealthCheckResult> PerformHealthCheckAsync() => repository.PerformHealthCheckAsync();

public async ValueTask<T?> GetByIdAsync(string id) =>
(await memoryCache.GetOrCreateAsync(id, async entry =>
public async ValueTask<T?> GetByIdAsync(string id) => await GetByIdAsync(id, TimeSpan.FromDays(7));

public async ValueTask<T?> GetByIdAsync(string id, TimeSpan expiration) => await fusionCache.GetOrSetAsync(id, async c =>
{
entry.SlidingExpiration = TimeSpan.FromDays(7);
return await repository.GetByIdAsync(id);
}))!;
}, expiration);

public async ValueTask<IPagedList<T>> GetAllAsync(Expression<Func<T, bool>>? filter = null,
Expression<Func<T, object>>? orderBy = null,
Expand All @@ -53,14 +53,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<string> ids) => await repository.DeleteBulkAsync(ids);
Expand Down
20 changes: 8 additions & 12 deletions src/LinkDotNet.Blog.Web/Controller/SitemapController.cs
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -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<IActionResult> 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");
}
Expand Down
34 changes: 16 additions & 18 deletions src/LinkDotNet.Blog.Web/Features/Home/Index.razor
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
@page "/"
@page "/"
@page "/{page:int}"
@using Markdig
@using LinkDotNet.Blog.Domain
Expand All @@ -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<BlogPost> BlogPostRepository
@inject IOptions<Introduction> Introduction
Expand Down Expand Up @@ -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;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -13,7 +15,8 @@ public static IServiceCollection AddStorageProvider(this IServiceCollection serv
{
ArgumentNullException.ThrowIfNull(configuration);

services.AddMemoryCache();
services.AddSingleton<IFusionCacheMemoryLocker, AsyncKeyedMemoryLocker>();
services.AddFusionCache().WithRegisteredMemoryLocker();

var provider = configuration["PersistenceProvider"] ?? throw new InvalidOperationException("No persistence provider configured");
var persistenceProvider = PersistenceProvider.Create(provider);
Expand Down Expand Up @@ -58,6 +61,6 @@ private static void RegisterCachedRepository<TRepo>(this IServiceCollection serv
services.AddScoped<TRepo>();
services.AddScoped<IRepository<BlogPost>>(provider => new CachedRepository<BlogPost>(
provider.GetRequiredService<TRepo>(),
provider.GetRequiredService<IMemoryCache>()));
provider.GetRequiredService<IFusionCache>()));
}
}
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -19,7 +20,7 @@ public async Task ShouldNotCacheWhenDifferentQueries()
await Repository.StoreAsync(bp2);
await Repository.StoreAsync(bp3);
var searchTerm = "tag 1";
var sut = new CachedRepository<BlogPost>(Repository, new MemoryCache(new MemoryCacheOptions()));
var sut = new CachedRepository<BlogPost>(Repository, new FusionCache(new FusionCacheOptions(), memoryLocker: new AsyncKeyedMemoryLocker()));
await sut.GetAllAsync(f => f.Tags.Any(t => t == searchTerm));
searchTerm = "tag 2";

Expand All @@ -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<BlogPost>(Repository, new MemoryCache(new MemoryCacheOptions()));
var sut = new CachedRepository<BlogPost>(Repository, new FusionCache(new FusionCacheOptions(), memoryLocker: new AsyncKeyedMemoryLocker()));
await sut.StoreAsync(bp1);
await sut.StoreAsync(bp2);
await sut.GetAllAsync();
Expand All @@ -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<BlogPost>(Repository, new MemoryCache(new MemoryCacheOptions()));
var sut = new CachedRepository<BlogPost>(Repository, new FusionCache(new FusionCacheOptions(), memoryLocker: new AsyncKeyedMemoryLocker()));
await sut.StoreAsync(bp1);
await sut.GetAllAsync();
bp1.Update(bp2);
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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<BlogPost>(Repository, new MemoryCache(new MemoryCacheOptions()));
var sut = new CachedRepository<BlogPost>(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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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<ICacheTokenProvider>());
ctx.Services.AddScoped(_ => Substitute.For<IBookmarkService>());
ctx.Services.AddScoped<IFusionCache>(_ => new FusionCache(new FusionCacheOptions(), memoryLocker: new AsyncKeyedMemoryLocker()));
}
}
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -17,7 +18,7 @@ public sealed class CachedRepositoryTests
public CachedRepositoryTests()
{
repositoryMock = Substitute.For<IRepository<BlogPost>>();
sut = new CachedRepository<BlogPost>(repositoryMock, new MemoryCache(new MemoryCacheOptions()));
sut = new CachedRepository<BlogPost>(repositoryMock, new FusionCache(new FusionCacheOptions(), memoryLocker: new AsyncKeyedMemoryLocker()));
}

[Fact]
Expand Down Expand Up @@ -138,4 +139,4 @@ private void SetupRepository()
Arg.Any<int>(),
Arg.Any<int>()).Returns(new PagedList<BlogPost>([blogPost], 1, 1, 1));
}
}
}