Skip to content

Commit 7bf6be8

Browse files
committed
To render selectors for derived types, a selector for each non-abstract derived type must be available in QueryLayer.Selection, which wasn't the case.
Parsing of include chains is changed to include relationships on all concrete derived types. For example, given an inclusion chain of "wheels" at `/vehicles`, the parser now returns `Car.Wheels` and `Truck.Wheels`. This used to be `Vehicle.Wheels`. There's no point in adding abstract types, because they can't be instantiated in selectors. `QueryLayerComposer` was fixed to generate selectors for the left-type of these relationships, instead of for the `QueryLayer` resource type (so `Car.Wheels` and `Truck.Wheels` instead of `Vehicle.Wheels`). `SelectClauseBuilder` was missing a cast to the derived type when descending into relationship selectors, which is fixed now. Being unable to include relationships of sibling types after a POST/PATCH resource request at a base endpoint was because a `QueryLayer` for fetch-after-post was built against the accurized resource type, and then sent to the original (non-accurized) repository. For example, `POST /vehicles?include=TruckRelationship,CarRelationship` only works if the query executes against the non-accurized table (so `Vehicle` instead of `Car`, because `Car` doesn't contain `TruckRelationship`). The fix is to discard the accurized resource type and use the original `TResource`. Other improvements: - During the process of building expression trees, consistency checks have been added to prevent downstream crashes that are difficult to diagnose. - `SelectClauseBuilder` internally passed along a `LambdaScope` that overruled the one present in context, so care had to be taken to use the right one. To eliminate this pitfall, now a new context is forked which contains the appropriate `LambdaScope`, so the overruling parameter is no longer needed. Fixes #1639, fixes #1640.
1 parent a257c08 commit 7bf6be8

File tree

12 files changed

+236
-62
lines changed

12 files changed

+236
-62
lines changed

JsonApiDotNetCore.sln.DotSettings

+1
Original file line numberDiff line numberDiff line change
@@ -668,6 +668,7 @@ $left$ = $right$;</s:String>
668668
<s:String x:Key="/Default/PatternsAndTemplates/StructuralSearch/Pattern/=D29C3A091CD9E74BBFDF1B8974F5A977/SearchPattern/@EntryValue">if ($argument$ is null) throw new ArgumentNullException(nameof($argument$));</s:String>
669669
<s:String x:Key="/Default/PatternsAndTemplates/StructuralSearch/Pattern/=D29C3A091CD9E74BBFDF1B8974F5A977/Severity/@EntryValue">WARNING</s:String>
670670
<s:Boolean x:Key="/Default/UserDictionary/Words/=Accurize/@EntryIndexedValue">True</s:Boolean>
671+
<s:Boolean x:Key="/Default/UserDictionary/Words/=accurized/@EntryIndexedValue">True</s:Boolean>
671672
<s:Boolean x:Key="/Default/UserDictionary/Words/=appsettings/@EntryIndexedValue">True</s:Boolean>
672673
<s:Boolean x:Key="/Default/UserDictionary/Words/=Assignee/@EntryIndexedValue">True</s:Boolean>
673674
<s:Boolean x:Key="/Default/UserDictionary/Words/=Contoso/@EntryIndexedValue">True</s:Boolean>

src/JsonApiDotNetCore/Queries/Parsing/IncludeParser.cs

+27-1
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ private static ReadOnlyCollection<IncludeTreeNode> LookupRelationshipName(string
122122
{
123123
// Depending on the left side of the include chain, we may match relationships anywhere in the resource type hierarchy.
124124
// This is compensated for when rendering the response, which substitutes relationships on base types with the derived ones.
125-
IReadOnlySet<RelationshipAttribute> relationships = parent.Relationship.RightType.GetRelationshipsInTypeOrDerived(relationshipName);
125+
HashSet<RelationshipAttribute> relationships = GetRelationshipsInConcreteTypes(parent.Relationship.RightType, relationshipName);
126126

127127
if (relationships.Count > 0)
128128
{
@@ -140,6 +140,32 @@ private static ReadOnlyCollection<IncludeTreeNode> LookupRelationshipName(string
140140
return children.AsReadOnly();
141141
}
142142

143+
private static HashSet<RelationshipAttribute> GetRelationshipsInConcreteTypes(ResourceType resourceType, string relationshipName)
144+
{
145+
HashSet<RelationshipAttribute> relationshipsToInclude = [];
146+
147+
foreach (RelationshipAttribute relationship in resourceType.GetRelationshipsInTypeOrDerived(relationshipName))
148+
{
149+
if (!relationship.LeftType.ClrType.IsAbstract)
150+
{
151+
relationshipsToInclude.Add(relationship);
152+
}
153+
154+
IncludeRelationshipsFromConcreteDerivedTypes(relationship, relationshipsToInclude);
155+
}
156+
157+
return relationshipsToInclude;
158+
}
159+
160+
private static void IncludeRelationshipsFromConcreteDerivedTypes(RelationshipAttribute relationship, HashSet<RelationshipAttribute> relationshipsToInclude)
161+
{
162+
foreach (ResourceType derivedType in relationship.LeftType.GetAllConcreteDerivedTypes())
163+
{
164+
RelationshipAttribute relationshipInDerived = derivedType.GetRelationshipByPublicName(relationship.PublicName);
165+
relationshipsToInclude.Add(relationshipInDerived);
166+
}
167+
}
168+
143169
private static void AssertRelationshipsFound(HashSet<RelationshipAttribute> relationshipsFound, string relationshipName,
144170
IReadOnlyCollection<IncludeTreeNode> parents, int position)
145171
{

src/JsonApiDotNetCore/Queries/QueryLayerComposer.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,7 @@ private IImmutableSet<IncludeElementExpression> ProcessIncludeSet(IImmutableSet<
212212
foreach (IncludeElementExpression includeElement in includeElementsEvaluated)
213213
{
214214
parentLayer.Selection ??= new FieldSelection();
215-
FieldSelectors selectors = parentLayer.Selection.GetOrCreateSelectors(parentLayer.ResourceType);
215+
FieldSelectors selectors = parentLayer.Selection.GetOrCreateSelectors(includeElement.Relationship.LeftType);
216216

217217
if (!selectors.ContainsField(includeElement.Relationship))
218218
{

src/JsonApiDotNetCore/Queries/QueryableBuilding/QueryClauseBuilderContext.cs

+12
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ public QueryClauseBuilderContext(Expression source, ResourceType resourceType, T
6161
ArgumentGuard.NotNull(lambdaScopeFactory);
6262
ArgumentGuard.NotNull(lambdaScope);
6363
ArgumentGuard.NotNull(queryableBuilder);
64+
AssertSameType(source.Type, resourceType);
6465

6566
Source = source;
6667
ResourceType = resourceType;
@@ -72,6 +73,17 @@ public QueryClauseBuilderContext(Expression source, ResourceType resourceType, T
7273
State = state;
7374
}
7475

76+
private static void AssertSameType(Type sourceType, ResourceType resourceType)
77+
{
78+
Type? sourceElementType = CollectionConverter.Instance.FindCollectionElementType(sourceType);
79+
80+
if (sourceElementType != resourceType.ClrType)
81+
{
82+
throw new InvalidOperationException(
83+
$"Internal error: Mismatch between expression type '{sourceElementType?.Name}' and resource type '{resourceType.ClrType.Name}'.");
84+
}
85+
}
86+
7587
public QueryClauseBuilderContext WithSource(Expression source)
7688
{
7789
ArgumentGuard.NotNull(source);

src/JsonApiDotNetCore/Queries/QueryableBuilding/QueryableBuilder.cs

+10
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ public virtual Expression ApplyQuery(QueryLayer layer, QueryableBuilderContext c
3535
{
3636
ArgumentGuard.NotNull(layer);
3737
ArgumentGuard.NotNull(context);
38+
AssertSameType(layer.ResourceType, context.ElementType);
3839

3940
Expression expression = context.Source;
4041

@@ -66,6 +67,15 @@ public virtual Expression ApplyQuery(QueryLayer layer, QueryableBuilderContext c
6667
return expression;
6768
}
6869

70+
private static void AssertSameType(ResourceType resourceType, Type elementType)
71+
{
72+
if (elementType != resourceType.ClrType)
73+
{
74+
throw new InvalidOperationException(
75+
$"Internal error: Mismatch between query layer type '{resourceType.ClrType.Name}' and query element type '{elementType.Name}'.");
76+
}
77+
}
78+
6979
protected virtual Expression ApplyInclude(Expression source, IncludeExpression include, ResourceType resourceType, QueryableBuilderContext context)
7080
{
7181
ArgumentGuard.NotNull(source);

src/JsonApiDotNetCore/Queries/QueryableBuilding/SelectClauseBuilder.cs

+47-32
Original file line numberDiff line numberDiff line change
@@ -29,36 +29,47 @@ public virtual Expression ApplySelect(FieldSelection selection, QueryClauseBuild
2929
{
3030
ArgumentGuard.NotNull(selection);
3131

32-
Expression bodyInitializer = CreateLambdaBodyInitializer(selection, context.ResourceType, context.LambdaScope, false, context);
32+
Expression bodyInitializer = CreateLambdaBodyInitializer(selection, context.ResourceType, false, context);
3333

3434
LambdaExpression lambda = Expression.Lambda(bodyInitializer, context.LambdaScope.Parameter);
3535

3636
return SelectExtensionMethodCall(context.ExtensionType, context.Source, context.LambdaScope.Parameter.Type, lambda);
3737
}
3838

39-
private Expression CreateLambdaBodyInitializer(FieldSelection selection, ResourceType resourceType, LambdaScope lambdaScope,
40-
bool lambdaAccessorRequiresTestForNull, QueryClauseBuilderContext context)
39+
private Expression CreateLambdaBodyInitializer(FieldSelection selection, ResourceType resourceType, bool lambdaAccessorRequiresTestForNull,
40+
QueryClauseBuilderContext context)
4141
{
42+
AssertSameType(context.LambdaScope.Accessor.Type, resourceType);
43+
4244
IReadOnlyEntityType entityType = context.EntityModel.FindEntityType(resourceType.ClrType)!;
4345
IReadOnlyEntityType[] concreteEntityTypes = entityType.GetConcreteDerivedTypesInclusive().ToArray();
4446

4547
Expression bodyInitializer = concreteEntityTypes.Length > 1
46-
? CreateLambdaBodyInitializerForTypeHierarchy(selection, resourceType, concreteEntityTypes, lambdaScope, context)
47-
: CreateLambdaBodyInitializerForSingleType(selection, resourceType, lambdaScope, context);
48+
? CreateLambdaBodyInitializerForTypeHierarchy(selection, resourceType, concreteEntityTypes, context)
49+
: CreateLambdaBodyInitializerForSingleType(selection, resourceType, context);
4850

4951
if (!lambdaAccessorRequiresTestForNull)
5052
{
5153
return bodyInitializer;
5254
}
5355

54-
return TestForNull(lambdaScope.Accessor, bodyInitializer);
56+
return TestForNull(context.LambdaScope.Accessor, bodyInitializer);
57+
}
58+
59+
private static void AssertSameType(Type lambdaAccessorType, ResourceType resourceType)
60+
{
61+
if (lambdaAccessorType != resourceType.ClrType)
62+
{
63+
throw new InvalidOperationException(
64+
$"Internal error: Mismatch between lambda accessor type '{lambdaAccessorType.Name}' and resource type '{resourceType.ClrType.Name}'.");
65+
}
5566
}
5667

5768
private Expression CreateLambdaBodyInitializerForTypeHierarchy(FieldSelection selection, ResourceType baseResourceType,
58-
IEnumerable<IReadOnlyEntityType> concreteEntityTypes, LambdaScope lambdaScope, QueryClauseBuilderContext context)
69+
IEnumerable<IReadOnlyEntityType> concreteEntityTypes, QueryClauseBuilderContext context)
5970
{
6071
IReadOnlySet<ResourceType> resourceTypes = selection.GetResourceTypes();
61-
Expression rootCondition = lambdaScope.Accessor;
72+
Expression rootCondition = context.LambdaScope.Accessor;
6273

6374
foreach (IReadOnlyEntityType entityType in concreteEntityTypes)
6475
{
@@ -73,14 +84,14 @@ private Expression CreateLambdaBodyInitializerForTypeHierarchy(FieldSelection se
7384
Dictionary<PropertyInfo, PropertySelector>.ValueCollection propertySelectors =
7485
ToPropertySelectors(fieldSelectors, resourceType, entityType.ClrType, context.EntityModel);
7586

76-
MemberBinding[] propertyAssignments = propertySelectors.Select(selector => CreatePropertyAssignment(selector, lambdaScope, context))
87+
MemberBinding[] propertyAssignments = propertySelectors.Select(selector => CreatePropertyAssignment(selector, context))
7788
.Cast<MemberBinding>().ToArray();
7889

7990
NewExpression createInstance = _resourceFactory.CreateNewExpression(entityType.ClrType);
8091
MemberInitExpression memberInit = Expression.MemberInit(createInstance, propertyAssignments);
8192
UnaryExpression castToBaseType = Expression.Convert(memberInit, baseResourceType.ClrType);
8293

83-
BinaryExpression typeCheck = CreateRuntimeTypeCheck(lambdaScope, entityType.ClrType);
94+
BinaryExpression typeCheck = CreateRuntimeTypeCheck(context.LambdaScope, entityType.ClrType);
8495
rootCondition = Expression.Condition(typeCheck, castToBaseType, rootCondition);
8596
}
8697
}
@@ -100,18 +111,16 @@ private static BinaryExpression CreateRuntimeTypeCheck(LambdaScope lambdaScope,
100111
return Expression.MakeBinary(ExpressionType.Equal, getTypeCall, concreteTypeConstant, false, TypeOpEqualityMethod);
101112
}
102113

103-
private MemberInitExpression CreateLambdaBodyInitializerForSingleType(FieldSelection selection, ResourceType resourceType, LambdaScope lambdaScope,
114+
private MemberInitExpression CreateLambdaBodyInitializerForSingleType(FieldSelection selection, ResourceType resourceType,
104115
QueryClauseBuilderContext context)
105116
{
106117
FieldSelectors fieldSelectors = selection.GetOrCreateSelectors(resourceType);
107118

108119
Dictionary<PropertyInfo, PropertySelector>.ValueCollection propertySelectors =
109-
ToPropertySelectors(fieldSelectors, resourceType, lambdaScope.Accessor.Type, context.EntityModel);
110-
111-
MemberBinding[] propertyAssignments = propertySelectors.Select(selector => CreatePropertyAssignment(selector, lambdaScope, context))
112-
.Cast<MemberBinding>().ToArray();
120+
ToPropertySelectors(fieldSelectors, resourceType, context.LambdaScope.Accessor.Type, context.EntityModel);
113121

114-
NewExpression createInstance = _resourceFactory.CreateNewExpression(lambdaScope.Accessor.Type);
122+
MemberBinding[] propertyAssignments = propertySelectors.Select(selector => CreatePropertyAssignment(selector, context)).Cast<MemberBinding>().ToArray();
123+
NewExpression createInstance = _resourceFactory.CreateNewExpression(context.LambdaScope.Accessor.Type);
115124
return Expression.MemberInit(createInstance, propertyAssignments);
116125
}
117126

@@ -182,50 +191,56 @@ private static void IncludeEagerLoads(ResourceType resourceType, Dictionary<Prop
182191
}
183192
}
184193

185-
private MemberAssignment CreatePropertyAssignment(PropertySelector propertySelector, LambdaScope lambdaScope, QueryClauseBuilderContext context)
194+
private MemberAssignment CreatePropertyAssignment(PropertySelector propertySelector, QueryClauseBuilderContext context)
186195
{
187-
bool requiresUpCast = lambdaScope.Accessor.Type != propertySelector.Property.DeclaringType &&
188-
lambdaScope.Accessor.Type.IsAssignableFrom(propertySelector.Property.DeclaringType);
196+
bool requiresUpCast = context.LambdaScope.Accessor.Type != propertySelector.Property.DeclaringType &&
197+
context.LambdaScope.Accessor.Type.IsAssignableFrom(propertySelector.Property.DeclaringType);
198+
199+
UnaryExpression? derivedAccessor = requiresUpCast ? Expression.Convert(context.LambdaScope.Accessor, propertySelector.Property.DeclaringType!) : null;
189200

190-
MemberExpression propertyAccess = requiresUpCast
191-
? Expression.MakeMemberAccess(Expression.Convert(lambdaScope.Accessor, propertySelector.Property.DeclaringType!), propertySelector.Property)
192-
: Expression.Property(lambdaScope.Accessor, propertySelector.Property);
201+
MemberExpression propertyAccess = derivedAccessor != null
202+
? Expression.MakeMemberAccess(derivedAccessor, propertySelector.Property)
203+
: Expression.Property(context.LambdaScope.Accessor, propertySelector.Property);
193204

194205
Expression assignmentRightHandSide = propertyAccess;
195206

196207
if (propertySelector.NextLayer != null)
197208
{
198-
assignmentRightHandSide =
199-
CreateAssignmentRightHandSideForLayer(propertySelector.NextLayer, lambdaScope, propertyAccess, propertySelector.Property, context);
209+
QueryClauseBuilderContext rightHandSideContext =
210+
derivedAccessor != null ? context.WithLambdaScope(context.LambdaScope.WithAccessor(derivedAccessor)) : context;
211+
212+
assignmentRightHandSide = CreateAssignmentRightHandSideForLayer(propertySelector.NextLayer, propertyAccess,
213+
propertySelector.Property, rightHandSideContext);
200214
}
201215

202216
return Expression.Bind(propertySelector.Property, assignmentRightHandSide);
203217
}
204218

205-
private Expression CreateAssignmentRightHandSideForLayer(QueryLayer layer, LambdaScope outerLambdaScope, MemberExpression propertyAccess,
206-
PropertyInfo selectorPropertyInfo, QueryClauseBuilderContext context)
219+
private Expression CreateAssignmentRightHandSideForLayer(QueryLayer layer, MemberExpression propertyAccess, PropertyInfo selectorPropertyInfo,
220+
QueryClauseBuilderContext context)
207221
{
208222
Type? collectionElementType = CollectionConverter.Instance.FindCollectionElementType(selectorPropertyInfo.PropertyType);
209223
Type bodyElementType = collectionElementType ?? selectorPropertyInfo.PropertyType;
210224

211225
if (collectionElementType != null)
212226
{
213-
return CreateCollectionInitializer(outerLambdaScope, selectorPropertyInfo, bodyElementType, layer, context);
227+
return CreateCollectionInitializer(selectorPropertyInfo, bodyElementType, layer, context);
214228
}
215229

216230
if (layer.Selection == null || layer.Selection.IsEmpty)
217231
{
218232
return propertyAccess;
219233
}
220234

221-
using LambdaScope scope = context.LambdaScopeFactory.CreateScope(bodyElementType, propertyAccess);
222-
return CreateLambdaBodyInitializer(layer.Selection, layer.ResourceType, scope, true, context);
235+
using LambdaScope initializerScope = context.LambdaScopeFactory.CreateScope(bodyElementType, propertyAccess);
236+
QueryClauseBuilderContext initializerContext = context.WithLambdaScope(initializerScope);
237+
return CreateLambdaBodyInitializer(layer.Selection, layer.ResourceType, true, initializerContext);
223238
}
224239

225-
private static MethodCallExpression CreateCollectionInitializer(LambdaScope lambdaScope, PropertyInfo collectionProperty, Type elementType,
226-
QueryLayer layer, QueryClauseBuilderContext context)
240+
private static MethodCallExpression CreateCollectionInitializer(PropertyInfo collectionProperty, Type elementType, QueryLayer layer,
241+
QueryClauseBuilderContext context)
227242
{
228-
MemberExpression propertyExpression = Expression.Property(lambdaScope.Accessor, collectionProperty);
243+
MemberExpression propertyExpression = Expression.Property(context.LambdaScope.Accessor, collectionProperty);
229244

230245
var nestedContext = new QueryableBuilderContext(propertyExpression, elementType, typeof(Enumerable), context.EntityModel, context.LambdaScopeFactory,
231246
context.State);

src/JsonApiDotNetCore/Repositories/IResourceRepositoryAccessor.cs

+5
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@ namespace JsonApiDotNetCore.Repositories;
1111
/// </summary>
1212
public interface IResourceRepositoryAccessor
1313
{
14+
/// <summary>
15+
/// Uses the <see cref="IResourceGraph" /> to lookup the corresponding <see cref="ResourceType" /> for the specified CLR type.
16+
/// </summary>
17+
ResourceType LookupResourceType(Type resourceClrType);
18+
1419
/// <summary>
1520
/// Invokes <see cref="IResourceReadRepository{TResource,TId}.GetAsync" /> for the specified resource type.
1621
/// </summary>

src/JsonApiDotNetCore/Repositories/ResourceRepositoryAccessor.cs

+8
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,14 @@ public ResourceRepositoryAccessor(IServiceProvider serviceProvider, IResourceGra
2929
_request = request;
3030
}
3131

32+
/// <inheritdoc />
33+
public ResourceType LookupResourceType(Type resourceClrType)
34+
{
35+
ArgumentGuard.NotNull(resourceClrType);
36+
37+
return _resourceGraph.GetResourceType(resourceClrType);
38+
}
39+
3240
/// <inheritdoc />
3341
public async Task<IReadOnlyCollection<TResource>> GetAsync<TResource>(QueryLayer queryLayer, CancellationToken cancellationToken)
3442
where TResource : class, IIdentifiable

0 commit comments

Comments
 (0)