Skip to content

Commit

Permalink
feat: Add possiblity to configure and invalidate first page cache
Browse files Browse the repository at this point in the history
  • Loading branch information
linkdotnet committed Mar 1, 2024
1 parent 313c53e commit c41e18e
Show file tree
Hide file tree
Showing 15 changed files with 152 additions and 6 deletions.
2 changes: 2 additions & 0 deletions docs/Setup/Configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ The appsettings.json file has a lot of options to customize the content of the b
"LogoutUri": ""
},
"BlogPostsPerPage": 10,
"FirstPageCacheDurationInMinutes": 10,
"ProfileInformation": {
"Name": "Steven Giesel",
"Heading": "Software Engineer",
Expand Down Expand Up @@ -73,6 +74,7 @@ The appsettings.json file has a lot of options to customize the content of the b
| ClientSecret | string | |
| LogoutUri | string | |
| BlogPostsPerPage | int | Gives the amount of blog posts loaded and display per page. For more the user has to use the navigation |
| FirstPageCacheDurationInMinutes | int | The duration in minutes the first page is cached. |
| AboutMeProfileInformation | node | Sets information for the About Me Page. If omitted the page is disabled completely |
| Name | string | Name, which is displayed on top of the profile card |
| Heading | string | Displayed under the name. For example job title |
Expand Down
2 changes: 2 additions & 0 deletions src/LinkDotNet.Blog.Web/ApplicationConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ public sealed record ApplicationConfiguration

public int BlogPostsPerPage { get; init; } = 10;

public int FirstPageCacheDurationInMinutes { get; init; } = 5;

public bool IsAboutMeEnabled { get; set; }

public bool IsGiscusEnabled { get; set; }
Expand Down
38 changes: 38 additions & 0 deletions src/LinkDotNet.Blog.Web/Features/Admin/Settings/SettingsPage.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
@page "/settings"
@using LinkDotNet.Blog.Web.Features.Services
@inject IOptions<ApplicationConfiguration> ApplicationConfiguration
@inject ICacheInvalidator CacheInvalidator
@inject IToastService ToastService
@attribute [Authorize]

<div class="container-fluid ms-3">
<h3>Settings</h3>
<table class="table table-responsive table-hover">
<thead>
<tr>
<th scope="col">Configuration Name</th>
<th scope="col">Description</th>
<th scope="col">Value</th>
<th scope="col">Actions</th>
</tr>
</thead>
<tbody>
<tr>
<td>Cache Duration (First Page)</td>
<td>Defines how long the first page remains cached before a refresh is required.<br/>
When a scheduled blog post is published, the cache is always invalidated.<br/>
The longer the cache lives, the longer it takes for the user to see updated content on the first page.</td>
<td>@ApplicationConfiguration.Value.FirstPageCacheDurationInMinutes Minutes</td>
<td><button class="btn btn-warning" id="invalidate-cache" @onclick="InvalidateCache">Invalidate Cache</button></td>
</tr>
</tbody>
</table>
</div>

@code {
private void InvalidateCache()
{
CacheInvalidator.Cancel();
ToastService.ShowInfo("Cache was invalidated.");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
@using LinkDotNet.Blog.Web.Features.Admin.Sitemap.Services
@inject ISitemapService SitemapService
@attribute [Authorize]
<h3 xmlns="http://www.w3.org/1999/html">Sitemap</h3>
<h3>Sitemap</h3>
<div class="row px-2">
<p>A sitemap is a file which lists all important links in a webpage. It helps crawler to find all of the
important pages. Especially newer sites benefit from having a sitemap.xml.
Expand Down Expand Up @@ -47,4 +47,4 @@
isGenerating = false;
await SitemapService.SaveSitemapToFileAsync(sitemapUrlSet);
}
}
}
13 changes: 11 additions & 2 deletions src/LinkDotNet.Blog.Web/Features/BlogPostPublisher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using LinkDotNet.Blog.Domain;
using LinkDotNet.Blog.Infrastructure;
using LinkDotNet.Blog.Infrastructure.Persistence;
using LinkDotNet.Blog.Web.Features.Services;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
Expand All @@ -14,10 +15,12 @@ public sealed partial class BlogPostPublisher : BackgroundService
{
private readonly IServiceProvider serviceProvider;
private readonly ILogger<BlogPostPublisher> logger;
private readonly ICacheInvalidator cacheInvalidator;

public BlogPostPublisher(IServiceProvider serviceProvider, ILogger<BlogPostPublisher> logger)
public BlogPostPublisher(IServiceProvider serviceProvider, ICacheInvalidator cacheInvalidator, ILogger<BlogPostPublisher> logger)
{
this.serviceProvider = serviceProvider;
this.cacheInvalidator = cacheInvalidator;
this.logger = logger;
}

Expand All @@ -44,12 +47,18 @@ private async Task PublishScheduledBlogPostsAsync()
using var scope = serviceProvider.CreateScope();
var repository = scope.ServiceProvider.GetRequiredService<IRepository<BlogPost>>();

foreach (var blogPost in await GetScheduledBlogPostsAsync(repository))
var blogPostsToPublish = await GetScheduledBlogPostsAsync(repository);
foreach (var blogPost in blogPostsToPublish)
{
blogPost.Publish();
await repository.StoreAsync(blogPost);
LogPublishedBlogPost(blogPost.Id);
}

if (blogPostsToPublish.Count > 0)
{
cacheInvalidator.Cancel();
}
}

