From f7949dfd322a3b58e20a07067e2fdecff6a10ea8 Mon Sep 17 00:00:00 2001 From: Bart Koelman <10324372+bkoelman@users.noreply.github.com> Date: Fri, 25 Apr 2025 00:45:14 +0200 Subject: [PATCH] Property detect collection element type for custom collection types that have no or multiple type parameters --- .../CollectionConverter.cs | 15 ++- .../Configuration/ResourceGraphBuilder.cs | 12 +- .../CollectionConverterTests.cs | 104 ++++++++++++++++++ 3 files changed, 118 insertions(+), 13 deletions(-) create mode 100644 test/JsonApiDotNetCoreTests/UnitTests/TypeConversion/CollectionConverterTests.cs diff --git a/src/JsonApiDotNetCore.Annotations/CollectionConverter.cs b/src/JsonApiDotNetCore.Annotations/CollectionConverter.cs index cbf834b630..683e34764b 100644 --- a/src/JsonApiDotNetCore.Annotations/CollectionConverter.cs +++ b/src/JsonApiDotNetCore.Annotations/CollectionConverter.cs @@ -104,17 +104,26 @@ public IReadOnlyCollection ExtractResources(object? value) /// public Type? FindCollectionElementType(Type? type) { - if (type is { IsGenericType: true, GenericTypeArguments.Length: 1 }) + if (type != null) { - if (type.IsOrImplementsInterface()) + Type? enumerableClosedType = IsEnumerableClosedType(type) ? type : null; + enumerableClosedType ??= type.GetInterfaces().FirstOrDefault(IsEnumerableClosedType); + + if (enumerableClosedType != null) { - return type.GenericTypeArguments[0]; + return enumerableClosedType.GenericTypeArguments[0]; } } return null; } + private static bool IsEnumerableClosedType(Type type) + { + bool isClosedType = type is { IsGenericType: true, ContainsGenericParameters: false }; + return isClosedType && type.GetGenericTypeDefinition() == typeof(IEnumerable<>); + } + /// /// Indicates whether a instance can be assigned to the specified type, for example: /// GetEagerLoads(Type resourceClrTyp continue; } - Type innerType = TypeOrElementType(property.PropertyType); - eagerLoad.Children = GetEagerLoads(innerType, recursionDepth + 1); + Type rightType = CollectionConverter.Instance.FindCollectionElementType(property.PropertyType) ?? property.PropertyType; + eagerLoad.Children = GetEagerLoads(rightType, recursionDepth + 1); eagerLoad.Property = property; eagerLoads.Add(eagerLoad); @@ -459,14 +459,6 @@ private static void AssertNoInfiniteRecursion(int recursionDepth) } } - private Type TypeOrElementType(Type type) - { - Type[] interfaces = type.GetInterfaces().Where(@interface => @interface.IsGenericType && @interface.GetGenericTypeDefinition() == typeof(IEnumerable<>)) - .ToArray(); - - return interfaces.Length == 1 ? interfaces.Single().GenericTypeArguments[0] : type; - } - private string FormatResourceName(Type resourceClrType) { var formatter = new ResourceNameFormatter(_options.SerializerOptions.PropertyNamingPolicy); diff --git a/test/JsonApiDotNetCoreTests/UnitTests/TypeConversion/CollectionConverterTests.cs b/test/JsonApiDotNetCoreTests/UnitTests/TypeConversion/CollectionConverterTests.cs new file mode 100644 index 0000000000..1f679912ba --- /dev/null +++ b/test/JsonApiDotNetCoreTests/UnitTests/TypeConversion/CollectionConverterTests.cs @@ -0,0 +1,104 @@ +using FluentAssertions; +using JsonApiDotNetCore; +using Xunit; + +namespace JsonApiDotNetCoreTests.UnitTests.TypeConversion; + +public sealed class CollectionConverterTests +{ + [Fact] + public void Finds_element_type_for_generic_list() + { + // Arrange + Type sourceType = typeof(List); + + // Act + Type? elementType = CollectionConverter.Instance.FindCollectionElementType(sourceType); + + // Assert + elementType.Should().Be(); + } + + [Fact] + public void Finds_element_type_for_generic_enumerable() + { + // Arrange + Type sourceType = typeof(IEnumerable); + + // Act + Type? elementType = CollectionConverter.Instance.FindCollectionElementType(sourceType); + + // Assert + elementType.Should().Be(); + } + + [Fact] + public void Finds_element_type_for_custom_generic_collection_with_multiple_type_parameters() + { + // Arrange + Type sourceType = typeof(CustomCollection); + + // Act + Type? elementType = CollectionConverter.Instance.FindCollectionElementType(sourceType); + + // Assert + elementType.Should().Be(); + } + + [Fact] + public void Finds_element_type_for_custom_non_generic_collection() + { + // Arrange + Type sourceType = typeof(CustomCollectionOfIntString); + + // Act + Type? elementType = CollectionConverter.Instance.FindCollectionElementType(sourceType); + + // Assert + elementType.Should().Be(); + } + + [Fact] + public void Finds_no_element_type_for_non_generic_type() + { + // Arrange + Type sourceType = typeof(int); + + // Act + Type? elementType = CollectionConverter.Instance.FindCollectionElementType(sourceType); + + // Assert + elementType.Should().BeNull(); + } + + [Fact] + public void Finds_no_element_type_for_non_collection_generic_type() + { + // Arrange + Type sourceType = typeof(Tuple); + + // Act + Type? elementType = CollectionConverter.Instance.FindCollectionElementType(sourceType); + + // Assert + elementType.Should().BeNull(); + } + + [Fact] + public void Finds_no_element_type_for_unbound_generic_type() + { + // Arrange + Type sourceType = typeof(List<>); + + // Act + Type? elementType = CollectionConverter.Instance.FindCollectionElementType(sourceType); + + // Assert + elementType.Should().BeNull(); + } + + // ReSharper disable once UnusedTypeParameter + private class CustomCollection : List; + + private sealed class CustomCollectionOfIntString : CustomCollection; +}