@@ -193,7 +193,47 @@ protected void AssertTokenStackIsEmpty()
193193 }
194194 }
195195
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 )
197237 {
198238 // A relationship name usually matches a single relationship, even when overridden in derived types.
199239 // But in the following case, two relationships are matched on GET /shoppingBaskets?include=items:
@@ -221,29 +261,35 @@ private protected void ParseRelationshipChain(IncludeTreeNode treeRoot)
221261 // that there's currently no way to include Products without Articles. We could add such optional upcast syntax
222262 // in the future, if desired.
223263
224- ReadOnlyCollection < IncludeTreeNode > children = ParseRelationshipName ( [ treeRoot ] ) ;
264+ ReadOnlyCollection < IncludeTreeNode > children = ParseRelationshipName ( [ treeRoot ] , requireChainEndsInToMany , alternativeErrorMessage ) ;
225265
226266 while ( TokenStack . TryPeek ( out Token ? nextToken ) && nextToken . Kind == TokenKind . Period )
227267 {
228268 EatSingleCharacterToken ( TokenKind . Period ) ;
229269
230- children = ParseRelationshipName ( children ) ;
270+ children = ParseRelationshipName ( children , requireChainEndsInToMany , null ) ;
231271 }
232272 }
233273
234- private ReadOnlyCollection < IncludeTreeNode > ParseRelationshipName ( IReadOnlyCollection < IncludeTreeNode > parents )
274+ private ReadOnlyCollection < IncludeTreeNode > ParseRelationshipName ( IReadOnlyCollection < IncludeTreeNode > parents , bool requireChainEndsInToMany ,
275+ string ? alternativeErrorMessage )
235276 {
236277 int position = GetNextTokenPositionOrEnd ( ) ;
237278
238279 if ( TokenStack . TryPop ( out Token ? token ) && token . Kind == TokenKind . Text )
239280 {
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 ) ;
241285 }
242286
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 ) ;
244289 }
245290
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 )
247293 {
248294 List < IncludeTreeNode > children = [ ] ;
249295 HashSet < RelationshipAttribute > relationshipsFound = [ ] ;
@@ -252,51 +298,58 @@ private ReadOnlyCollection<IncludeTreeNode> LookupRelationshipName(string relati
252298 {
253299 // Depending on the left side of the include chain, we may match relationships anywhere in the resource type hierarchy.
254300 // 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 ) ;
256302
257303 if ( relationships . Count > 0 )
258304 {
259305 relationshipsFound . UnionWith ( relationships ) ;
260306
261307 RelationshipAttribute [ ] relationshipsToInclude = relationships . Where ( relationship => ! relationship . IsIncludeBlocked ( ) ) . ToArray ( ) ;
262- IReadOnlyCollection < IncludeTreeNode > affectedChildren = parent . EnsureChildren ( relationshipsToInclude ) ;
308+ ReadOnlyCollection < IncludeTreeNode > affectedChildren = parent . EnsureChildren ( relationshipsToInclude ) ;
263309 children . AddRange ( affectedChildren ) ;
264310 }
265311 }
266312
267- AssertRelationshipsFound ( relationshipsFound , relationshipName , parents , position ) ;
313+ AssertRelationshipsFound ( relationshipsFound , relationshipName , requireToMany , parents , position ) ;
268314 AssertAtLeastOneCanBeIncluded ( relationshipsFound , relationshipName , position ) ;
269315
270316 return children . AsReadOnly ( ) ;
271317 }
272318
273- private static HashSet < RelationshipAttribute > GetRelationshipsInConcreteTypes ( ResourceType resourceType , string relationshipName )
319+ private static HashSet < RelationshipAttribute > GetRelationshipsInConcreteTypes ( ResourceType resourceType , string relationshipName , bool requireToMany )
274320 {
275321 HashSet < RelationshipAttribute > relationshipsToInclude = [ ] ;
276322
277323 foreach ( RelationshipAttribute relationship in resourceType . GetRelationshipsInTypeOrDerived ( relationshipName ) )
278324 {
279- if ( ! relationship . LeftType . ClrType . IsAbstract )
325+ if ( ! requireToMany || relationship is HasManyAttribute )
280326 {
281- relationshipsToInclude . Add ( relationship ) ;
327+ if ( ! relationship . LeftType . ClrType . IsAbstract )
328+ {
329+ relationshipsToInclude . Add ( relationship ) ;
330+ }
282331 }
283332
284- IncludeRelationshipsFromConcreteDerivedTypes ( relationship , relationshipsToInclude ) ;
333+ IncludeRelationshipsFromConcreteDerivedTypes ( relationship , requireToMany , relationshipsToInclude ) ;
285334 }
286335
287336 return relationshipsToInclude ;
288337 }
289338
290- private static void IncludeRelationshipsFromConcreteDerivedTypes ( RelationshipAttribute relationship , HashSet < RelationshipAttribute > relationshipsToInclude )
339+ private static void IncludeRelationshipsFromConcreteDerivedTypes ( RelationshipAttribute relationship , bool requireToMany ,
340+ HashSet < RelationshipAttribute > relationshipsToInclude )
291341 {
292342 foreach ( ResourceType derivedType in relationship . LeftType . GetAllConcreteDerivedTypes ( ) )
293343 {
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+ }
296349 }
297350 }
298351
299- private static void AssertRelationshipsFound ( HashSet < RelationshipAttribute > relationshipsFound , string relationshipName ,
352+ private static void AssertRelationshipsFound ( HashSet < RelationshipAttribute > relationshipsFound , string relationshipName , bool requireToMany ,
300353 IReadOnlyCollection < IncludeTreeNode > parents , int position )
301354 {
302355 if ( relationshipsFound . Count > 0 )
@@ -308,13 +361,13 @@ private static void AssertRelationshipsFound(HashSet<RelationshipAttribute> rela
308361
309362 bool hasDerivedTypes = parents . Any ( parent => parent . Relationship . RightType . DirectlyDerivedTypes . Count > 0 ) ;
310363
311- string message = GetErrorMessageForNoneFound ( relationshipName , parentResourceTypes , hasDerivedTypes ) ;
364+ string message = GetErrorMessageForNoneFound ( relationshipName , requireToMany , parentResourceTypes , hasDerivedTypes ) ;
312365 throw new QueryParseException ( message , position ) ;
313366 }
314367
315- private static string GetErrorMessageForNoneFound ( string relationshipName , ResourceType [ ] parentResourceTypes , bool hasDerivedTypes )
368+ private static string GetErrorMessageForNoneFound ( string relationshipName , bool requireToMany , ResourceType [ ] parentResourceTypes , bool hasDerivedTypes )
316369 {
317- var builder = new StringBuilder ( $ "Relationship '{ relationshipName } '") ;
370+ var builder = new StringBuilder ( $ "{ ( requireToMany ? "To-many relationship" : " Relationship" ) } '{ relationshipName } '") ;
318371
319372 if ( parentResourceTypes . Length == 1 )
320373 {
@@ -345,7 +398,7 @@ private void AssertAtLeastOneCanBeIncluded(HashSet<RelationshipAttribute> relati
345398 }
346399 }
347400
348- internal sealed class IncludeTreeNode
401+ private sealed class IncludeTreeNode
349402 {
350403 private readonly Dictionary < RelationshipAttribute , IncludeTreeNode > _children = [ ] ;
351404
@@ -362,7 +415,7 @@ public static IncludeTreeNode CreateRoot(ResourceType resourceType)
362415 return new IncludeTreeNode ( relationship ) ;
363416 }
364417
365- public IReadOnlyCollection < IncludeTreeNode > EnsureChildren ( RelationshipAttribute [ ] relationships )
418+ public ReadOnlyCollection < IncludeTreeNode > EnsureChildren ( RelationshipAttribute [ ] relationships )
366419 {
367420 foreach ( RelationshipAttribute relationship in relationships )
368421 {
0 commit comments