From 15fbc03201fa526f032061f18ff393d32f91d9cd Mon Sep 17 00:00:00 2001 From: Jim Washbrook Date: Mon, 18 Nov 2024 17:07:53 +0000 Subject: [PATCH] feat: add IContentComponent to C&S contents tests: add FooterLinkTagHelper changes unit tests chore: Linted code for plan-technology-for-your-school.sln solution chore: sonarcloud fix chore: add .sonarqube to gitignore fix: handle prefixed "/" chore formatting fix: expected url fix: expected url fix: test fix: Look in all assemblies for IContentComponent types wip chore: add text fix: unit tests, tweak validation fix(redis): Handle serialisation of IContentComponent properties chore: formatting fix: handle redisc onnection failures better fix tests fixes + refactor --- .../Models/ContentComponentJsonExtensions.cs | 50 +++++++------------ .../Helpers/ReflectionHelpers.cs | 27 +++++++--- .../JsonSerialiser.cs | 5 +- .../RedisCache.cs | 26 ++++++---- .../ContentComponentJsonExtensionsTests.cs | 14 ++++-- .../RedisConnectionManagerTests.cs | 0 6 files changed, 65 insertions(+), 57 deletions(-) delete mode 100644 tests/Dfe.PlanTech.Infrastructure.Redis.UnitTests/RedisConnectionManagerTests.cs diff --git a/src/Dfe.PlanTech.Domain/Content/Models/ContentComponentJsonExtensions.cs b/src/Dfe.PlanTech.Domain/Content/Models/ContentComponentJsonExtensions.cs index d61ed0ec6..35d791fbb 100644 --- a/src/Dfe.PlanTech.Domain/Content/Models/ContentComponentJsonExtensions.cs +++ b/src/Dfe.PlanTech.Domain/Content/Models/ContentComponentJsonExtensions.cs @@ -9,53 +9,37 @@ namespace Dfe.PlanTech.Domain.Content.Models; /// public static class ContentComponentJsonExtensions { - private const string TypeDiscriminatorName = "$contentcomponenttype"; - /// - /// Gets all classes that inherit from and creates mapping information for deserialisation + /// Gets all types inheriting that would be valid for serialisation /// - private static readonly List ContentComponentTypes = ReflectionHelpers - .GetTypesInheritingFrom() - .Select(type => new JsonDerivedType(type, type.Name)) - .ToList(); - - private static readonly Type ContentComponentType = typeof(ContentComponent); - - private static JsonPolymorphismOptions? _contentComponentPolymorphismOptions = null; - - private static JsonPolymorphismOptions ContentComponentPolymorphismOptions => - _contentComponentPolymorphismOptions ??= CreateJsonPolymorphismOptions(); + /// + /// + private static List GetInheritingTypes(Type type) => [.. ReflectionHelpers + .GetTypesInheritingFrom(type) + .Where(derivedType => derivedType != type && derivedType.IsConcreteClass() && derivedType.HasParameterlessConstructor()) + .Select(type => new JsonDerivedType(type, type.Name))]; /// - /// Creates polymorphism support for the class. + /// Adds polymorphism support for the class. /// - /// - private static JsonPolymorphismOptions CreateJsonPolymorphismOptions() + /// + public static void AddContentComponentPolymorphicInfo(JsonTypeInfo jsonTypeInfo) { - var options = new JsonPolymorphismOptions() + if (jsonTypeInfo.Type != typeof(TType)) + return; + + var options = new JsonPolymorphismOptions { - TypeDiscriminatorPropertyName = TypeDiscriminatorName, + TypeDiscriminatorPropertyName = $"${typeof(TType).Name.ToLower()}", IgnoreUnrecognizedTypeDiscriminators = true, UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FailSerialization, }; - foreach (var derivedType in ContentComponentTypes) + foreach (var derivedType in GetInheritingTypes(typeof(TType))) { options.DerivedTypes.Add(derivedType); } - return options; - } - - /// - /// Adds polymorphism support for the class. - /// - /// - public static void AddContentComponentPolymorphicInfo(JsonTypeInfo jsonTypeInfo) - { - if (jsonTypeInfo.Type != ContentComponentType) - return; - - jsonTypeInfo.PolymorphismOptions = ContentComponentPolymorphismOptions; + jsonTypeInfo.PolymorphismOptions = options; } } diff --git a/src/Dfe.PlanTech.Domain/Helpers/ReflectionHelpers.cs b/src/Dfe.PlanTech.Domain/Helpers/ReflectionHelpers.cs index 8d1b425ca..c14ab5cb9 100644 --- a/src/Dfe.PlanTech.Domain/Helpers/ReflectionHelpers.cs +++ b/src/Dfe.PlanTech.Domain/Helpers/ReflectionHelpers.cs @@ -1,13 +1,24 @@ +using System.Reflection; + namespace Dfe.PlanTech.Domain.Helpers; public static class ReflectionHelpers { - public static IEnumerable GetTypesInheritingFrom() - { - var baseType = typeof(TBase); - - return AppDomain.CurrentDomain.GetAssemblies() - .SelectMany((assembley) => assembley.GetTypes()) - .Where((type) => baseType.IsAssignableFrom(type) && type != baseType); - } + public static IEnumerable GetTypesInheritingFrom() => GetTypesInheritingFrom(typeof(TBase)); + + public static IEnumerable GetTypesInheritingFrom(Type baseType, bool internalProjectsOnly = true) => AppDomain.CurrentDomain.GetAssemblies() + .Where(assembly => IsNotTestProject(assembly, internalProjectsOnly)) + .SelectMany(AssemblyTypes) + .Where(InheritsBaseType(baseType)); + + private static Func InheritsBaseType(Type baseType) => (type) => baseType.IsAssignableFrom(type) && type != baseType; + + private static Type[] AssemblyTypes(Assembly assembly) => assembly.GetTypes(); + + private static bool IsNotTestProject(Assembly assembly, bool internalProjectsOnly) + => assembly.FullName != null && !assembly.FullName.Contains("Test") && (!internalProjectsOnly || assembly.FullName.Contains("Dfe.PlanTech.")); + + public static bool HasParameterlessConstructor(this Type type) => type.GetConstructor(Type.EmptyTypes) != null || type.GetConstructors().Length == 0; + + public static bool IsConcreteClass(this Type type) => !type.IsAbstract && !type.IsInterface; } diff --git a/src/Dfe.PlanTech.Infrastructure.Redis/JsonSerialiser.cs b/src/Dfe.PlanTech.Infrastructure.Redis/JsonSerialiser.cs index 159674a79..1f347f485 100644 --- a/src/Dfe.PlanTech.Infrastructure.Redis/JsonSerialiser.cs +++ b/src/Dfe.PlanTech.Infrastructure.Redis/JsonSerialiser.cs @@ -1,6 +1,7 @@ using System.Text.Json; using System.Text.Json.Serialization; using System.Text.Json.Serialization.Metadata; +using Dfe.PlanTech.Domain.Content.Interfaces; using Dfe.PlanTech.Domain.Content.Models; using StackExchange.Redis; @@ -17,8 +18,8 @@ public static class JsonSerialiser private static readonly JsonSerializerOptions JsonSerialiserOptions = new() { TypeInfoResolver = - new DefaultJsonTypeInfoResolver().WithAddedModifier(ContentComponentJsonExtensions - .AddContentComponentPolymorphicInfo), + new DefaultJsonTypeInfoResolver().WithAddedModifier(ContentComponentJsonExtensions.AddContentComponentPolymorphicInfo) + .WithAddedModifier(ContentComponentJsonExtensions.AddContentComponentPolymorphicInfo), ReferenceHandler = ReferenceHandler.Preserve }; diff --git a/src/Dfe.PlanTech.Infrastructure.Redis/RedisCache.cs b/src/Dfe.PlanTech.Infrastructure.Redis/RedisCache.cs index c07cb7b78..2fadc63fb 100644 --- a/src/Dfe.PlanTech.Infrastructure.Redis/RedisCache.cs +++ b/src/Dfe.PlanTech.Infrastructure.Redis/RedisCache.cs @@ -37,20 +37,28 @@ public RedisCache(IRedisConnectionManager connectionManager, ILogger { _logger.LogInformation("Attempting to get or create cache item with key: {Key}", key); - var db = await _connectionManager.GetDatabaseAsync(databaseId); - var redisResult = await GetAsync(db, key); - - if (redisResult.ExistedInCache == true) + try { - _logger.LogTrace("Cache item with key: {Key} found", key); - return redisResult.CacheValue; + var db = await _connectionManager.GetDatabaseAsync(databaseId); + var redisResult = await GetAsync(db, key); + + if (redisResult.ExistedInCache == true) + { + _logger.LogTrace("Cache item with key: {Key} found", key); + return redisResult.CacheValue; + } + else if (redisResult.Errored) + { + return await action(); + } + + return await CreateAndCacheItemAsync(db, key, action, expiry, onCacheItemCreation); } - else if (redisResult.Errored) + catch (RedisConnectionException redisException) { + _logger.LogError(redisException, "Failed to connect to Redis server: \"{Message}\". Retrieving server using {Action}.", redisException.Message, nameof(Action)); return await action(); } - - return await CreateAndCacheItemAsync(db, key, action, expiry, onCacheItemCreation); } /// diff --git a/tests/Dfe.PlanTech.Infrastructure.Redis.UnitTests/ContentComponentJsonExtensionsTests.cs b/tests/Dfe.PlanTech.Infrastructure.Redis.UnitTests/ContentComponentJsonExtensionsTests.cs index ac99cbe04..a004f652d 100644 --- a/tests/Dfe.PlanTech.Infrastructure.Redis.UnitTests/ContentComponentJsonExtensionsTests.cs +++ b/tests/Dfe.PlanTech.Infrastructure.Redis.UnitTests/ContentComponentJsonExtensionsTests.cs @@ -1,24 +1,28 @@ using System.Text.Json; using System.Text.Json.Serialization; using System.Text.Json.Serialization.Metadata; +using Dfe.PlanTech.Domain.Content.Interfaces; using Dfe.PlanTech.Domain.Content.Models; namespace Dfe.PlanTech.Infrastructure.Redis.UnitTests; public class ContentComponentJsonExtensionsTests { - [Fact] - public void AddContentComponentPolymorphicInfo_ShouldSetPolymorphismOptions_WhenTypeIsContentComponent() + [Theory] + [InlineData(typeof(ContentComponent))] + [InlineData(typeof(IContentComponent))] + public void AddContentComponentPolymorphicInfo_ShouldSetPolymorphismOptions(Type type) { var options = new JsonSerializerOptions() { - TypeInfoResolver = new DefaultJsonTypeInfoResolver().WithAddedModifier(ContentComponentJsonExtensions.AddContentComponentPolymorphicInfo) + TypeInfoResolver = new DefaultJsonTypeInfoResolver().WithAddedModifier(ContentComponentJsonExtensions.AddContentComponentPolymorphicInfo) + .WithAddedModifier(ContentComponentJsonExtensions.AddContentComponentPolymorphicInfo) }; - var typeInfo = options.GetTypeInfo(typeof(ContentComponent)); + var typeInfo = options.GetTypeInfo(type); Assert.NotNull(typeInfo); Assert.NotNull(typeInfo.PolymorphismOptions); - Assert.Equal("$contentcomponenttype", typeInfo.PolymorphismOptions.TypeDiscriminatorPropertyName); + Assert.Equal("$" + type.Name.ToLower(), typeInfo.PolymorphismOptions.TypeDiscriminatorPropertyName); Assert.True(typeInfo.PolymorphismOptions.IgnoreUnrecognizedTypeDiscriminators); Assert.Equal(JsonUnknownDerivedTypeHandling.FailSerialization, typeInfo.PolymorphismOptions.UnknownDerivedTypeHandling); } diff --git a/tests/Dfe.PlanTech.Infrastructure.Redis.UnitTests/RedisConnectionManagerTests.cs b/tests/Dfe.PlanTech.Infrastructure.Redis.UnitTests/RedisConnectionManagerTests.cs deleted file mode 100644 index e69de29bb..000000000