Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Handle IContentComponent serialisation + Redis errors better #892

Merged
Merged
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
Expand Up @@ -9,53 +9,37 @@ namespace Dfe.PlanTech.Domain.Content.Models;
/// </summary>
public static class ContentComponentJsonExtensions
{
private const string TypeDiscriminatorName = "$contentcomponenttype";

/// <summary>
/// Gets all classes that inherit from <see cref="ContentComponent"/> and creates <see cref="JsonDerivedType"/> mapping information for deserialisation
/// Gets all types inheriting <see cref="Type"/> that would be valid for serialisation
/// </summary>
private static readonly List<JsonDerivedType> ContentComponentTypes = ReflectionHelpers
.GetTypesInheritingFrom<ContentComponent>()
.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();
/// <param name="type"></param>
/// <returns></returns>
private static List<JsonDerivedType> GetInheritingTypes(Type type) => [.. ReflectionHelpers
.GetTypesInheritingFrom(type)
.Where(derivedType => derivedType != type && derivedType.IsConcreteClass() && derivedType.HasParameterlessConstructor())
.Select(type => new JsonDerivedType(type, type.Name))];

/// <summary>
/// Creates polymorphism support for the <see cref="ContentComponent"/> class.
/// Adds polymorphism support for the <see cref="ContentComponent"/> class.
/// </summary>
/// <returns></returns>
private static JsonPolymorphismOptions CreateJsonPolymorphismOptions()
/// <param name="jsonTypeInfo"></param>
public static void AddContentComponentPolymorphicInfo<TType>(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;
}

/// <summary>
/// Adds polymorphism support for the <see cref="ContentComponent"/> class.
/// </summary>
/// <param name="jsonTypeInfo"></param>
public static void AddContentComponentPolymorphicInfo(JsonTypeInfo jsonTypeInfo)
{
if (jsonTypeInfo.Type != ContentComponentType)
return;

jsonTypeInfo.PolymorphismOptions = ContentComponentPolymorphismOptions;
jsonTypeInfo.PolymorphismOptions = options;
}
}
27 changes: 19 additions & 8 deletions src/Dfe.PlanTech.Domain/Helpers/ReflectionHelpers.cs
Original file line number Diff line number Diff line change
@@ -1,13 +1,24 @@
using System.Reflection;

namespace Dfe.PlanTech.Domain.Helpers;

public static class ReflectionHelpers
{
public static IEnumerable<Type> GetTypesInheritingFrom<TBase>()
{
var baseType = typeof(TBase);

return AppDomain.CurrentDomain.GetAssemblies()
.SelectMany((assembley) => assembley.GetTypes())
.Where((type) => baseType.IsAssignableFrom(type) && type != baseType);
}
public static IEnumerable<Type> GetTypesInheritingFrom<TBase>() => GetTypesInheritingFrom(typeof(TBase));

public static IEnumerable<Type> GetTypesInheritingFrom(Type baseType, bool internalProjectsOnly = true) => AppDomain.CurrentDomain.GetAssemblies()
.Where(assembly => IsNotTestProject(assembly, internalProjectsOnly))
.SelectMany(AssemblyTypes)
.Where(InheritsBaseType(baseType));

private static Func<Type, bool> 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;
}
5 changes: 3 additions & 2 deletions src/Dfe.PlanTech.Infrastructure.Redis/JsonSerialiser.cs
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -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<IContentComponent>)
.WithAddedModifier(ContentComponentJsonExtensions.AddContentComponentPolymorphicInfo<ContentComponent>),
ReferenceHandler = ReferenceHandler.Preserve
};

Expand Down
26 changes: 17 additions & 9 deletions src/Dfe.PlanTech.Infrastructure.Redis/RedisCache.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,20 +37,28 @@ public RedisCache(IRedisConnectionManager connectionManager, ILogger<RedisCache>
{
_logger.LogInformation("Attempting to get or create cache item with key: {Key}", key);

var db = await _connectionManager.GetDatabaseAsync(databaseId);
var redisResult = await GetAsync<T>(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<T>(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);
}

/// <inheritdoc/>
Expand Down
Original file line number Diff line number Diff line change
@@ -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<ContentComponent>)
.WithAddedModifier(ContentComponentJsonExtensions.AddContentComponentPolymorphicInfo<IContentComponent>)
};

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);
}
Expand Down
Loading