@@ -193,7 +193,47 @@ protected void AssertTokenStackIsEmpty()
193
193
}
194
194
}
195
195
196
- private protected void ParseRelationshipChain ( IncludeTreeNode treeRoot )
196
+ /// <summary>
197
+ /// Parses a comma-separated sequence of relationship chains, taking relationships on derived types into account.
198
+ /// </summary>
199
+ protected IncludeExpression ParseCommaSeparatedSequenceOfRelationshipChains ( ResourceType resourceType )
200
+ {
201
+ ArgumentNullException . ThrowIfNull ( resourceType ) ;
202
+
203
+ var treeRoot = IncludeTreeNode . CreateRoot ( resourceType ) ;
204
+ bool isAtStart = true ;
205
+
206
+ while ( TokenStack . Count > 0 )
207
+ {
208
+ if ( ! isAtStart )
209
+ {
210
+ EatSingleCharacterToken ( TokenKind . Comma ) ;
211
+ }
212
+ else
213
+ {
214
+ isAtStart = false ;
215
+ }
216
+
217
+ ParseRelationshipChain ( treeRoot , false , null ) ;
218
+ }
219
+
220
+ return treeRoot . ToExpression ( ) ;
221
+ }
222
+
223
+ /// <summary>
224
+ /// Parses a relationship chain that ends in a to-many relationship, taking relationships on derived types into account.
225
+ /// </summary>
226
+ protected IncludeExpression ParseRelationshipChainEndingInToMany ( ResourceType resourceType , string ? alternativeErrorMessage )
227
+ {
228
+ ArgumentNullException . ThrowIfNull ( resourceType ) ;
229
+
230
+ var treeRoot = IncludeTreeNode . CreateRoot ( resourceType ) ;
231
+ ParseRelationshipChain ( treeRoot , true , alternativeErrorMessage ) ;
232
+
233
+ return treeRoot . ToExpression ( ) ;
234
+ }
235
+
236
+ private void ParseRelationshipChain ( IncludeTreeNode treeRoot , bool requireChainEndsInToMany , string ? alternativeErrorMessage )
197
237
{
198
238
// A relationship name usually matches a single relationship, even when overridden in derived types.
199
239
// But in the following case, two relationships are matched on GET /shoppingBaskets?include=items:
@@ -221,29 +261,35 @@ private protected void ParseRelationshipChain(IncludeTreeNode treeRoot)
221
261
// that there's currently no way to include Products without Articles. We could add such optional upcast syntax
222
262
// in the future, if desired.
223
263
224
- ReadOnlyCollection < IncludeTreeNode > children = ParseRelationshipName ( [ treeRoot ] ) ;
264
+ ReadOnlyCollection < IncludeTreeNode > children = ParseRelationshipName ( [ treeRoot ] , requireChainEndsInToMany , alternativeErrorMessage ) ;
225
265
226
266
while ( TokenStack . TryPeek ( out Token ? nextToken ) && nextToken . Kind == TokenKind . Period )
227
267
{
228
268
EatSingleCharacterToken ( TokenKind . Period ) ;
229
269
230
- children = ParseRelationshipName ( children ) ;
270
+ children = ParseRelationshipName ( children , requireChainEndsInToMany , null ) ;
231
271
}
232
272
}
233
273
234
- private ReadOnlyCollection < IncludeTreeNode > ParseRelationshipName ( IReadOnlyCollection < IncludeTreeNode > parents )
274
+ private ReadOnlyCollection < IncludeTreeNode > ParseRelationshipName ( IReadOnlyCollection < IncludeTreeNode > parents , bool requireChainEndsInToMany ,
275
+ string ? alternativeErrorMessage )
235
276
{
236
277
int position = GetNextTokenPositionOrEnd ( ) ;
237
278
238
279
if ( TokenStack . TryPop ( out Token ? token ) && token . Kind == TokenKind . Text )
239
280
{
240
- return LookupRelationshipName ( token . Value ! , parents , position ) ;
281
+ bool isAtEndOfChain = ! TokenStack . TryPeek ( out Token ? nextToken ) || nextToken . Kind != TokenKind . Period ;
282
+ bool requireToMany = requireChainEndsInToMany && isAtEndOfChain ;
283
+
284
+ return LookupRelationshipName ( token . Value ! , parents , requireToMany , position ) ;
241
285
}
242
286
243
- throw new QueryParseException ( "Relationship name expected." , position ) ;
287
+ string message = alternativeErrorMessage ?? ( requireChainEndsInToMany ? "To-many relationship name expected." : "Relationship name expected." ) ;
288
+ throw new QueryParseException ( message , position ) ;
244
289
}
245
290
246
- private ReadOnlyCollection < IncludeTreeNode > LookupRelationshipName ( string relationshipName , IReadOnlyCollection < IncludeTreeNode > parents , int position )
291
+ private ReadOnlyCollection < IncludeTreeNode > LookupRelationshipName ( string relationshipName , IReadOnlyCollection < IncludeTreeNode > parents ,
292
+ bool requireToMany , int position )
247
293
{
248
294
List < IncludeTreeNode > children = [ ] ;
249
295
HashSet < RelationshipAttribute > relationshipsFound = [ ] ;
@@ -252,51 +298,58 @@ private ReadOnlyCollection<IncludeTreeNode> LookupRelationshipName(string relati
252
298
{
253
299
// Depending on the left side of the include chain, we may match relationships anywhere in the resource type hierarchy.
254
300
// This is compensated for when rendering the response, which substitutes relationships on base types with the derived ones.
255
- HashSet < RelationshipAttribute > relationships = GetRelationshipsInConcreteTypes ( parent . Relationship . RightType , relationshipName ) ;
301
+ HashSet < RelationshipAttribute > relationships = GetRelationshipsInConcreteTypes ( parent . Relationship . RightType , relationshipName , requireToMany ) ;
256
302
257
303
if ( relationships . Count > 0 )
258
304
{
259
305
relationshipsFound . UnionWith ( relationships ) ;
260
306
261
307
RelationshipAttribute [ ] relationshipsToInclude = relationships . Where ( relationship => ! relationship . IsIncludeBlocked ( ) ) . ToArray ( ) ;
262
- IReadOnlyCollection < IncludeTreeNode > affectedChildren = parent . EnsureChildren ( relationshipsToInclude ) ;
308
+ ReadOnlyCollection < IncludeTreeNode > affectedChildren = parent . EnsureChildren ( relationshipsToInclude ) ;
263
309
children . AddRange ( affectedChildren ) ;
264
310
}
265
311
}
266
312
267
- AssertRelationshipsFound ( relationshipsFound , relationshipName , parents , position ) ;
313
+ AssertRelationshipsFound ( relationshipsFound , relationshipName , requireToMany , parents , position ) ;
268
314
AssertAtLeastOneCanBeIncluded ( relationshipsFound , relationshipName , position ) ;
269
315
270
316
return children . AsReadOnly ( ) ;
271
317
}
272
318
273
- private static HashSet < RelationshipAttribute > GetRelationshipsInConcreteTypes ( ResourceType resourceType , string relationshipName )
319
+ private static HashSet < RelationshipAttribute > GetRelationshipsInConcreteTypes ( ResourceType resourceType , string relationshipName , bool requireToMany )
274
320
{
275
321
HashSet < RelationshipAttribute > relationshipsToInclude = [ ] ;
276
322
277
323
foreach ( RelationshipAttribute relationship in resourceType . GetRelationshipsInTypeOrDerived ( relationshipName ) )
278
324
{
279
- if ( ! relationship . LeftType . ClrType . IsAbstract )
325
+ if ( ! requireToMany || relationship is HasManyAttribute )
280
326
{
281
- relationshipsToInclude . Add ( relationship ) ;
327
+ if ( ! relationship . LeftType . ClrType . IsAbstract )
328
+ {
329
+ relationshipsToInclude . Add ( relationship ) ;
330
+ }
282
331
}
283
332
284
- IncludeRelationshipsFromConcreteDerivedTypes ( relationship , relationshipsToInclude ) ;
333
+ IncludeRelationshipsFromConcreteDerivedTypes ( relationship , requireToMany , relationshipsToInclude ) ;
285
334
}
286
335
287
336
return relationshipsToInclude ;
288
337
}
289
338
290
- private static void IncludeRelationshipsFromConcreteDerivedTypes ( RelationshipAttribute relationship , HashSet < RelationshipAttribute > relationshipsToInclude )
339
+ private static void IncludeRelationshipsFromConcreteDerivedTypes ( RelationshipAttribute relationship , bool requireToMany ,
340
+ HashSet < RelationshipAttribute > relationshipsToInclude )
291
341
{
292
342
foreach ( ResourceType derivedType in relationship . LeftType . GetAllConcreteDerivedTypes ( ) )
293
343
{
294
- RelationshipAttribute relationshipInDerived = derivedType . GetRelationshipByPublicName ( relationship . PublicName ) ;
295
- relationshipsToInclude . Add ( relationshipInDerived ) ;
344
+ if ( ! requireToMany || relationship is HasManyAttribute )
345
+ {
346
+ RelationshipAttribute relationshipInDerived = derivedType . GetRelationshipByPublicName ( relationship . PublicName ) ;
347
+ relationshipsToInclude . Add ( relationshipInDerived ) ;
348
+ }
296
349
}
297
350
}
298
351
299
- private static void AssertRelationshipsFound ( HashSet < RelationshipAttribute > relationshipsFound , string relationshipName ,
352
+ private static void AssertRelationshipsFound ( HashSet < RelationshipAttribute > relationshipsFound , string relationshipName , bool requireToMany ,
300
353
IReadOnlyCollection < IncludeTreeNode > parents , int position )
301
354
{
302
355
if ( relationshipsFound . Count > 0 )
@@ -308,13 +361,13 @@ private static void AssertRelationshipsFound(HashSet<RelationshipAttribute> rela
308
361
309
362
bool hasDerivedTypes = parents . Any ( parent => parent . Relationship . RightType . DirectlyDerivedTypes . Count > 0 ) ;
310
363
311
- string message = GetErrorMessageForNoneFound ( relationshipName , parentResourceTypes , hasDerivedTypes ) ;
364
+ string message = GetErrorMessageForNoneFound ( relationshipName , requireToMany , parentResourceTypes , hasDerivedTypes ) ;
312
365
throw new QueryParseException ( message , position ) ;
313
366
}
314
367
315
- private static string GetErrorMessageForNoneFound ( string relationshipName , ResourceType [ ] parentResourceTypes , bool hasDerivedTypes )
368
+ private static string GetErrorMessageForNoneFound ( string relationshipName , bool requireToMany , ResourceType [ ] parentResourceTypes , bool hasDerivedTypes )
316
369
{
317
- var builder = new StringBuilder ( $ "Relationship '{ relationshipName } '") ;
370
+ var builder = new StringBuilder ( $ "{ ( requireToMany ? "To-many relationship" : " Relationship" ) } '{ relationshipName } '") ;
318
371
319
372
if ( parentResourceTypes . Length == 1 )
320
373
{
@@ -345,7 +398,7 @@ private void AssertAtLeastOneCanBeIncluded(HashSet<RelationshipAttribute> relati
345
398
}
346
399
}
347
400
348
- internal sealed class IncludeTreeNode
401
+ private sealed class IncludeTreeNode
349
402
{
350
403
private readonly Dictionary < RelationshipAttribute , IncludeTreeNode > _children = [ ] ;
351
404
@@ -362,7 +415,7 @@ public static IncludeTreeNode CreateRoot(ResourceType resourceType)
362
415
return new IncludeTreeNode ( relationship ) ;
363
416
}
364
417
365
- public IReadOnlyCollection < IncludeTreeNode > EnsureChildren ( RelationshipAttribute [ ] relationships )
418
+ public ReadOnlyCollection < IncludeTreeNode > EnsureChildren ( RelationshipAttribute [ ] relationships )
366
419
{
367
420
foreach ( RelationshipAttribute relationship in relationships )
368
421
{
0 commit comments