Skip to content

Commit

Permalink
Added basic caching layer (#106)
Browse files Browse the repository at this point in the history
Co-authored-by: Tom Whittington <[email protected]>
  • Loading branch information
ThomasWhittington and Tom Whittington authored Jul 3, 2024
1 parent dc9458e commit da53160
Show file tree
Hide file tree
Showing 8 changed files with 165 additions and 7 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using Dfe.ContentSupport.Web.Configuration;
using Dfe.ContentSupport.Web.Http;
using Dfe.ContentSupport.Web.Models.Mapped;
using Dfe.ContentSupport.Web.Services;

namespace Dfe.ContentSupport.Web.Extensions;
Expand All @@ -12,7 +13,8 @@ public static void InitDependencyInjection(this WebApplicationBuilder app)
app.Configuration.GetSection("Contentful").Bind(contentfulOptions);
app.Services.AddSingleton(contentfulOptions);


app.Services
.AddTransient<ICacheService<List<CsPage>>, CsPagesCacheService>();
app.Services.AddTransient<IContentfulService, ContentfulService>();
app.Services.AddTransient<IContentService, ContentService>();

Expand Down
17 changes: 14 additions & 3 deletions src/Dfe.ContentSupport.Web/Services/ContentService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@

namespace Dfe.ContentSupport.Web.Services;

public class ContentService(IContentfulService contentfulService) : IContentService
public class ContentService(IContentfulService contentfulService, ICacheService<List<CsPage>> cache)
: IContentService
{
public async Task<CsPage?> GetContent(string slug, bool isPreview = false)
{
Expand Down Expand Up @@ -41,12 +42,22 @@ public async Task<List<CsPage>> GetCsPages()
return pages.ToList();
}

private async Task<List<CsPage>> GetContentSupportPages(
public async Task<List<CsPage>> GetContentSupportPages(
string field, string value, bool isPreview)
{
var key = $"{field}_{value}";
var fromCache = cache.GetFromCache(key);
if (fromCache is not null)
{
return fromCache;
}

var builder = QueryBuilder<ContentSupportPage>.New.ContentTypeIs(nameof(ContentSupportPage))
.FieldEquals($"fields.{field}", value);
var result = await contentfulService.ContentfulClient(isPreview).Query(builder);
return result.Select(page => new CsPage(page)).ToList();
var pages = result.Select(page => new CsPage(page)).ToList();

cache.AddToCache(key, pages);
return pages;
}
}
26 changes: 26 additions & 0 deletions src/Dfe.ContentSupport.Web/Services/CsPagesCacheService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using System.Diagnostics.CodeAnalysis;
using Dfe.ContentSupport.Web.Models.Mapped;
using Microsoft.Extensions.Caching.Memory;

namespace Dfe.ContentSupport.Web.Services;

[ExcludeFromCodeCoverage]
public class CsPagesCacheService(IMemoryCache cache, IConfiguration configuration)
: ICacheService<List<CsPage>>
{
private readonly int _cacheTimeOutMs = configuration.GetSection("CacheTimeOutMs").Get<int>();

public void AddToCache(string key, List<CsPage> item)
{
MemoryCacheEntryOptions options = new()
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromMilliseconds(_cacheTimeOutMs)
};
cache.Set(key, item, options);
}

public List<CsPage>? GetFromCache(string key)
{
return cache.Get<List<CsPage>>(key);
}
}
7 changes: 7 additions & 0 deletions src/Dfe.ContentSupport.Web/Services/ICacheService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace Dfe.ContentSupport.Web.Services;

public interface ICacheService<T>
{
void AddToCache(string key,T item);
T? GetFromCache(string key);
}
2 changes: 1 addition & 1 deletion src/Dfe.ContentSupport.Web/Views/Shared/_BetaHeader.cshtml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
@* <govuk-phase-banner-tag>Beta</govuk-phase-banner-tag> *@
<strong class="govuk-tag govuk-phase-banner__content__tag">Beta</strong>
This is a new service - your
<a href="https://forms.office.com/e/Jk5PuNWvGe" class="govuk-link" target="_blank" rel="noopener">
<a href="" class="govuk-link" target="_blank" rel="noopener">
feedback
</a> will
help us to improve it.
Expand Down
1 change: 1 addition & 0 deletions src/Dfe.ContentSupport.Web/appsettings.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"CacheTimeOutMs": 30000,
"Logging": {
"LogLevel": {
"Default": "Information",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using Dfe.ContentSupport.Web.Extensions;
using Dfe.ContentSupport.Web.Http;
using Dfe.ContentSupport.Web.Models.Mapped;
using Microsoft.AspNetCore.Builder;

namespace Dfe.ContentSupport.Web.Tests.Extensions;
Expand All @@ -16,7 +17,8 @@ public void Builder_Contains_Correct_Services()
{
typeof(IContentService),
typeof(IContentfulService),
typeof(IHttpContentfulClient)
typeof(IHttpContentfulClient),
typeof(ICacheService<List<CsPage>>)
};
foreach (var type in types)
builder.Services.Where(o => o.ServiceType == type).Should().ContainSingle();
Expand Down
111 changes: 110 additions & 1 deletion tests/Dfe.ContentSupport.Web.Tests/Services/ContentServiceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ namespace Dfe.ContentSupport.Web.Tests.Services;
public class ContentServiceTests
{
private readonly Mock<IHttpContentfulClient> _httpContentClientMock = new();
private readonly Mock<ICacheService<List<CsPage>>> _cacheMock = new();


private readonly ContentfulCollection<ContentSupportPage> _response = new()
Expand All @@ -24,7 +25,7 @@ public class ContentServiceTests

private ContentService GetService()
{
return new ContentService(GetClient());
return new ContentService(GetClient(), _cacheMock.Object);
}

private IContentfulService GetClient()
Expand Down Expand Up @@ -104,4 +105,112 @@ public async void GetCsPages_Calls_Client_Once()
Times.Once
);
}

[Fact]
public async void GetCsPages_Calls_Cache_Correct_Key()
{
const string expectedKey = "IsSitemap_true";
SetupResponse();
var sut = GetService();
await sut.GetCsPages();

_cacheMock.Verify(o => o.GetFromCache(expectedKey));
}


[Fact]
public async void GetContent_Calls_Cache_Correct_Key()
{
const string slug = "dummy-slug";
const string expectedKey = $"Slug_{slug}";
SetupResponse();
var sut = GetService();
await sut.GetContent(slug, It.IsAny<bool>());

_cacheMock.Verify(o => o.GetFromCache(expectedKey));
}

[Fact]
public async void GetCsPage_Calls_Cache_Correct_Key()
{
const string slug = "dummy-slug";
const string expectedKey = $"Slug_{slug}";
SetupResponse();
var sut = GetService();
await sut.GetContent(slug, It.IsAny<bool>());

_cacheMock.Verify(o => o.GetFromCache(expectedKey));
}

[Fact]
public async void GetContentSupportPages_Calls_Cache_Correct_Key()
{
const string field = "field";
const string value = "value";
SetupResponse();
var isPreview = It.IsAny<bool>();
const string expectedKey = $"{field}_{value}";
var sut = GetService();
await sut.GetContentSupportPages(field, value, isPreview);

_cacheMock.Verify(o => o.GetFromCache(expectedKey));
}

[Fact]
public async void GetContentSupportPages_GotCache_Returns_Cache()
{
var cacheValue = new List<CsPage> { It.IsAny<CsPage>() };

const string field = "field";
const string value = "value";
const string expectedKey = $"{field}_{value}";
var isPreview = It.IsAny<bool>();
_cacheMock.Setup(o => o.GetFromCache(expectedKey)).Returns(cacheValue);

var sut = GetService();
var result = await sut.GetContentSupportPages(field, value, isPreview);

result.Should().BeEquivalentTo(cacheValue);
}

[Fact]
public async void GetContentSupportPages_NotGotCache_Calls_Client()
{
List<CsPage>? cacheValue = null;

const string field = "field";
const string value = "value";
const string expectedKey = $"{field}_{value}";
SetupResponse();
var isPreview = It.IsAny<bool>();
_cacheMock.Setup(o => o.GetFromCache(expectedKey)).Returns(cacheValue);

var sut = GetService();
await sut.GetContentSupportPages(field, value, isPreview);

_httpContentClientMock.Verify(o =>
o.Query(
It.IsAny<QueryBuilder<ContentSupportPage>>(),
It.IsAny<CancellationToken>()),
Times.Once
);
}

[Fact]
public async void GetContentSupportPages_NotGotCache_AddsToCache()
{
List<CsPage>? cacheValue = null;

const string field = "field";
const string value = "value";
const string expectedKey = $"{field}_{value}";
SetupResponse();
var isPreview = It.IsAny<bool>();
_cacheMock.Setup(o => o.GetFromCache(expectedKey)).Returns(cacheValue);

var sut = GetService();
await sut.GetContentSupportPages(field, value, isPreview);

_cacheMock.Verify(o => o.AddToCache(expectedKey, It.IsAny<List<CsPage>>()), Times.Once);
}
}

0 comments on commit da53160

Please sign in to comment.