diff --git a/.github/workflows/deploy-image.yml b/.github/workflows/deploy-image.yml
index 84b32cd64..ccb7d527b 100644
--- a/.github/workflows/deploy-image.yml
+++ b/.github/workflows/deploy-image.yml
@@ -117,13 +117,40 @@ jobs:
--output none
&> /dev/null
+ flush-redis-cache:
+ runs-on: ubuntu-22.04
+ name: Flush Redis Cache on ${{ inputs.environment }}
+ environment: ${{ inputs.environment }}
+ needs: [upgrade-database, pull-image-from-gcr-and-publish-to-acr, deploy-image]
+
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ ref: ${{ github.ref }}
+
+ - name: Azure CLI Login
+ uses: ./.github/actions/azure-login
+ with:
+ az_tenant_id: ${{ secrets.AZ_TENANT_ID }}
+ az_subscription_id: ${{ secrets.AZ_SUBSCRIPTION_ID }}
+ az_client_id: ${{ secrets.AZ_CLIENT_ID }}
+ az_client_secret: ${{ secrets.AZ_CLIENT_SECRET }}
+
+ - name: Flush Redis Cache
+ uses: azure/CLI@v1
+ id: azure
+ with:
+ azcliversion: 2.67.0
+ inlineScript: |
+ az redis flush --name ${{ secrets.AZ_ENVIRONMENT }}${{ secrets.DFE_PROJECT_NAME }} --resource-group ${{ secrets.AZ_ENVIRONMENT }}${{ secrets.DFE_PROJECT_NAME }} --yes
+
run-e2e-testing:
runs-on: ubuntu-22.04
name: Clear Database & Run E2E Tests on ${{ inputs.environment }}
if: ${{ inputs.environment == 'tst' || inputs.environment == 'staging' }}
environment: ${{ inputs.environment }}
needs:
- [upgrade-database, pull-image-from-gcr-and-publish-to-acr, deploy-image]
+ [upgrade-database, pull-image-from-gcr-and-publish-to-acr, deploy-image, flush-redis-cache]
env:
az_keyvault_name: ${{ secrets.AZ_ENVIRONMENT }}${{ secrets.DFE_PROJECT_NAME }}-kv
az_keyvault_database_connectionstring_name: ${{ secrets.AZ_KEYVAULT_DATABASE_CONNECTIONSTRING_NAME }}
diff --git a/src/Dfe.PlanTech.Application/Caching/Interfaces/ICmsCache.cs b/src/Dfe.PlanTech.Application/Caching/Interfaces/ICmsCache.cs
index c8b78157d..ffb287431 100644
--- a/src/Dfe.PlanTech.Application/Caching/Interfaces/ICmsCache.cs
+++ b/src/Dfe.PlanTech.Application/Caching/Interfaces/ICmsCache.cs
@@ -10,6 +10,7 @@ public interface ICmsCache : IDistributedCache
/// Then removes the dependency array itself
///
/// Id of component to invalidate dependencies of
+ /// Name of the content type of the component being invalidated
///
- Task InvalidateCacheAsync(string contentComponentId);
+ Task InvalidateCacheAsync(string contentComponentId, string contentType);
}
diff --git a/src/Dfe.PlanTech.Application/Content/Queries/GetEntityFromContentfulQuery.cs b/src/Dfe.PlanTech.Application/Content/Queries/GetEntityFromContentfulQuery.cs
index 09eaef93c..208bd7f64 100644
--- a/src/Dfe.PlanTech.Application/Content/Queries/GetEntityFromContentfulQuery.cs
+++ b/src/Dfe.PlanTech.Application/Content/Queries/GetEntityFromContentfulQuery.cs
@@ -1,4 +1,3 @@
-using Dfe.PlanTech.Application.Caching.Interfaces;
using Dfe.PlanTech.Application.Core;
using Dfe.PlanTech.Application.Persistence.Interfaces;
using Dfe.PlanTech.Domain.Content.Interfaces;
@@ -16,12 +15,10 @@ public class GetEntityFromContentfulQuery : ContentRetriever, IGetEntityFromCont
public const string ExceptionMessageEntityContentful = "Error fetching Entity from Contentful";
private readonly ILogger _logger;
- private readonly ICmsCache _cache;
- public GetEntityFromContentfulQuery(ILogger logger, IContentRepository repository, ICmsCache cache) : base(repository)
+ public GetEntityFromContentfulQuery(ILogger logger, IContentRepository repository) : base(repository)
{
_logger = logger;
- _cache = cache;
}
public async Task GetEntityById(string contentId, CancellationToken cancellationToken = default)
@@ -29,8 +26,7 @@ public GetEntityFromContentfulQuery(ILogger logger
{
try
{
- return await _cache.GetOrCreateAsync($"Entity:{contentId}",
- () => repository.GetEntityById(contentId, cancellationToken: cancellationToken));
+ return await repository.GetEntityById(contentId, cancellationToken: cancellationToken);
}
catch (Exception ex)
{
diff --git a/src/Dfe.PlanTech.Application/Content/Queries/GetNavigationQuery.cs b/src/Dfe.PlanTech.Application/Content/Queries/GetNavigationQuery.cs
index c3d1e6cd7..f0ed667b5 100644
--- a/src/Dfe.PlanTech.Application/Content/Queries/GetNavigationQuery.cs
+++ b/src/Dfe.PlanTech.Application/Content/Queries/GetNavigationQuery.cs
@@ -1,5 +1,3 @@
-
-using Dfe.PlanTech.Application.Caching.Interfaces;
using Dfe.PlanTech.Application.Core;
using Dfe.PlanTech.Application.Persistence.Interfaces;
using Dfe.PlanTech.Domain.Content.Interfaces;
@@ -16,20 +14,17 @@ public class GetNavigationQuery : ContentRetriever, IGetNavigationQuery
public const string ExceptionMessageContentful = "Error getting navigation links from Contentful";
private readonly ILogger _logger;
- private readonly ICmsCache _cache;
- public GetNavigationQuery(ILogger logger, IContentRepository repository, ICmsCache cache) : base(repository)
+ public GetNavigationQuery(ILogger logger, IContentRepository repository) : base(repository)
{
_logger = logger;
- _cache = cache;
}
public async Task> GetNavigationLinks(CancellationToken cancellationToken = default)
{
try
{
- var navigationLinks = await _cache.GetOrCreateAsync("NavigationLinks", () => repository.GetEntities(cancellationToken)) ?? [];
- return navigationLinks;
+ return await repository.GetEntities(cancellationToken);
}
catch (Exception ex)
{
@@ -42,7 +37,7 @@ public async Task> GetNavigationLinks(CancellationT
{
try
{
- return await _cache.GetOrCreateAsync($"NavigationLink:{contentId}", () => repository.GetEntityById(contentId, cancellationToken: cancellationToken));
+ return await repository.GetEntityById(contentId, cancellationToken: cancellationToken);
}
catch (Exception ex)
{
diff --git a/src/Dfe.PlanTech.Application/Content/Queries/GetPageQuery.cs b/src/Dfe.PlanTech.Application/Content/Queries/GetPageQuery.cs
index 922bc7d46..c854fdc55 100644
--- a/src/Dfe.PlanTech.Application/Content/Queries/GetPageQuery.cs
+++ b/src/Dfe.PlanTech.Application/Content/Queries/GetPageQuery.cs
@@ -1,4 +1,3 @@
-using Dfe.PlanTech.Application.Caching.Interfaces;
using Dfe.PlanTech.Application.Exceptions;
using Dfe.PlanTech.Application.Persistence.Interfaces;
using Dfe.PlanTech.Application.Persistence.Models;
@@ -12,14 +11,12 @@ namespace Dfe.PlanTech.Application.Content.Queries;
public class GetPageQuery : IGetPageQuery
{
- private readonly ICmsCache _cache;
private readonly IContentRepository _repository;
private readonly ILogger _logger;
private readonly GetPageFromContentfulOptions _options;
- public GetPageQuery(ICmsCache cache, IContentRepository repository, ILogger logger, GetPageFromContentfulOptions options)
+ public GetPageQuery(IContentRepository repository, ILogger logger, GetPageFromContentfulOptions options)
{
- _cache = cache;
_repository = repository;
_logger = logger;
_options = options;
@@ -52,8 +49,7 @@ public GetPageQuery(ICmsCache cache, IContentRepository repository, ILogger
- _repository.GetEntityById(pageId, cancellationToken: cancellationToken));
+ return await _repository.GetEntityById(pageId, cancellationToken: cancellationToken);
}
catch (Exception ex)
{
@@ -67,7 +63,7 @@ public GetPageQuery(ICmsCache cache, IContentRepository repository, ILogger _repository.GetEntities(options, cancellationToken)) ?? [];
+ var pages = await _repository.GetEntities(options, cancellationToken);
var page = pages.FirstOrDefault();
diff --git a/src/Dfe.PlanTech.Application/Content/Queries/GetSubTopicRecommendationQuery.cs b/src/Dfe.PlanTech.Application/Content/Queries/GetSubTopicRecommendationQuery.cs
index c4929b277..4f2b921b5 100644
--- a/src/Dfe.PlanTech.Application/Content/Queries/GetSubTopicRecommendationQuery.cs
+++ b/src/Dfe.PlanTech.Application/Content/Queries/GetSubTopicRecommendationQuery.cs
@@ -1,4 +1,3 @@
-using Dfe.PlanTech.Application.Caching.Interfaces;
using Dfe.PlanTech.Application.Persistence.Interfaces;
using Dfe.PlanTech.Application.Persistence.Models;
using Dfe.PlanTech.Domain.Content.Interfaces;
@@ -11,14 +10,12 @@
namespace Dfe.PlanTech.Application.Content.Queries;
public class GetSubTopicRecommendationQuery(IContentRepository repository,
- ILogger logger,
- ICmsCache cache) : IGetSubTopicRecommendationQuery
+ ILogger logger) : IGetSubTopicRecommendationQuery
{
public async Task GetSubTopicRecommendation(string subtopicId, CancellationToken cancellationToken = default)
{
var options = CreateGetEntityOptions(subtopicId);
- var subTopicRecommendations = await cache.GetOrCreateAsync($"SubtopicRecommendation:{subtopicId}",
- () => repository.GetEntities(options, cancellationToken)) ?? [];
+ var subTopicRecommendations = await repository.GetEntities(options, cancellationToken);
var subtopicRecommendation = subTopicRecommendations.FirstOrDefault();
@@ -35,8 +32,7 @@ public class GetSubTopicRecommendationQuery(IContentRepository repository,
var options = CreateGetEntityOptions(subtopicId, 2);
options.Select = ["fields.intros", "sys"];
- var subtopicRecommendations = await cache.GetOrCreateAsync($"RecommendationViewDto:{subtopicId}",
- () => repository.GetEntities(options, cancellationToken)) ?? [];
+ var subtopicRecommendations = await repository.GetEntities(options, cancellationToken);
var subtopicRecommendation = subtopicRecommendations.FirstOrDefault();
diff --git a/src/Dfe.PlanTech.Application/Persistence/Commands/WebhookMessageProcessor.cs b/src/Dfe.PlanTech.Application/Persistence/Commands/WebhookMessageProcessor.cs
index c8d729789..73c3917cb 100644
--- a/src/Dfe.PlanTech.Application/Persistence/Commands/WebhookMessageProcessor.cs
+++ b/src/Dfe.PlanTech.Application/Persistence/Commands/WebhookMessageProcessor.cs
@@ -18,7 +18,7 @@ public async Task ProcessMessage(string subject, string body,
{
var payload = MapMessageToPayload(body);
- await cache.InvalidateCacheAsync(payload.Sys.Id);
+ await cache.InvalidateCacheAsync(payload.Sys.Id, payload.ContentType);
return new ServiceBusSuccessResult();
}
catch (Exception ex) when (ex is JsonException)
diff --git a/src/Dfe.PlanTech.Application/Persistence/Interfaces/IContentRepository.cs b/src/Dfe.PlanTech.Application/Persistence/Interfaces/IContentRepository.cs
index f12b155df..1b0b9c8c0 100644
--- a/src/Dfe.PlanTech.Application/Persistence/Interfaces/IContentRepository.cs
+++ b/src/Dfe.PlanTech.Application/Persistence/Interfaces/IContentRepository.cs
@@ -1,3 +1,4 @@
+using Dfe.PlanTech.Application.Persistence.Models;
using Dfe.PlanTech.Domain.Persistence.Interfaces;
namespace Dfe.PlanTech.Application.Persistence.Interfaces;
@@ -18,14 +19,12 @@ public interface IContentRepository
Task GetEntityById(string id, int include = 2, CancellationToken cancellationToken = default);
///
- /// Get all entities of the specified type.
+ /// Get options to use for fetching an Entity by Id
///
- ///
- /// Additional filtere
- ///
- ///
+ ///
+ ///
///
- Task> GetEntities(string entityTypeId, IGetEntitiesOptions? options, CancellationToken cancellationToken = default);
+ GetEntitiesOptions GetEntityByIdOptions(string id, int include = 2);
///
/// Get all entities of the specified type, using the name of the generic parameter's type as the entity type id (to lower case).
diff --git a/src/Dfe.PlanTech.Application/Persistence/Models/GetEntitiesOptions.cs b/src/Dfe.PlanTech.Application/Persistence/Models/GetEntitiesOptions.cs
index 04a0320e6..b6665e8d6 100644
--- a/src/Dfe.PlanTech.Application/Persistence/Models/GetEntitiesOptions.cs
+++ b/src/Dfe.PlanTech.Application/Persistence/Models/GetEntitiesOptions.cs
@@ -1,4 +1,6 @@
+using System.Text;
using Dfe.PlanTech.Domain.Persistence.Interfaces;
+using Dfe.PlanTech.Infrastructure.Application.Models;
namespace Dfe.PlanTech.Application.Persistence.Models;
@@ -29,4 +31,35 @@ public GetEntitiesOptions()
public IEnumerable? Queries { get; init; }
public int Include { get; init; } = 2;
+
+ public string SerializeToRedisFormat()
+ {
+ var builder = new StringBuilder();
+
+ builder.Append(":Include=");
+ builder.Append(Include);
+
+ if (Select != null && Select.Any())
+ builder.Append($":Select=[{string.Join(",", Select)}]");
+
+ if (Queries != null && Queries.Any())
+ {
+ builder.Append(":Queries=[");
+ foreach (var query in Queries)
+ {
+ builder.Append(query.Field);
+
+ if (query is ContentQueryEquals queryEquals)
+ builder.Append($"={queryEquals.Value}");
+ else if (query is ContentQueryIncludes queryIncludes)
+ builder.Append($"=[{string.Join(',', queryIncludes.Value)}]");
+
+ builder.Append(',');
+ }
+ builder.Length--;
+ builder.Append(']');
+ }
+
+ return builder.ToString();
+ }
}
diff --git a/src/Dfe.PlanTech.Application/Questionnaire/Queries/GetSectionQuery.cs b/src/Dfe.PlanTech.Application/Questionnaire/Queries/GetSectionQuery.cs
index 9099cf03c..f3a167ddd 100644
--- a/src/Dfe.PlanTech.Application/Questionnaire/Queries/GetSectionQuery.cs
+++ b/src/Dfe.PlanTech.Application/Questionnaire/Queries/GetSectionQuery.cs
@@ -1,4 +1,3 @@
-using Dfe.PlanTech.Application.Caching.Interfaces;
using Dfe.PlanTech.Application.Core;
using Dfe.PlanTech.Application.Exceptions;
using Dfe.PlanTech.Application.Persistence.Interfaces;
@@ -12,11 +11,9 @@ namespace Dfe.PlanTech.Application.Questionnaire.Queries;
public class GetSectionQuery : ContentRetriever, IGetSectionQuery
{
public const string SlugFieldPath = "fields.interstitialPage.fields.slug";
- private readonly ICmsCache _cache;
- public GetSectionQuery(IContentRepository repository, ICmsCache cache) : base(repository)
+ public GetSectionQuery(IContentRepository repository) : base(repository)
{
- _cache = cache;
}
public async Task GetSectionBySlug(string sectionSlug, CancellationToken cancellationToken = default)
@@ -40,7 +37,7 @@ public GetSectionQuery(IContentRepository repository, ICmsCache cache) : base(re
try
{
- var sections = await _cache.GetOrCreateAsync($"Section:{sectionSlug}", () => repository.GetEntities(options, cancellationToken)) ?? [];
+ var sections = await repository.GetEntities(options, cancellationToken);
return sections.FirstOrDefault();
}
catch (Exception ex)
@@ -56,13 +53,9 @@ public GetSectionQuery(IContentRepository repository, ICmsCache cache) : base(re
{
try
{
- var sections = await _cache.GetOrCreateAsync("Sections", async () =>
- {
- var options = new GetEntitiesOptions(include: 3);
- var sections = await repository.GetEntities(options, cancellationToken);
- return sections.Select(MapSection);
- }) ?? [];
- return sections;
+ var options = new GetEntitiesOptions(include: 3);
+ var sections = await repository.GetEntities(options, cancellationToken);
+ return sections.Select(MapSection);
}
catch (Exception ex)
{
diff --git a/src/Dfe.PlanTech.Domain/Extensions/StringHelpers.cs b/src/Dfe.PlanTech.Domain/Extensions/StringHelpers.cs
index 1db229ed1..7f4df562f 100644
--- a/src/Dfe.PlanTech.Domain/Extensions/StringHelpers.cs
+++ b/src/Dfe.PlanTech.Domain/Extensions/StringHelpers.cs
@@ -21,4 +21,6 @@ private static string ReplaceWhitespaceCharacters(this string text, string repla
private static string ReplaceNonSlugCharacters(this string text, string replacement)
=> MatchNonAlphaNumericExceptHyphensPattern().Replace(text, replacement);
+
+ public static string FirstCharToLower(this string input) => char.ToLower(input[0]) + input[1..];
}
diff --git a/src/Dfe.PlanTech.Domain/Persistence/Interfaces/IGetEntitiesOptions.cs b/src/Dfe.PlanTech.Domain/Persistence/Interfaces/IGetEntitiesOptions.cs
index 0291bc9d3..063679467 100644
--- a/src/Dfe.PlanTech.Domain/Persistence/Interfaces/IGetEntitiesOptions.cs
+++ b/src/Dfe.PlanTech.Domain/Persistence/Interfaces/IGetEntitiesOptions.cs
@@ -19,4 +19,6 @@ public interface IGetEntitiesOptions
/// If null, return all
///
public IEnumerable? Select { get; set; }
+
+ public string SerializeToRedisFormat();
}
diff --git a/src/Dfe.PlanTech.Infrastructure.Contentful/Helpers/ContentfulSetup.cs b/src/Dfe.PlanTech.Infrastructure.Contentful/Helpers/ContentfulSetup.cs
index 59b60dd47..75f966484 100644
--- a/src/Dfe.PlanTech.Infrastructure.Contentful/Helpers/ContentfulSetup.cs
+++ b/src/Dfe.PlanTech.Infrastructure.Contentful/Helpers/ContentfulSetup.cs
@@ -34,7 +34,8 @@ public static IServiceCollection SetupContentfulClient(this IServiceCollection s
services.AddTransient();
services.AddScoped();
- services.AddScoped();
+ services.AddKeyedScoped("contentfulRepository");
+ services.AddScoped();
services.SetupRichTextRenderer();
diff --git a/src/Dfe.PlanTech.Infrastructure.Contentful/Persistence/CachedContentfulRepository.cs b/src/Dfe.PlanTech.Infrastructure.Contentful/Persistence/CachedContentfulRepository.cs
new file mode 100644
index 000000000..7f682a8df
--- /dev/null
+++ b/src/Dfe.PlanTech.Infrastructure.Contentful/Persistence/CachedContentfulRepository.cs
@@ -0,0 +1,63 @@
+using Dfe.PlanTech.Application.Caching.Interfaces;
+using Dfe.PlanTech.Application.Persistence.Interfaces;
+using Dfe.PlanTech.Application.Persistence.Models;
+using Dfe.PlanTech.Domain.Persistence.Interfaces;
+using Dfe.PlanTech.Infrastructure.Contentful.Helpers;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Dfe.PlanTech.Infrastructure.Contentful.Persistence;
+
+///
+/// Encapsulates ContentfulClient functionality, whilst abstracting through the IEntityRepository interface
+///
+///
+public class CachedContentfulRepository : IContentRepository
+{
+ private readonly IContentRepository _contentRepository;
+ private readonly ICmsCache _cache;
+
+ public CachedContentfulRepository([FromKeyedServices("contentfulRepository")] IContentRepository contentRepository, ICmsCache cache)
+ {
+ _contentRepository = contentRepository ?? throw new ArgumentNullException(nameof(contentRepository));
+ _cache = cache ?? throw new ArgumentNullException(nameof(cache));
+ }
+
+ public async Task> GetEntities(CancellationToken cancellationToken = default)
+ {
+ string contentType = GetContentTypeName();
+ var key = $"{contentType}s";
+
+ return await _cache.GetOrCreateAsync(key, async () => await _contentRepository.GetEntities(cancellationToken)) ?? [];
+ }
+
+ public async Task> GetEntities(IGetEntitiesOptions options, CancellationToken cancellationToken = default)
+ {
+ var contentType = GetContentTypeName();
+ var jsonOptions = options.SerializeToRedisFormat();
+ var key = $"{contentType}{jsonOptions}";
+
+ return await _cache.GetOrCreateAsync(key, async () => await _contentRepository.GetEntities(options, cancellationToken)) ?? [];
+ }
+
+ public async Task GetEntityById(string id, int include = 2, CancellationToken cancellationToken = default)
+ {
+ var options = GetEntityByIdOptions(id, include);
+ var entities = (await GetEntities(options, cancellationToken)).ToList();
+
+ if (entities.Count > 1)
+ throw new GetEntitiesException($"Found more than 1 entity with id {id}");
+
+ return entities.FirstOrDefault();
+ }
+
+ public GetEntitiesOptions GetEntityByIdOptions(string id, int include = 2)
+ {
+ return _contentRepository.GetEntityByIdOptions(id, include);
+ }
+
+ private static string GetContentTypeName()
+ {
+ var name = typeof(TEntity).Name;
+ return name == "ContentSupportPage" ? name : name.FirstCharToLower();
+ }
+}
diff --git a/src/Dfe.PlanTech.Infrastructure.Contentful/Persistence/ContentfulRepository.cs b/src/Dfe.PlanTech.Infrastructure.Contentful/Persistence/ContentfulRepository.cs
index 12aa5627c..5a443131b 100644
--- a/src/Dfe.PlanTech.Infrastructure.Contentful/Persistence/ContentfulRepository.cs
+++ b/src/Dfe.PlanTech.Infrastructure.Contentful/Persistence/ContentfulRepository.cs
@@ -56,12 +56,23 @@ private static string CreateErrorString(ContentfulError error)
}
public async Task> GetEntities(CancellationToken cancellationToken = default)
- => await GetEntities(LowerCaseFirstLetter(typeof(TEntity).Name), null, cancellationToken);
+ => await GetEntities(GetContentTypeName(), null, cancellationToken);
public async Task> GetEntities(IGetEntitiesOptions options, CancellationToken cancellationToken = default)
- => await GetEntities(LowerCaseFirstLetter(typeof(TEntity).Name), options, cancellationToken);
+ => await GetEntities(GetContentTypeName(), options, cancellationToken);
public async Task GetEntityById(string id, int include = 2, CancellationToken cancellationToken = default)
+ {
+ var options = GetEntityByIdOptions(id, include);
+ var entities = (await GetEntities(options, cancellationToken)).ToList();
+
+ if (entities.Count > 1)
+ throw new GetEntitiesException($"Found more than 1 entity with id {id}");
+
+ return entities.FirstOrDefault();
+ }
+
+ public GetEntitiesOptions GetEntityByIdOptions(string id, int include = 2)
{
if (string.IsNullOrEmpty(id))
throw new ArgumentNullException(nameof(id));
@@ -70,29 +81,16 @@ public async Task> GetEntities(IGetEntitiesOptions
//option doesn't seem to have any effect there - it only seems to return the main parent entry
//with links to children. This was proving rather useless, so I have used the "GetEntries" option here
//instead.
- var options = new GetEntitiesOptions(include, new[] {
+ return new GetEntitiesOptions(include, new[] {
new ContentQueryEquals(){
Field = "sys.id",
Value = id
- }});
-
- var entities = (await GetEntities(options, cancellationToken)).ToList();
-
- if (entities.Count > 1)
- {
- throw new GetEntitiesException($"Found more than 1 entity with id {id}");
- }
-
- return entities.FirstOrDefault();
+ }});
}
- private static string LowerCaseFirstLetter(string input)
+ private static string GetContentTypeName()
{
- if (input == "ContentSupportPage")
- return input;
-
- char[] array = input.ToCharArray();
- array[0] = char.ToLower(array[0]);
- return new string(array);
+ var name = typeof(TEntity).Name;
+ return name == "ContentSupportPage" ? name : name.FirstCharToLower();
}
}
diff --git a/src/Dfe.PlanTech.Infrastructure.Redis/IRedisDependencyManager.cs b/src/Dfe.PlanTech.Infrastructure.Redis/IRedisDependencyManager.cs
index 009b1ae01..ed516e12d 100644
--- a/src/Dfe.PlanTech.Infrastructure.Redis/IRedisDependencyManager.cs
+++ b/src/Dfe.PlanTech.Infrastructure.Redis/IRedisDependencyManager.cs
@@ -7,6 +7,11 @@ namespace Dfe.PlanTech.Infrastructure.Redis;
///
public interface IRedisDependencyManager
{
+ ///
+ /// Key to use for dependencies of a content fetch that returns nothing
+ ///
+ string EmptyCollectionDependencyKey { get; }
+
///
/// Find and set dependencies for a given
///
diff --git a/src/Dfe.PlanTech.Infrastructure.Redis/RedisCache.cs b/src/Dfe.PlanTech.Infrastructure.Redis/RedisCache.cs
index db81907d0..0bae6a695 100644
--- a/src/Dfe.PlanTech.Infrastructure.Redis/RedisCache.cs
+++ b/src/Dfe.PlanTech.Infrastructure.Redis/RedisCache.cs
@@ -253,16 +253,26 @@ private static CacheResult CreateCacheResult(RedisValue redisResult)
}
///
- public Task InvalidateCacheAsync(string contentComponentId)
+ public Task InvalidateCacheAsync(string contentComponentId, string contentType)
=> _backgroundTaskService.QueueBackgroundWorkItemAsync(async (cancellationToken) =>
{
var key = _dependencyManager.GetDependencyKey(contentComponentId);
- var dependencies = await GetSetMembersAsync(key);
- foreach (var item in dependencies)
- {
- await RemoveAsync(item);
- }
+ await RemoveDependenciesAsync(key);
- await SetRemoveItemsAsync(key, dependencies);
+ // Invalidate all empty collections
+ await RemoveDependenciesAsync(_dependencyManager.EmptyCollectionDependencyKey);
+
+ // Invalidate collection of the content type if there is one
+ await RemoveAsync($"{contentType}s");
});
+
+ private async Task RemoveDependenciesAsync(string dependencyKey)
+ {
+ var dependencies = (await GetSetMembersAsync(dependencyKey)).ToList();
+ foreach (var item in dependencies)
+ {
+ await RemoveAsync(item);
+ }
+ await SetRemoveItemsAsync(dependencyKey, dependencies);
+ }
}
diff --git a/src/Dfe.PlanTech.Infrastructure.Redis/RedisDependencyManager.cs b/src/Dfe.PlanTech.Infrastructure.Redis/RedisDependencyManager.cs
index 84b2f9b07..e360484b4 100644
--- a/src/Dfe.PlanTech.Infrastructure.Redis/RedisDependencyManager.cs
+++ b/src/Dfe.PlanTech.Infrastructure.Redis/RedisDependencyManager.cs
@@ -8,6 +8,8 @@ namespace Dfe.PlanTech.Infrastructure.Redis;
/// To add dependency set operations to a queue for background processing
public class RedisDependencyManager(IBackgroundTaskQueue backgroundTaskQueue) : IRedisDependencyManager
{
+ public string EmptyCollectionDependencyKey => "Missing";
+
///
public Task RegisterDependenciesAsync(IDatabase database, string key, T value, CancellationToken cancellationToken = default)
=> backgroundTaskQueue.QueueBackgroundWorkItemAsync((cancellationToken) => GetAndSetDependencies(database, key, value));
@@ -26,6 +28,11 @@ private async Task GetAndSetDependencies(IDatabase database, string key, T va
{
var batch = database.CreateBatch();
var tasks = GetDependencies(value).Select(dependency => batch.SetAddAsync(GetDependencyKey(dependency), key, CommandFlags.FireAndForget)).ToArray();
+ if (tasks.Length == 0)
+ {
+ // If the value has no dependencies (is empty) it should be invalidated when new content comes in
+ tasks = tasks.Append(batch.SetAddAsync(EmptyCollectionDependencyKey, key, CommandFlags.FireAndForget)).ToArray();
+ }
batch.Execute();
await Task.WhenAll(tasks);
}
diff --git a/tests/Dfe.PlanTech.Application.UnitTests/Content/Queries/GetEntityFromContentfulQueryTests.cs b/tests/Dfe.PlanTech.Application.UnitTests/Content/Queries/GetEntityFromContentfulQueryTests.cs
index 148b77488..25407f03b 100644
--- a/tests/Dfe.PlanTech.Application.UnitTests/Content/Queries/GetEntityFromContentfulQueryTests.cs
+++ b/tests/Dfe.PlanTech.Application.UnitTests/Content/Queries/GetEntityFromContentfulQueryTests.cs
@@ -1,4 +1,3 @@
-using Dfe.PlanTech.Application.Caching.Interfaces;
using Dfe.PlanTech.Application.Content.Queries;
using Dfe.PlanTech.Application.Persistence.Interfaces;
using Dfe.PlanTech.Domain.Content.Models;
@@ -13,20 +12,13 @@ public class GetEntityFromContentfulQueryTests
{
private readonly IContentRepository _contentRepository = Substitute.For();
private readonly ILogger _logger = Substitute.For>();
- private readonly ICmsCache _cache = Substitute.For();
private readonly GetEntityFromContentfulQuery _getEntityFromContentfulQuery;
private readonly Question _firstQuestion = new() { Sys = new SystemDetails { Id = "question-1" } };
public GetEntityFromContentfulQueryTests()
{
- _getEntityFromContentfulQuery = new GetEntityFromContentfulQuery(_logger, _contentRepository, _cache);
- _cache.GetOrCreateAsync(Arg.Any(), Arg.Any>>())
- .Returns(callInfo =>
- {
- var func = callInfo.ArgAt>>(1);
- return func();
- });
+ _getEntityFromContentfulQuery = new GetEntityFromContentfulQuery(_logger, _contentRepository);
}
diff --git a/tests/Dfe.PlanTech.Application.UnitTests/Content/Queries/GetNavigationQueryTests.cs b/tests/Dfe.PlanTech.Application.UnitTests/Content/Queries/GetNavigationQueryTests.cs
index 053f3bbe8..ab9d2cb26 100644
--- a/tests/Dfe.PlanTech.Application.UnitTests/Content/Queries/GetNavigationQueryTests.cs
+++ b/tests/Dfe.PlanTech.Application.UnitTests/Content/Queries/GetNavigationQueryTests.cs
@@ -1,4 +1,3 @@
-using Dfe.PlanTech.Application.Caching.Interfaces;
using Dfe.PlanTech.Application.Content.Queries;
using Dfe.PlanTech.Application.Persistence.Interfaces;
using Dfe.PlanTech.Domain.Content.Models;
@@ -11,7 +10,6 @@ namespace Dfe.PlanTech.Application.UnitTests.Content.Queries;
public class GetNavigationQueryTests
{
private readonly IContentRepository _contentRepository = Substitute.For();
- private readonly ICmsCache _cache = Substitute.For();
private readonly NavigationLink _contentfulLink = new NavigationLink
{
@@ -20,28 +18,11 @@ public class GetNavigationQueryTests
};
private readonly IList _contentfulLinks;
-
private readonly ILogger _logger = Substitute.For>();
public GetNavigationQueryTests()
{
_contentfulLinks = new List { _contentfulLink };
-
- _cache.GetOrCreateAsync(Arg.Any(), Arg.Any?>>>())
- .Returns(callInfo =>
- {
- var func = callInfo.ArgAt?>>>(1);
- return func();
- });
-
- _cache.GetOrCreateAsync(
- Arg.Any(),
- Arg.Any>>())
- .Returns(callInfo =>
- {
- var func = callInfo.ArgAt>>(1);
- return func();
- });
}
[Fact]
@@ -49,7 +30,7 @@ public async Task Should_Retrieve_Nav_Links_From_Contentful()
{
_contentRepository.GetEntities(CancellationToken.None).Returns(_contentfulLinks);
- GetNavigationQuery navQuery = new(_logger, _contentRepository, _cache);
+ GetNavigationQuery navQuery = new(_logger, _contentRepository);
var result = await navQuery.GetNavigationLinks();
@@ -61,7 +42,7 @@ public async Task Should_LogError_When_Contentful_Exception()
{
_contentRepository.GetEntities(CancellationToken.None).Throws(_ => new Exception("Contentful error"));
- GetNavigationQuery navQuery = new(_logger, _contentRepository, _cache);
+ GetNavigationQuery navQuery = new(_logger, _contentRepository);
var result = await navQuery.GetNavigationLinks();
@@ -75,7 +56,7 @@ public async Task Should_LogError_When_Contentful_Exception()
public async Task Should_Retrieve_Nav_Link_By_Id_When_Exists()
{
_contentRepository.GetEntityById(Arg.Any(), Arg.Any(), Arg.Any()).Returns(_contentfulLink);
- var navQuery = new GetNavigationQuery(_logger, _contentRepository, _cache);
+ var navQuery = new GetNavigationQuery(_logger, _contentRepository);
var result = await navQuery.GetLinkById("contentId");
Assert.NotNull(result);
@@ -88,7 +69,7 @@ public async Task Should_Return_Null_When_Nav_Link_Does_Not_Exist()
{
_contentRepository.GetEntityById(Arg.Any(), Arg.Any(), cancellationToken: CancellationToken.None).Returns((NavigationLink?)null);
- var navQuery = new GetNavigationQuery(_logger, _contentRepository, _cache);
+ var navQuery = new GetNavigationQuery(_logger, _contentRepository);
var result = await navQuery.GetLinkById("NonExistentId");
diff --git a/tests/Dfe.PlanTech.Application.UnitTests/Content/Queries/GetPageQueryTests.cs b/tests/Dfe.PlanTech.Application.UnitTests/Content/Queries/GetPageQueryTests.cs
index bc9c7e0c8..0ccc03ed4 100644
--- a/tests/Dfe.PlanTech.Application.UnitTests/Content/Queries/GetPageQueryTests.cs
+++ b/tests/Dfe.PlanTech.Application.UnitTests/Content/Queries/GetPageQueryTests.cs
@@ -1,4 +1,3 @@
-using Dfe.PlanTech.Application.Caching.Interfaces;
using Dfe.PlanTech.Application.Content.Queries;
using Dfe.PlanTech.Application.Exceptions;
using Dfe.PlanTech.Application.Persistence.Interfaces;
@@ -21,7 +20,6 @@ public class GetPageQueryTests
private readonly IContentRepository _repoSubstitute = Substitute.For();
private readonly ILogger _logger = Substitute.For>();
- private readonly ICmsCache _cache = Substitute.For();
private readonly List _pages = new() {
new Page(){
@@ -56,18 +54,6 @@ public class GetPageQueryTests
public GetPageQueryTests()
{
SetupRepository();
- _cache.GetOrCreateAsync(Arg.Any(), Arg.Any?>>>())
- .Returns(callInfo =>
- {
- var func = callInfo.ArgAt?>>>(1);
- return func();
- });
- _cache.GetOrCreateAsync(Arg.Any(), Arg.Any>>())
- .Returns(callInfo =>
- {
- var func = callInfo.ArgAt>>(1);
- return func();
- });
}
private void SetupRepository()
@@ -98,7 +84,7 @@ private void SetupRepository()
}
private GetPageQuery CreateGetPageQuery()
- => new(_cache, _repoSubstitute, _logger, new GetPageFromContentfulOptions() { Include = 4 });
+ => new(_repoSubstitute, _logger, new GetPageFromContentfulOptions() { Include = 4 });
[Fact]
diff --git a/tests/Dfe.PlanTech.Application.UnitTests/Content/Queries/GetSubTopicRecommendationQueryTests.cs b/tests/Dfe.PlanTech.Application.UnitTests/Content/Queries/GetSubTopicRecommendationQueryTests.cs
index 1bbbe5023..2f3dd430a 100644
--- a/tests/Dfe.PlanTech.Application.UnitTests/Content/Queries/GetSubTopicRecommendationQueryTests.cs
+++ b/tests/Dfe.PlanTech.Application.UnitTests/Content/Queries/GetSubTopicRecommendationQueryTests.cs
@@ -1,4 +1,3 @@
-using Dfe.PlanTech.Application.Caching.Interfaces;
using Dfe.PlanTech.Application.Content.Queries;
using Dfe.PlanTech.Application.Persistence.Interfaces;
using Dfe.PlanTech.Application.Persistence.Models;
@@ -23,7 +22,6 @@ public class GetSubTopicRecommendationQueryTests
private readonly List _subtopicRecommendations = [];
private readonly ILogger _logger = Substitute.For>();
- private readonly ICmsCache _cache = Substitute.For();
public GetSubTopicRecommendationQueryTests()
{
@@ -135,13 +133,7 @@ public GetSubTopicRecommendationQueryTests()
_subtopicRecommendations.Add(_subtopicRecommendationOne);
_subtopicRecommendations.Add(_subtopicRecommendationTwo);
- _query = new(_repoSubstitute, _logger, _cache);
- _cache.GetOrCreateAsync(Arg.Any(), Arg.Any?>>>())
- .Returns(callInfo =>
- {
- var func = callInfo.ArgAt?>>>(1);
- return func();
- });
+ _query = new(_repoSubstitute, _logger);
}
[Fact]
diff --git a/tests/Dfe.PlanTech.Application.UnitTests/Persistence/Commands/WebhookMessageProcessorTests.cs b/tests/Dfe.PlanTech.Application.UnitTests/Persistence/Commands/WebhookMessageProcessorTests.cs
index 39f4e9cc8..f33fba1db 100644
--- a/tests/Dfe.PlanTech.Application.UnitTests/Persistence/Commands/WebhookMessageProcessorTests.cs
+++ b/tests/Dfe.PlanTech.Application.UnitTests/Persistence/Commands/WebhookMessageProcessorTests.cs
@@ -42,7 +42,7 @@ public async Task ProcessMessage_Should_Execute_Successfully()
var result = await _webhookMessageProcessor.ProcessMessage(subject, QuestionJsonBody, "message id", CancellationToken.None);
Assert.IsType(result);
- await _cache.Received(1).InvalidateCacheAsync(QuestionId);
+ await _cache.Received(1).InvalidateCacheAsync(QuestionId, "question");
}
[Fact]
diff --git a/tests/Dfe.PlanTech.Application.UnitTests/Persistence/GetEntitiesOptionsTests.cs b/tests/Dfe.PlanTech.Application.UnitTests/Persistence/GetEntitiesOptionsTests.cs
index 4b1603c0f..8acfa9c9b 100644
--- a/tests/Dfe.PlanTech.Application.UnitTests/Persistence/GetEntitiesOptionsTests.cs
+++ b/tests/Dfe.PlanTech.Application.UnitTests/Persistence/GetEntitiesOptionsTests.cs
@@ -6,6 +6,42 @@ namespace Dfe.PlanTech.Application.UnitTests.Persistence;
public class GetEntitiesOptionsTests
{
+ private readonly Dictionary _testData;
+
+ public GetEntitiesOptionsTests()
+ {
+ _testData = new Dictionary
+ {
+ { "Empty", new GetEntitiesOptions() },
+ { "Include", new GetEntitiesOptions(include: 4) },
+ {
+ "Select", new GetEntitiesOptions()
+ {
+ Select = ["field.intros", "field.sys"]
+ }
+ },
+ {
+ "Query", new GetEntitiesOptions(queries:
+ [
+ new ContentQueryEquals() { Field = "slug", Value = "/" },
+ new ContentQueryEquals() { Field = "id", Value = "1234" },
+ new ContentQueryIncludes() { Field = "toinclude", Value = ["value1", "value2"] }
+ ])
+ },
+ {
+ "Combined", new GetEntitiesOptions()
+ {
+ Select = ["field.intros", "field.sys"],
+ Queries =
+ [
+ new ContentQueryEquals() { Field = "slug", Value = "/test" },
+ ],
+ Include = 6
+ }
+ }
+ };
+ }
+
[Fact]
public void Should_Set_Include()
{
@@ -39,4 +75,17 @@ public void Should_Have_Default_Values()
Assert.Null(options.Queries);
}
+ [Theory]
+ [InlineData("Empty", ":Include=2")]
+ [InlineData("Select", ":Include=2:Select=[field.intros,field.sys]")]
+ [InlineData("Include", ":Include=4")]
+ [InlineData("Query", ":Include=2:Queries=[slug=/,id=1234,toinclude=[value1,value2]]")]
+ [InlineData("Combined", ":Include=6:Select=[field.intros,field.sys]:Queries=[slug=/test]")]
+ public void Should_Serialise_Options_Into_Suitable_Redis_Format(string testDataKey, string expectedValue)
+ {
+ var testData = _testData[testDataKey];
+ var serialized = testData.SerializeToRedisFormat();
+ Assert.Equal(expectedValue, serialized);
+ }
+
}
diff --git a/tests/Dfe.PlanTech.Application.UnitTests/Questionnaire/Queries/GetSectionQueryTests.cs b/tests/Dfe.PlanTech.Application.UnitTests/Questionnaire/Queries/GetSectionQueryTests.cs
index bee621373..bcaf808e7 100644
--- a/tests/Dfe.PlanTech.Application.UnitTests/Questionnaire/Queries/GetSectionQueryTests.cs
+++ b/tests/Dfe.PlanTech.Application.UnitTests/Questionnaire/Queries/GetSectionQueryTests.cs
@@ -1,4 +1,3 @@
-using Dfe.PlanTech.Application.Caching.Interfaces;
using Dfe.PlanTech.Application.Exceptions;
using Dfe.PlanTech.Application.Persistence.Interfaces;
using Dfe.PlanTech.Application.Persistence.Models;
@@ -75,17 +74,6 @@ public class GetSectionQueryTests
};
private readonly Section[] _sections = new[] { FirstSection, SecondSection, ThirdSection };
- private readonly ICmsCache _cache = Substitute.For();
-
- public GetSectionQueryTests()
- {
- _cache.GetOrCreateAsync(Arg.Any(), Arg.Any?>>>())
- .Returns(callInfo =>
- {
- var func = callInfo.ArgAt?>>>(1);
- return func();
- });
- }
[Fact]
public async Task GetSectionBySlug_Returns_Section_From_Repository()
@@ -108,7 +96,7 @@ public async Task GetSectionBySlug_Returns_Section_From_Repository()
return _sections.Where(section => section.InterstitialPage?.Slug == slugQuery.Value);
});
- var getSectionQuery = new GetSectionQuery(repository, _cache);
+ var getSectionQuery = new GetSectionQuery(repository);
await getSectionQuery.GetSectionBySlug(sectionSlug, cancellationToken);
Assert.Equal(FirstSection.InterstitialPage.Slug, sectionSlug);
@@ -129,7 +117,7 @@ public async Task GetSectionBySlug_ThrowsExceptionOnRepositoryError()
.When(repo => repo.GetEntities(Arg.Any(), cancellationToken))
.Throw(new Exception("Dummy Exception"));
- var getSectionQuery = new GetSectionQuery(repository, _cache);
+ var getSectionQuery = new GetSectionQuery(repository);
await Assert.ThrowsAsync(
async () => await getSectionQuery.GetSectionBySlug(sectionSlug, cancellationToken)
@@ -144,7 +132,7 @@ public async Task GetAllSections_ThrowsExceptionOnRepositoryError()
.When(repo => repo.GetEntities(Arg.Any(), Arg.Any()))
.Throw(new Exception("Dummy Exception"));
- var getSectionQuery = new GetSectionQuery(repository, _cache);
+ var getSectionQuery = new GetSectionQuery(repository);
await Assert.ThrowsAsync(
async () => await getSectionQuery.GetAllSections(CancellationToken.None)
@@ -158,7 +146,7 @@ public async Task GetAllSections_Should_Omit_NextQuestion_Answers()
repository.GetEntities(Arg.Any(), Arg.Any())
.Returns(_ => _sections);
- var getSectionQuery = new GetSectionQuery(repository, _cache);
+ var getSectionQuery = new GetSectionQuery(repository);
var sections = (await getSectionQuery.GetAllSections()).ToList();
Assert.Equal(_sections.Length, sections.Count);
diff --git a/tests/Dfe.PlanTech.Infrastructure.Contentful.UnitTests/Persistence/CachedContentfulRepositoryTests.cs b/tests/Dfe.PlanTech.Infrastructure.Contentful.UnitTests/Persistence/CachedContentfulRepositoryTests.cs
new file mode 100644
index 000000000..39bc0bbc2
--- /dev/null
+++ b/tests/Dfe.PlanTech.Infrastructure.Contentful.UnitTests/Persistence/CachedContentfulRepositoryTests.cs
@@ -0,0 +1,83 @@
+using Dfe.PlanTech.Application.Caching.Interfaces;
+using Dfe.PlanTech.Application.Persistence.Interfaces;
+using Dfe.PlanTech.Application.Persistence.Models;
+using Dfe.PlanTech.Domain.Questionnaire.Models;
+using Dfe.PlanTech.Infrastructure.Application.Models;
+using Dfe.PlanTech.Infrastructure.Contentful.Persistence;
+using NSubstitute;
+
+namespace Dfe.PlanTech.Infrastructure.Contentful.UnitTests.Persistence
+{
+ public class CachedContentfulRepositoryTests
+ {
+ private readonly IContentRepository _contentRepository = Substitute.For();
+ private readonly ICmsCache _cache = Substitute.For();
+ private readonly IContentRepository _cachedContentRepository;
+
+ public CachedContentfulRepositoryTests()
+ {
+ _cachedContentRepository = new CachedContentfulRepository(_contentRepository, _cache);
+ _cache.GetOrCreateAsync(Arg.Any(), Arg.Any>>())
+ .Returns(callInfo =>
+ {
+ var func = callInfo.ArgAt>>(1);
+ return func();
+ });
+ _cache.GetOrCreateAsync(Arg.Any(), Arg.Any?>>>())
+ .Returns(callInfo =>
+ {
+ var func = callInfo.ArgAt?>>>(1);
+ return func();
+ });
+ _contentRepository
+ .GetEntityByIdOptions(Arg.Any(), Arg.Any())
+ .Returns(callinfo =>
+ {
+ var id = callinfo.ArgAt(0);
+ var include = callinfo.ArgAt(1);
+ return new GetEntitiesOptions(include, [
+ new ContentQueryEquals()
+ {
+ Field = "sys.id",
+ Value = id
+ }
+ ]);
+ });
+ }
+
+ [Fact]
+ public async Task Should_Cache_GetEntities_Without_Options()
+ {
+ await _cachedContentRepository.GetEntities();
+ await _cache.Received(1).GetOrCreateAsync(Arg.Any(), Arg.Any>>>());
+ await _contentRepository.Received(1).GetEntities();
+ }
+
+ [Fact]
+ public async Task Should_Cache_GetEntities_With_Options()
+ {
+ var options = new GetEntitiesOptions(include: 3);
+ await _cachedContentRepository.GetEntities(options);
+ await _cache.Received(1).GetOrCreateAsync(Arg.Any(), Arg.Any>>>());
+ await _contentRepository.Received(1).GetEntities(options);
+ }
+
+ [Fact]
+ public async Task Should_Cache_GetEntityById()
+ {
+ var id = "test-id";
+ await _cachedContentRepository.GetEntityById(id);
+ await _cache.Received(1).GetOrCreateAsync(Arg.Any(), Arg.Any>>>());
+ await _contentRepository.Received(1).GetEntities(
+ Arg.Is(arg => ValidateGetEntitiesOptions(arg, id)));
+ }
+
+ private static bool ValidateGetEntitiesOptions(GetEntitiesOptions arg, string id)
+ {
+ var first = arg.Queries?.FirstOrDefault();
+ return first is ContentQueryEquals queryEquals &&
+ queryEquals.Field == "sys.id" &&
+ queryEquals.Value == id;
+ }
+ }
+}
diff --git a/tests/Dfe.PlanTech.Infrastructure.Redis.UnitTests/RedisCacheTestHelpers.cs b/tests/Dfe.PlanTech.Infrastructure.Redis.UnitTests/RedisCacheTestHelpers.cs
index 4140d0777..62f237aad 100644
--- a/tests/Dfe.PlanTech.Infrastructure.Redis.UnitTests/RedisCacheTestHelpers.cs
+++ b/tests/Dfe.PlanTech.Infrastructure.Redis.UnitTests/RedisCacheTestHelpers.cs
@@ -28,4 +28,5 @@ public static class RedisCacheTestHelpers
public static Answer FirstAnswer => _firstAnswer;
public static Answer SecondAnswer => _secondAnswer;
public static Question Question => _question;
+ public static List EmptyQuestionCollection => [];
}
diff --git a/tests/Dfe.PlanTech.Infrastructure.Redis.UnitTests/RedisCacheTests.cs b/tests/Dfe.PlanTech.Infrastructure.Redis.UnitTests/RedisCacheTests.cs
index df7aba6d9..1b0ba4fae 100644
--- a/tests/Dfe.PlanTech.Infrastructure.Redis.UnitTests/RedisCacheTests.cs
+++ b/tests/Dfe.PlanTech.Infrastructure.Redis.UnitTests/RedisCacheTests.cs
@@ -12,6 +12,7 @@ public class RedisCacheTests : RedisCacheTestsBase
private readonly IRedisConnectionManager _connectionManager = Substitute.For();
private readonly IRedisDependencyManager _dependencyManager = Substitute.For();
private readonly RedisCache _cache;
+ private readonly string _missingKey = "Missing";
public RedisCacheTests()
{
@@ -23,6 +24,12 @@ public RedisCacheTests()
);
_connectionManager.GetDatabaseAsync(Arg.Any()).Returns(Database);
+ _dependencyManager.EmptyCollectionDependencyKey.Returns(_missingKey);
+ _dependencyManager.GetDependencyKey(Arg.Any()).Returns(callinfo =>
+ {
+ var arg = callinfo.ArgAt(0);
+ return $"Dependency:{arg}";
+ });
}
[Fact]
@@ -183,10 +190,13 @@ public async Task InvalidateCacheAsync_RemovesAllDependencies()
{
var firstAnswerKey = _dependencyManager.GetDependencyKey(RedisCacheTestHelpers.FirstAnswer.Sys.Id);
var keys = new List { "one", "two", "three" };
+ var missingKeys = new List { "four" };
var dependencies = keys.Select(dep => new RedisValue(dep)).ToArray();
+ var missingDependencies = missingKeys.Select(dep => new RedisValue(dep)).ToArray();
Database.SetMembersAsync(firstAnswerKey, Arg.Any()).Returns(dependencies);
- await _cache.InvalidateCacheAsync(RedisCacheTestHelpers.FirstAnswer.Sys.Id);
+ Database.SetMembersAsync(_missingKey, Arg.Any()).Returns(missingDependencies);
+ await _cache.InvalidateCacheAsync(RedisCacheTestHelpers.FirstAnswer.Sys.Id, "answer");
Assert.NotNull(QueuedFunc);
@@ -195,5 +205,14 @@ await Database.Received(1).SetRemoveAsync(
firstAnswerKey,
Arg.Is(arg => dependencies.All(dep => arg.Contains(dep))),
Arg.Any());
+
+ await Database.Received(1).SetRemoveAsync(
+ _missingKey,
+ Arg.Is(arg => missingDependencies.All(dep => arg.Contains(dep))),
+ Arg.Any());
+
+ await Database.Received(1).KeyDeleteAsync(
+ "answers",
+ Arg.Any());
}
}
diff --git a/tests/Dfe.PlanTech.Infrastructure.Redis.UnitTests/RedisDependencyManagerTests.cs b/tests/Dfe.PlanTech.Infrastructure.Redis.UnitTests/RedisDependencyManagerTests.cs
index c12b7cf3c..77c4f4b91 100644
--- a/tests/Dfe.PlanTech.Infrastructure.Redis.UnitTests/RedisDependencyManagerTests.cs
+++ b/tests/Dfe.PlanTech.Infrastructure.Redis.UnitTests/RedisDependencyManagerTests.cs
@@ -30,6 +30,21 @@ public async Task RegisterDependencyAsync_StoresAllContentIds()
await batch.Received(1).SetAddAsync(_dependencyManager.GetDependencyKey(RedisCacheTestHelpers.SecondAnswer.Sys.Id), Key, CommandFlags.FireAndForget);
}
+ [Fact]
+ public async Task RegisterDependencyAsync_StoresEmptyCollectionsUnderMissing()
+ {
+ var batch = Substitute.For();
+ Database.CreateBatch().Returns(batch);
+
+ await _dependencyManager.RegisterDependenciesAsync(Database, Key, RedisCacheTestHelpers.EmptyQuestionCollection);
+
+ Assert.NotNull(QueuedFunc);
+
+ await QueuedFunc(default);
+
+ await batch.Received(1).SetAddAsync(_dependencyManager.EmptyCollectionDependencyKey, Key, CommandFlags.FireAndForget);
+ }
+
[Fact]
public void GetDependencies_ThrowsException_When_Value_NotIContentComponent()
{