Skip to content

Commit b092f2d

Browse files
author
Bart Koelman
committed
Resource inheritance: sorting on derived fields
1 parent 0bfba08 commit b092f2d

File tree

20 files changed

+1285
-118
lines changed

20 files changed

+1285
-118
lines changed

Diff for: src/JsonApiDotNetCore.Annotations/Configuration/ResourceType.cs

+55
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,61 @@ public IReadOnlySet<ResourceType> GetAllConcreteDerivedTypes()
206206
return _lazyAllConcreteDerivedTypes.Value;
207207
}
208208

209+
internal IReadOnlySet<AttrAttribute> GetAttributesInTypeOrDerived(string publicName)
210+
{
211+
return GetAttributesInTypeOrDerived(this, publicName);
212+
}
213+
214+
private static IReadOnlySet<AttrAttribute> GetAttributesInTypeOrDerived(ResourceType resourceType, string publicName)
215+
{
216+
AttrAttribute? attribute = resourceType.FindAttributeByPublicName(publicName);
217+
218+
if (attribute != null)
219+
{
220+
return attribute.AsHashSet();
221+
}
222+
223+
// Hiding base members using the 'new' keyword instead of 'override' (effectively breaking inheritance) is currently not supported.
224+
// https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/knowing-when-to-use-override-and-new-keywords
225+
HashSet<AttrAttribute> attributesInDerivedTypes = new();
226+
227+
foreach (AttrAttribute attributeInDerivedType in resourceType.DirectlyDerivedTypes
228+
.Select(derivedType => GetAttributesInTypeOrDerived(derivedType, publicName)).SelectMany(attributesInDerivedType => attributesInDerivedType))
229+
{
230+
attributesInDerivedTypes.Add(attributeInDerivedType);
231+
}
232+
233+
return attributesInDerivedTypes;
234+
}
235+
236+
internal IReadOnlySet<RelationshipAttribute> GetRelationshipsInTypeOrDerived(string publicName)
237+
{
238+
return GetRelationshipsInTypeOrDerived(this, publicName);
239+
}
240+
241+
private static IReadOnlySet<RelationshipAttribute> GetRelationshipsInTypeOrDerived(ResourceType resourceType, string publicName)
242+
{
243+
RelationshipAttribute? relationship = resourceType.FindRelationshipByPublicName(publicName);
244+
245+
if (relationship != null)
246+
{
247+
return relationship.AsHashSet();
248+
}
249+
250+
// Hiding base members using the 'new' keyword instead of 'override' (effectively breaking inheritance) is currently not supported.
251+
// https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/knowing-when-to-use-override-and-new-keywords
252+
HashSet<RelationshipAttribute> relationshipsInDerivedTypes = new();
253+
254+
foreach (RelationshipAttribute relationshipInDerivedType in resourceType.DirectlyDerivedTypes
255+
.Select(derivedType => GetRelationshipsInTypeOrDerived(derivedType, publicName))
256+
.SelectMany(relationshipsInDerivedType => relationshipsInDerivedType))
257+
{
258+
relationshipsInDerivedTypes.Add(relationshipInDerivedType);
259+
}
260+
261+
return relationshipsInDerivedTypes;
262+
}
263+
209264
public override string ToString()
210265
{
211266
return PublicName;

Diff for: src/JsonApiDotNetCore/Configuration/IResourceGraph.cs

+3-3
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ ResourceType GetResourceType<TResource>()
5454
/// (TResource resource) => new { resource.Attribute1, resource.Relationship2 }
5555
/// ]]>
5656
/// </param>
57-
IReadOnlyCollection<ResourceFieldAttribute> GetFields<TResource>(Expression<Func<TResource, dynamic?>> selector)
57+
IReadOnlyCollection<ResourceFieldAttribute> GetFields<TResource>(Expression<Func<TResource, object?>> selector)
5858
where TResource : class, IIdentifiable;
5959

6060
/// <summary>
@@ -68,7 +68,7 @@ IReadOnlyCollection<ResourceFieldAttribute> GetFields<TResource>(Expression<Func
6868
/// (TResource resource) => new { resource.attribute1, resource.Attribute2 }
6969
/// ]]>
7070
/// </param>
71-
IReadOnlyCollection<AttrAttribute> GetAttributes<TResource>(Expression<Func<TResource, dynamic?>> selector)
71+
IReadOnlyCollection<AttrAttribute> GetAttributes<TResource>(Expression<Func<TResource, object?>> selector)
7272
where TResource : class, IIdentifiable;
7373

7474
/// <summary>
@@ -82,6 +82,6 @@ IReadOnlyCollection<AttrAttribute> GetAttributes<TResource>(Expression<Func<TRes
8282
/// (TResource resource) => new { resource.Relationship1, resource.Relationship2 }
8383
/// ]]>
8484
/// </param>
85-
IReadOnlyCollection<RelationshipAttribute> GetRelationships<TResource>(Expression<Func<TResource, dynamic?>> selector)
85+
IReadOnlyCollection<RelationshipAttribute> GetRelationships<TResource>(Expression<Func<TResource, object?>> selector)
8686
where TResource : class, IIdentifiable;
8787
}

Diff for: src/JsonApiDotNetCore/Configuration/ResourceGraph.cs

+5-5
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ public ResourceType GetResourceType<TResource>()
9191
}
9292

9393
/// <inheritdoc />
94-
public IReadOnlyCollection<ResourceFieldAttribute> GetFields<TResource>(Expression<Func<TResource, dynamic?>> selector)
94+
public IReadOnlyCollection<ResourceFieldAttribute> GetFields<TResource>(Expression<Func<TResource, object?>> selector)
9595
where TResource : class, IIdentifiable
9696
{
9797
ArgumentGuard.NotNull(selector, nameof(selector));
@@ -100,7 +100,7 @@ public IReadOnlyCollection<ResourceFieldAttribute> GetFields<TResource>(Expressi
100100
}
101101

102102
/// <inheritdoc />
103-
public IReadOnlyCollection<AttrAttribute> GetAttributes<TResource>(Expression<Func<TResource, dynamic?>> selector)
103+
public IReadOnlyCollection<AttrAttribute> GetAttributes<TResource>(Expression<Func<TResource, object?>> selector)
104104
where TResource : class, IIdentifiable
105105
{
106106
ArgumentGuard.NotNull(selector, nameof(selector));
@@ -109,15 +109,15 @@ public IReadOnlyCollection<AttrAttribute> GetAttributes<TResource>(Expression<Fu
109109
}
110110

111111
/// <inheritdoc />
112-
public IReadOnlyCollection<RelationshipAttribute> GetRelationships<TResource>(Expression<Func<TResource, dynamic?>> selector)
112+
public IReadOnlyCollection<RelationshipAttribute> GetRelationships<TResource>(Expression<Func<TResource, object?>> selector)
113113
where TResource : class, IIdentifiable
114114
{
115115
ArgumentGuard.NotNull(selector, nameof(selector));
116116

117117
return FilterFields<TResource, RelationshipAttribute>(selector);
118118
}
119119

120-
private IReadOnlyCollection<TField> FilterFields<TResource, TField>(Expression<Func<TResource, dynamic?>> selector)
120+
private IReadOnlyCollection<TField> FilterFields<TResource, TField>(Expression<Func<TResource, object?>> selector)
121121
where TResource : class, IIdentifiable
122122
where TField : ResourceFieldAttribute
123123
{
@@ -157,7 +157,7 @@ private IReadOnlyCollection<TKind> GetFieldsOfType<TResource, TKind>()
157157
return (IReadOnlyCollection<TKind>)resourceType.Fields;
158158
}
159159

160-
private IEnumerable<string> ToMemberNames<TResource>(Expression<Func<TResource, dynamic?>> selector)
160+
private IEnumerable<string> ToMemberNames<TResource>(Expression<Func<TResource, object?>> selector)
161161
{
162162
Expression selectorBody = RemoveConvert(selector.Body);
163163

Diff for: src/JsonApiDotNetCore/Queries/Expressions/SparseFieldSetExpressionExtensions.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ namespace JsonApiDotNetCore.Queries.Expressions;
1111
public static class SparseFieldSetExpressionExtensions
1212
{
1313
public static SparseFieldSetExpression? Including<TResource>(this SparseFieldSetExpression? sparseFieldSet,
14-
Expression<Func<TResource, dynamic?>> fieldSelector, IResourceGraph resourceGraph)
14+
Expression<Func<TResource, object?>> fieldSelector, IResourceGraph resourceGraph)
1515
where TResource : class, IIdentifiable
1616
{
1717
ArgumentGuard.NotNull(fieldSelector, nameof(fieldSelector));
@@ -39,7 +39,7 @@ public static class SparseFieldSetExpressionExtensions
3939
}
4040

4141
public static SparseFieldSetExpression? Excluding<TResource>(this SparseFieldSetExpression? sparseFieldSet,
42-
Expression<Func<TResource, dynamic?>> fieldSelector, IResourceGraph resourceGraph)
42+
Expression<Func<TResource, object?>> fieldSelector, IResourceGraph resourceGraph)
4343
where TResource : class, IIdentifiable
4444
{
4545
ArgumentGuard.NotNull(fieldSelector, nameof(fieldSelector));
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
namespace JsonApiDotNetCore.Queries.Internal.Parsing;
2+
3+
/// <summary>
4+
/// Indicates how to handle derived types when resolving resource field chains.
5+
/// </summary>
6+
internal enum FieldChainInheritanceRequirement
7+
{
8+
/// <summary>
9+
/// Do not consider derived types when resolving attributes or relationships.
10+
/// </summary>
11+
Disabled,
12+
13+
/// <summary>
14+
/// Consider derived types when resolving attributes or relationships, but fail when multiple matches are found.
15+
/// </summary>
16+
RequireSingleMatch
17+
}

Diff for: src/JsonApiDotNetCore/Queries/Internal/Parsing/FilterParser.cs

+4-2
Original file line numberDiff line numberDiff line change
@@ -425,12 +425,14 @@ protected override IImmutableList<ResourceFieldAttribute> OnResolveFieldChain(st
425425
{
426426
if (chainRequirements == FieldChainRequirements.EndsInToMany)
427427
{
428-
return ChainResolver.ResolveToOneChainEndingInToMany(_resourceTypeInScope!, path, _validateSingleFieldCallback);
428+
return ChainResolver.ResolveToOneChainEndingInToMany(_resourceTypeInScope!, path, FieldChainInheritanceRequirement.Disabled,
429+
_validateSingleFieldCallback);
429430
}
430431

431432
if (chainRequirements == FieldChainRequirements.EndsInAttribute)
432433
{
433-
return ChainResolver.ResolveToOneChainEndingInAttribute(_resourceTypeInScope!, path, _validateSingleFieldCallback);
434+
return ChainResolver.ResolveToOneChainEndingInAttribute(_resourceTypeInScope!, path, FieldChainInheritanceRequirement.Disabled,
435+
_validateSingleFieldCallback);
434436
}
435437

436438
if (chainRequirements == FieldChainRequirements.EndsInToOne)

Diff for: src/JsonApiDotNetCore/Queries/Internal/Parsing/IncludeParser.cs

+7-44
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ namespace JsonApiDotNetCore.Queries.Internal.Parsing;
1111
[PublicAPI]
1212
public class IncludeParser : QueryExpressionParser
1313
{
14+
private static readonly ResourceFieldChainErrorFormatter ErrorFormatter = new();
15+
1416
public IncludeExpression Parse(string source, ResourceType resourceTypeInScope, int? maximumDepth)
1517
{
1618
ArgumentGuard.NotNull(resourceTypeInScope, nameof(resourceTypeInScope));
@@ -98,7 +100,7 @@ private ICollection<IncludeTreeNode> LookupRelationshipName(string relationshipN
98100
{
99101
// Depending on the left side of the include chain, we may match relationships anywhere in the resource type hierarchy.
100102
// This is compensated for when rendering the response, which substitutes relationships on base types with the derived ones.
101-
ISet<RelationshipAttribute> relationships = GetRelationshipsInTypeOrDerived(parent.Relationship.RightType, relationshipName);
103+
IReadOnlySet<RelationshipAttribute> relationships = parent.Relationship.RightType.GetRelationshipsInTypeOrDerived(relationshipName);
102104

103105
if (relationships.Any())
104106
{
@@ -116,61 +118,22 @@ private ICollection<IncludeTreeNode> LookupRelationshipName(string relationshipN
116118
return children;
117119
}
118120

119-
private ISet<RelationshipAttribute> GetRelationshipsInTypeOrDerived(ResourceType resourceType, string relationshipName)
120-
{
121-
RelationshipAttribute? relationship = resourceType.FindRelationshipByPublicName(relationshipName);
122-
123-
if (relationship != null)
124-
{
125-
return relationship.AsHashSet();
126-
}
127-
128-
// Hiding base members using the 'new' keyword instead of 'override' (effectively breaking inheritance) is currently not supported.
129-
// https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/knowing-when-to-use-override-and-new-keywords
130-
HashSet<RelationshipAttribute> relationshipsInDerivedTypes = new();
131-
132-
foreach (ResourceType derivedType in resourceType.DirectlyDerivedTypes)
133-
{
134-
ISet<RelationshipAttribute> relationshipsInDerivedType = GetRelationshipsInTypeOrDerived(derivedType, relationshipName);
135-
relationshipsInDerivedTypes.AddRange(relationshipsInDerivedType);
136-
}
137-
138-
return relationshipsInDerivedTypes;
139-
}
140-
141121
private static void AssertRelationshipsFound(ISet<RelationshipAttribute> relationshipsFound, string relationshipName, ICollection<IncludeTreeNode> parents)
142122
{
143123
if (relationshipsFound.Any())
144124
{
145125
return;
146126
}
147127

148-
var messageBuilder = new StringBuilder();
149-
messageBuilder.Append($"Relationship '{relationshipName}'");
150-
151128
string[] parentPaths = parents.Select(parent => parent.Path).Distinct().Where(path => path != string.Empty).ToArray();
152-
153-
if (parentPaths.Length > 0)
154-
{
155-
messageBuilder.Append($" in '{parentPaths[0]}.{relationshipName}'");
156-
}
129+
string path = parentPaths.Length > 0 ? $"{parentPaths[0]}.{relationshipName}" : relationshipName;
157130

158131
ResourceType[] parentResourceTypes = parents.Select(parent => parent.Relationship.RightType).Distinct().ToArray();
159132

160-
if (parentResourceTypes.Length == 1)
161-
{
162-
messageBuilder.Append($" does not exist on resource type '{parentResourceTypes[0].PublicName}'");
163-
}
164-
else
165-
{
166-
string typeNames = string.Join(", ", parentResourceTypes.Select(type => $"'{type.PublicName}'"));
167-
messageBuilder.Append($" does not exist on any of the resource types {typeNames}");
168-
}
169-
170-
bool hasDerived = parents.Any(parent => parent.Relationship.RightType.DirectlyDerivedTypes.Count > 0);
171-
messageBuilder.Append(hasDerived ? " or any of its derived types." : ".");
133+
bool hasDerivedTypes = parents.Any(parent => parent.Relationship.RightType.DirectlyDerivedTypes.Count > 0);
172134

173-
throw new QueryParseException(messageBuilder.ToString());
135+
string message = ErrorFormatter.GetForNoneFound(ResourceFieldCategory.Relationship, relationshipName, path, parentResourceTypes, hasDerivedTypes);
136+
throw new QueryParseException(message);
174137
}
175138

176139
private static void AssertAtLeastOneCanBeIncluded(ISet<RelationshipAttribute> relationshipsFound, string relationshipName,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
namespace JsonApiDotNetCore.Queries.Internal.Parsing;
2+
3+
internal enum ResourceFieldCategory
4+
{
5+
Field,
6+
Attribute,
7+
Relationship
8+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
using System.Text;
2+
using JsonApiDotNetCore.Configuration;
3+
4+
namespace JsonApiDotNetCore.Queries.Internal.Parsing;
5+
6+
internal sealed class ResourceFieldChainErrorFormatter
7+
{
8+
public string GetForNotFound(ResourceFieldCategory category, string publicName, string path, ResourceType resourceType,
9+
FieldChainInheritanceRequirement inheritanceRequirement)
10+
{
11+
var builder = new StringBuilder();
12+
WriteSource(category, publicName, builder);
13+
WritePath(path, publicName, builder);
14+
15+
builder.Append($" does not exist on resource type '{resourceType.PublicName}'");
16+
17+
if (inheritanceRequirement != FieldChainInheritanceRequirement.Disabled && resourceType.DirectlyDerivedTypes.Any())
18+
{
19+
builder.Append(" or any of its derived types");
20+
}
21+
22+
builder.Append('.');
23+
24+
return builder.ToString();
25+
}
26+
27+
public string GetForMultipleMatches(ResourceFieldCategory category, string publicName, string path)
28+
{
29+
var builder = new StringBuilder();
30+
WriteSource(category, publicName, builder);
31+
WritePath(path, publicName, builder);
32+
33+
builder.Append(" is defined on multiple derived types.");
34+
35+
return builder.ToString();
36+
}
37+
38+
public string GetForWrongFieldType(ResourceFieldCategory category, string publicName, string path, ResourceType resourceType, string expected)
39+
{
40+
var builder = new StringBuilder();
41+
WriteSource(category, publicName, builder);
42+
WritePath(path, publicName, builder);
43+
44+
builder.Append($" must be {expected} on resource type '{resourceType.PublicName}'.");
45+
46+
return builder.ToString();
47+
}
48+
49+
public string GetForNoneFound(ResourceFieldCategory category, string publicName, string path, ICollection<ResourceType> parentResourceTypes,
50+
bool hasDerivedTypes)
51+
{
52+
var builder = new StringBuilder();
53+
WriteSource(category, publicName, builder);
54+
WritePath(path, publicName, builder);
55+
56+
if (parentResourceTypes.Count == 1)
57+
{
58+
builder.Append($" does not exist on resource type '{parentResourceTypes.First().PublicName}'");
59+
}
60+
else
61+
{
62+
string typeNames = string.Join(", ", parentResourceTypes.Select(type => $"'{type.PublicName}'"));
63+
builder.Append($" does not exist on any of the resource types {typeNames}");
64+
}
65+
66+
builder.Append(hasDerivedTypes ? " or any of its derived types." : ".");
67+
68+
return builder.ToString();
69+
}
70+
71+
private static void WriteSource(ResourceFieldCategory category, string publicName, StringBuilder builder)
72+
{
73+
builder.Append($"{category} '{publicName}'");
74+
}
75+
76+
private static void WritePath(string path, string publicName, StringBuilder builder)
77+
{
78+
if (path != publicName)
79+
{
80+
builder.Append($" in '{path}'");
81+
}
82+
}
83+
}

0 commit comments

Comments
 (0)