private async Task<IPagedList<BlogPost>> GetScheduledBlogPostsAsync(IRepository<BlogPost> repository)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
<li><h6 class="dropdown-header">Blog posts</h6></li>
<li><a class="dropdown-item" href="create">Create new</a></li>
<li><a class="dropdown-item" href="draft">Show drafts</a></li>
<li><a class="dropdown-item" href="settings">Show settings</a></li>
<li><hr class="dropdown-divider"></li>
<li><h6 class="dropdown-header">Analytics</h6></li>
<li><a class="dropdown-item" href="dashboard">Dashboard</a></li>
Expand Down
7 changes: 6 additions & 1 deletion src/LinkDotNet.Blog.Web/Features/Home/Index.razor
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@
@using LinkDotNet.Blog.Infrastructure
@using LinkDotNet.Blog.Infrastructure.Persistence
@using LinkDotNet.Blog.Web.Features.Home.Components
@using LinkDotNet.Blog.Web.Features.Services
@using Microsoft.Extensions.Caching.Memory
@using Microsoft.Extensions.Primitives
@inject IMemoryCache MemoryCache
@inject ICacheTokenProvider CacheTokenProvider
@inject IRepository<BlogPost> BlogPostRepository
@inject IOptions<Introduction> Introduction
@inject IOptions<ApplicationConfiguration> AppConfiguration
Expand Down Expand Up @@ -61,7 +64,9 @@
{
currentPage = await MemoryCache.GetOrCreateAsync(firstPageCacheKey, async entry =>
{
entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5);
var cacheDuration = TimeSpan.FromMinutes(AppConfiguration.Value.FirstPageCacheDurationInMinutes);
entry.AbsoluteExpirationRelativeToNow = cacheDuration;
entry.AddExpirationToken(new CancellationChangeToken(CacheTokenProvider.Token));
return await GetAllForPageAsync(1);
});
return;
Expand Down
19 changes: 19 additions & 0 deletions src/LinkDotNet.Blog.Web/Features/Services/CacheService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using System;
using System.Threading;

namespace LinkDotNet.Blog.Web.Features.Services;

