Skip to content

Commit a0877ab

Browse files
authored
Property detect collection element type for custom collection types that have no or multiple type parameters (#1720)
1 parent 3ad189d commit a0877ab

File tree

3 files changed

+118
-13
lines changed

3 files changed

+118
-13
lines changed

src/JsonApiDotNetCore.Annotations/CollectionConverter.cs

+12-3
Original file line numberDiff line numberDiff line change
@@ -104,17 +104,26 @@ public IReadOnlyCollection<IIdentifiable> ExtractResources(object? value)
104104
/// </summary>
105105
public Type? FindCollectionElementType(Type? type)
106106
{
107-
if (type is { IsGenericType: true, GenericTypeArguments.Length: 1 })
107+
if (type != null)
108108
{
109-
if (type.IsOrImplementsInterface<IEnumerable>())
109+
Type? enumerableClosedType = IsEnumerableClosedType(type) ? type : null;
110+
enumerableClosedType ??= type.GetInterfaces().FirstOrDefault(IsEnumerableClosedType);
111+
112+
if (enumerableClosedType != null)
110113
{
111-
return type.GenericTypeArguments[0];
114+
return enumerableClosedType.GenericTypeArguments[0];
112115
}
113116
}
114117

115118
return null;
116119
}
117120

121+
private static bool IsEnumerableClosedType(Type type)
122+
{
123+
bool isClosedType = type is { IsGenericType: true, ContainsGenericParameters: false };
124+
return isClosedType && type.GetGenericTypeDefinition() == typeof(IEnumerable<>);
125+
}
126+
118127
/// <summary>
119128
/// Indicates whether a <see cref="HashSet{T}" /> instance can be assigned to the specified type, for example:
120129
/// <code><![CDATA[

src/JsonApiDotNetCore/Configuration/ResourceGraphBuilder.cs

+2-10
Original file line numberDiff line numberDiff line change
@@ -396,8 +396,8 @@ private ReadOnlyCollection<EagerLoadAttribute> GetEagerLoads(Type resourceClrTyp
396396
continue;
397397
}
398398

399-
Type innerType = TypeOrElementType(property.PropertyType);
400-
eagerLoad.Children = GetEagerLoads(innerType, recursionDepth + 1);
399+
Type rightType = CollectionConverter.Instance.FindCollectionElementType(property.PropertyType) ?? property.PropertyType;
400+
eagerLoad.Children = GetEagerLoads(rightType, recursionDepth + 1);
401401
eagerLoad.Property = property;
402402

403403
eagerLoads.Add(eagerLoad);
@@ -459,14 +459,6 @@ private static void AssertNoInfiniteRecursion(int recursionDepth)
459459
}
460460
}
461461

462-
private Type TypeOrElementType(Type type)
463-
{
464-
Type[] interfaces = type.GetInterfaces().Where(@interface => @interface.IsGenericType && @interface.GetGenericTypeDefinition() == typeof(IEnumerable<>))
465-
.ToArray();
466-
467-
return interfaces.Length == 1 ? interfaces.Single().GenericTypeArguments[0] : type;
468-
}
469-
470462
private string FormatResourceName(Type resourceClrType)
471463
{
472464
var formatter = new ResourceNameFormatter(_options.SerializerOptions.PropertyNamingPolicy);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
using FluentAssertions;
2+
using JsonApiDotNetCore;
3+
using Xunit;
4+
5+
namespace JsonApiDotNetCoreTests.UnitTests.TypeConversion;
6+
7+
public sealed class CollectionConverterTests
8+
{
9+
[Fact]
10+
public void Finds_element_type_for_generic_list()
11+
{
12+
// Arrange
13+
Type sourceType = typeof(List<string>);
14+
15+
// Act
16+
Type? elementType = CollectionConverter.Instance.FindCollectionElementType(sourceType);
17+
18+
// Assert
19+
elementType.Should().Be<string>();
20+
}
21+
22+
[Fact]
23+
public void Finds_element_type_for_generic_enumerable()
24+
{
25+
// Arrange
26+
Type sourceType = typeof(IEnumerable<string>);
27+
28+
// Act
29+
Type? elementType = CollectionConverter.Instance.FindCollectionElementType(sourceType);
30+
31+
// Assert
32+
elementType.Should().Be<string>();
33+
}
34+
35+
[Fact]
36+
public void Finds_element_type_for_custom_generic_collection_with_multiple_type_parameters()
37+
{
38+
// Arrange
39+
Type sourceType = typeof(CustomCollection<int, string>);
40+
41+
// Act
42+
Type? elementType = CollectionConverter.Instance.FindCollectionElementType(sourceType);
43+
44+
// Assert
45+
elementType.Should().Be<string>();
46+
}
47+
48+
[Fact]
49+
public void Finds_element_type_for_custom_non_generic_collection()
50+
{
51+
// Arrange
52+
Type sourceType = typeof(CustomCollectionOfIntString);
53+
54+
// Act
55+
Type? elementType = CollectionConverter.Instance.FindCollectionElementType(sourceType);
56+
57+
// Assert
58+
elementType.Should().Be<string>();
59+
}
60+
61+
[Fact]
62+
public void Finds_no_element_type_for_non_generic_type()
63+
{
64+
// Arrange
65+
Type sourceType = typeof(int);
66+
67+
// Act
68+
Type? elementType = CollectionConverter.Instance.FindCollectionElementType(sourceType);
69+
70+
// Assert
71+
elementType.Should().BeNull();
72+
}
73+
74+
[Fact]
75+
public void Finds_no_element_type_for_non_collection_generic_type()
76+
{
77+
// Arrange
78+
Type sourceType = typeof(Tuple<int, string>);
79+
80+
// Act
81+
Type? elementType = CollectionConverter.Instance.FindCollectionElementType(sourceType);
82+
83+
// Assert
84+
elementType.Should().BeNull();
85+
}
86+
87+
[Fact]
88+
public void Finds_no_element_type_for_unbound_generic_type()
89+
{
90+
// Arrange
91+
Type sourceType = typeof(List<>);
92+
93+
// Act
94+
Type? elementType = CollectionConverter.Instance.FindCollectionElementType(sourceType);
95+
96+
// Assert
97+
elementType.Should().BeNull();
98+
}
99+
100+
// ReSharper disable once UnusedTypeParameter
101+
private class CustomCollection<TOther, TElement> : List<TElement>;
102+
103+
private sealed class CustomCollectionOfIntString : CustomCollection<int, string>;
104+
}

0 commit comments

Comments
 (0)