public sealed class CacheService : ICacheTokenProvider, ICacheInvalidator, IDisposable
{
private CancellationTokenSource cancellationTokenSource = new();

public CancellationToken Token => cancellationTokenSource.Token;

public void Cancel()
{
cancellationTokenSource.Cancel();
cancellationTokenSource = new();
}

public void Dispose() => cancellationTokenSource.Dispose();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace LinkDotNet.Blog.Web.Features.Services;

public interface ICacheInvalidator
{
void Cancel();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using System.Threading;

namespace LinkDotNet.Blog.Web.Features.Services;

public interface ICacheTokenProvider
{
CancellationToken Token { get; }
}
4 changes: 4 additions & 0 deletions src/LinkDotNet.Blog.Web/ServiceExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,9 @@ public static void RegisterServices(this IServiceCollection services)
services.AddScoped<ISitemapService, SitemapService>();
services.AddScoped<IXmlFileWriter, XmlFileWriter>();
services.AddScoped<IFileProcessor, FileProcessor>();

services.AddSingleton<CacheService>();
services.AddSingleton<ICacheTokenProvider>(s => s.GetRequiredService<CacheService>());
services.AddSingleton<ICacheInvalidator>(s => s.GetRequiredService<CacheService>());
}
}
1 change: 1 addition & 0 deletions src/LinkDotNet.Blog.Web/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"ClientSecret": ""
},
"BlogPostsPerPage": 10,
"FirstPageCacheDurationInMinutes": 10,
"ProfileInformation": {
"Name": "Steven Giesel",
"Heading": "Software Engineer",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using LinkDotNet.Blog.Domain;
using LinkDotNet.Blog.TestUtilities;
using LinkDotNet.Blog.Web.Features;
using LinkDotNet.Blog.Web.Features.Services;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

Expand All @@ -12,14 +13,17 @@ namespace LinkDotNet.Blog.IntegrationTests.Web.Features;
public sealed class BlogPostPublisherTests : SqlDatabaseTestBase<BlogPost>, IDisposable
{
private readonly BlogPostPublisher sut;
private readonly ICacheInvalidator cacheInvalidator;

public BlogPostPublisherTests()
{
var serviceProvider = new ServiceCollection()
.AddScoped(_ => Repository)
.BuildServiceProvider();

cacheInvalidator = Substitute.For<ICacheInvalidator>();

sut = new BlogPostPublisher(serviceProvider, Substitute.For<ILogger<BlogPostPublisher>>());
sut = new BlogPostPublisher(serviceProvider, cacheInvalidator, Substitute.For<ILogger<BlogPostPublisher>>());
}

[Fact]
Expand All @@ -39,6 +43,26 @@ public async Task ShouldPublishScheduledBlogPosts()
(await Repository.GetByIdAsync(bp2.Id)).IsPublished.Should().BeTrue();
(await Repository.GetByIdAsync(bp3.Id)).IsPublished.Should().BeFalse();
}

[Fact]
public async Task ShouldInvalidateCacheWhenPublishing()
{
var now = DateTime.Now;
var bp1 = new BlogPostBuilder().WithScheduledPublishDate(now.AddHours(-3)).IsPublished(false).Build();
await Repository.StoreAsync(bp1);

await sut.StartAsync(CancellationToken.None);

cacheInvalidator.Received().Cancel();
}

[Fact]
public async Task ShouldNotInvalidateCacheWhenThereIsNothingToPublish()
{
await sut.StartAsync(CancellationToken.None);

cacheInvalidator.DidNotReceive().Cancel();
}

public void Dispose() => sut?.Dispose();
}
Original file line number Diff line number Diff line change
Expand Up @@ -167,5 +167,6 @@ private void RegisterComponents(TestContextBase ctx, string profilePictureUri =
ctx.Services.AddScoped(_ => Repository);
ctx.Services.AddScoped(_ => Options.Create(CreateSampleAppConfiguration(profilePictureUri).ApplicationConfiguration));
ctx.Services.AddScoped(_ => Options.Create(CreateSampleAppConfiguration(profilePictureUri).Introduction));
ctx.Services.AddScoped(_ => Substitute.For<ICacheTokenProvider>());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using Blazored.Toast.Services;
using LinkDotNet.Blog.Web;
using LinkDotNet.Blog.Web.Features.Admin.Settings;
using LinkDotNet.Blog.Web.Features.Services;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;

namespace LinkDotNet.Blog.UnitTests.Web.Features.Admin.Settings;

public class SettingsPageTests : TestContext
{
[Fact]
public void GivenSettingsPage_WhenClicking_InvalidateCacheButton_TokenIsCancelled()
{
var cacheInvalidator = Substitute.For<ICacheInvalidator>();
Services.AddScoped(_ => cacheInvalidator);
Services.AddScoped(_ => Options.Create<ApplicationConfiguration>(new()));
Services.AddScoped(_ => Substitute.For<IToastService>());
var cut = RenderComponent<SettingsPage>();
var invalidateCacheButton = cut.Find("#invalidate-cache");

invalidateCacheButton.Click();

cacheInvalidator.Received(1).Cancel();
}
}

0 comments on commit c41e18e

Please sign in to comment.