Skip to content

Commit c103a30

Browse files
authored
Fix invalid partial json generated when serverside paging is applied in multi-level containment scenarios (#926)
1 parent fa255ff commit c103a30

File tree

6 files changed

+763
-21
lines changed

6 files changed

+763
-21
lines changed

Diff for: src/Microsoft.AspNetCore.OData/Formatter/LinkGenerationHelpers.cs

+114-3
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
using System;
99
using System.Collections.Generic;
1010
using System.ComponentModel;
11+
using System.Diagnostics;
1112
using System.Diagnostics.Contracts;
1213
using System.Linq;
1314
using Microsoft.AspNetCore.OData.Edm;
@@ -69,7 +70,16 @@ public static Uri GenerateNavigationPropertyLink(this ResourceContext resourceCo
6970
throw Error.ArgumentNull(nameof(resourceContext));
7071
}
7172

72-
IList<ODataPathSegment> navigationPathSegments = resourceContext.GenerateBaseODataPathSegments();
73+
IList<ODataPathSegment> navigationPathSegments;
74+
if (resourceContext.NavigationSource is IEdmContainedEntitySet &&
75+
resourceContext.NavigationSource != resourceContext.SerializerContext.Path.NavigationSource())
76+
{
77+
navigationPathSegments = resourceContext.GenerateContainmentODataPathSegments();
78+
}
79+
else
80+
{
81+
navigationPathSegments = resourceContext.GenerateBaseODataPathSegments();
82+
}
7383

7484
if (includeCast)
7585
{
@@ -429,8 +439,7 @@ private static void GenerateBaseODataPathSegments(
429439
// the case.
430440
odataPath.Clear();
431441

432-
IEdmContainedEntitySet containmnent = navigationSource as IEdmContainedEntitySet;
433-
if (containmnent != null)
442+
if (navigationSource is IEdmContainedEntitySet)
434443
{
435444
EdmEntityContainer container = new EdmEntityContainer("NS", "Default");
436445
IEdmEntitySet entitySet = new EdmEntitySet(container, navigationSource.Name,
@@ -465,5 +474,107 @@ private static void GenerateBaseODataPathSegmentsForFeed(
465474
feedContext.EntitySetBase,
466475
odataPath);
467476
}
477+
478+
private static IList<ODataPathSegment> GenerateContainmentODataPathSegments(this ResourceContext resourceContext)
479+
{
480+
List<ODataPathSegment> navigationPathSegments = new List<ODataPathSegment>();
481+
ResourceContext currentResourceContext = resourceContext;
482+
483+
// We loop till the base of the $expand expression then use GenerateBaseODataPathSegments to generate the base path segments
484+
// For instance, given $expand=Tabs($expand=Items($expand=Notes($expand=Tips))), we loop until we get to Tabs at the base
485+
while (currentResourceContext != null && currentResourceContext.NavigationSource != resourceContext.SerializerContext.Path.NavigationSource())
486+
{
487+
if (currentResourceContext.NavigationSource is IEdmContainedEntitySet containedEntitySet)
488+
{
489+
// Type-cast segment for the expanded resource that is passed into the method is added by the caller
490+
if (currentResourceContext != resourceContext && currentResourceContext.StructuredType != containedEntitySet.EntityType())
491+
{
492+
navigationPathSegments.Add(new TypeSegment(currentResourceContext.StructuredType, currentResourceContext.NavigationSource));
493+
}
494+
495+
KeySegment keySegment = new KeySegment(
496+
ConventionsHelpers.GetEntityKey(currentResourceContext),
497+
currentResourceContext.StructuredType as IEdmEntityType,
498+
navigationSource: currentResourceContext.NavigationSource);
499+
navigationPathSegments.Add(keySegment);
500+
501+
NavigationPropertySegment navPropertySegment = new NavigationPropertySegment(
502+
containedEntitySet.NavigationProperty,
503+
containedEntitySet.ParentNavigationSource);
504+
navigationPathSegments.Add(navPropertySegment);
505+
}
506+
else if (currentResourceContext.NavigationSource is IEdmEntitySet entitySet)
507+
{
508+
// We will get here if there's a non-contained entity set on the $expand expression
509+
if (currentResourceContext.StructuredType != entitySet.EntityType())
510+
{
511+
navigationPathSegments.Add(new TypeSegment(currentResourceContext.StructuredType, currentResourceContext.NavigationSource));
512+
}
513+
514+
KeySegment keySegment = new KeySegment(
515+
ConventionsHelpers.GetEntityKey(currentResourceContext),
516+
currentResourceContext.StructuredType as IEdmEntityType,
517+
currentResourceContext.NavigationSource);
518+
navigationPathSegments.Add(keySegment);
519+
520+
EntitySetSegment entitySetSegment = new EntitySetSegment(entitySet);
521+
navigationPathSegments.Add(entitySetSegment);
522+
523+
// Reverse the list such that the segments are in the right order
524+
navigationPathSegments.Reverse();
525+
return navigationPathSegments;
526+
}
527+
else if (currentResourceContext.NavigationSource is IEdmSingleton singleton)
528+
{
529+
// We will get here if there's a singleton on the $expand expression
530+
if (currentResourceContext.StructuredType != singleton.EntityType())
531+
{
532+
navigationPathSegments.Add(new TypeSegment(currentResourceContext.StructuredType, currentResourceContext.NavigationSource));
533+
}
534+
535+
SingletonSegment singletonSegment = new SingletonSegment(singleton);
536+
navigationPathSegments.Add(singletonSegment);
537+
538+
// Reverse the list such that the segments are in the right order
539+
navigationPathSegments.Reverse();
540+
return navigationPathSegments;
541+
}
542+
543+
currentResourceContext = currentResourceContext.SerializerContext.ExpandedResource;
544+
}
545+
546+
Debug.Assert(currentResourceContext != null, "currentResourceContext != null");
547+
// Once we are at the base of the $expand expression, we call GenerateBaseODataPathSegments to generate the base path segments
548+
IList<ODataPathSegment> pathSegments = currentResourceContext.GenerateBaseODataPathSegments();
549+
550+
Debug.Assert(pathSegments.Count > 0, "pathSegments.Count > 0");
551+
552+
ODataPathSegment lastNonKeySegment;
553+
554+
if (pathSegments.Count == 1)
555+
{
556+
lastNonKeySegment = pathSegments[0];
557+
Debug.Assert(lastNonKeySegment is SingletonSegment, "lastNonKeySegment is SingletonSegment");
558+
}
559+
else
560+
{
561+
Debug.Assert(pathSegments[pathSegments.Count - 1] is KeySegment, "pathSegments[pathSegments.Count - 1] is KeySegment");
562+
// 2nd last segment would be NavigationPathSegment or EntitySetSegment
563+
lastNonKeySegment = pathSegments[pathSegments.Count - 2];
564+
}
565+
566+
if (currentResourceContext.StructuredType != lastNonKeySegment.EdmType.AsElementType())
567+
{
568+
pathSegments.Add(new TypeSegment(currentResourceContext.StructuredType, currentResourceContext.NavigationSource));
569+
}
570+
571+
// Add the segments from the $expand expression in reverse order
572+
for (int i = navigationPathSegments.Count - 1; i >= 0; i--)
573+
{
574+
pathSegments.Add(navigationPathSegments[i]);
575+
}
576+
577+
return pathSegments;
578+
}
468579
}
469580
}

Diff for: src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataResourceSetSerializer.cs

+3-17
Original file line numberDiff line numberDiff line change
@@ -530,26 +530,12 @@ private IEnumerable<ODataOperation> CreateODataOperations(IEnumerable<IEdmOperat
530530
private static Uri GetNestedNextPageLink(ODataSerializerContext writeContext, int pageSize, object obj)
531531
{
532532
Contract.Assert(writeContext.ExpandedResource != null);
533-
Uri navigationLink;
534533
IEdmNavigationSource sourceNavigationSource = writeContext.ExpandedResource.NavigationSource;
535534
NavigationSourceLinkBuilderAnnotation linkBuilder = writeContext.Model.GetNavigationSourceLinkBuilder(sourceNavigationSource);
536535

537-
// In Contained Navigation, we don't have navigation property binding,
538-
// Hence we cannot get the NavigationLink from the NavigationLinkBuilder
539-
if (writeContext.NavigationSource.NavigationSourceKind() == EdmNavigationSourceKind.ContainedEntitySet)
540-
{
541-
// Contained navigation.
542-
Uri idlink = linkBuilder.BuildIdLink(writeContext.ExpandedResource);
543-
544-
var link = idlink.ToString() + "/" + writeContext.NavigationProperty.Name;
545-
navigationLink = new Uri(link);
546-
}
547-
else
548-
{
549-
// Non-Contained navigation.
550-
navigationLink =
551-
linkBuilder.BuildNavigationLink(writeContext.ExpandedResource, writeContext.NavigationProperty);
552-
}
536+
Uri navigationLink = linkBuilder.BuildNavigationLink(
537+
writeContext.ExpandedResource,
538+
writeContext.NavigationProperty);
553539

554540
Uri nestedNextLink = GenerateQueryFromExpandedItem(writeContext, navigationLink);
555541

Diff for: test/Microsoft.AspNetCore.OData.E2E.Tests/ServerSidePaging/ServerSidePagingControllers.cs

+134
Original file line numberDiff line numberDiff line change
@@ -155,4 +155,138 @@ public ActionResult<IEnumerable<SkipTokenPagingEdgeCase1Customer>> Get()
155155
return customers;
156156
}
157157
}
158+
159+
public class ContainmentPagingCustomersController : ODataController
160+
{
161+
[EnableQuery(PageSize = 2)]
162+
public ActionResult Get()
163+
{
164+
return Ok(ContainmentPagingDataSource.Customers);
165+
}
166+
167+
[EnableQuery(PageSize = 2)]
168+
public ActionResult GetOrders(int key)
169+
{
170+
var customer = ContainmentPagingDataSource.Customers.SingleOrDefault(d => d.Id == key);
171+
172+
if (customer == null)
173+
{
174+
return BadRequest();
175+
}
176+
177+
return Ok(customer.Orders);
178+
}
179+
}
180+
181+
public class ContainmentPagingCompanyController : ODataController
182+
{
183+
private static readonly ContainmentPagingCustomer company = new ContainmentPagingCustomer
184+
{
185+
Id = 1,
186+
Orders = ContainmentPagingDataSource.Orders.Take(ContainmentPagingDataSource.TargetSize).ToList()
187+
};
188+
189+
[EnableQuery(PageSize = 2)]
190+
public ActionResult Get()
191+
{
192+
return Ok(company);
193+
}
194+
195+
[EnableQuery(PageSize = 2)]
196+
public ActionResult GetOrders()
197+
{
198+
return Ok(company.Orders);
199+
}
200+
}
201+
202+
public class NoContainmentPagingCustomersController : ODataController
203+
{
204+
[EnableQuery(PageSize = 2)]
205+
public ActionResult Get()
206+
{
207+
return Ok(NoContainmentPagingDataSource.Customers);
208+
}
209+
210+
[EnableQuery(PageSize = 2)]
211+
public ActionResult GetOrders(int key)
212+
{
213+
var customer = NoContainmentPagingDataSource.Customers.SingleOrDefault(d => d.Id == key);
214+
215+
if (customer == null)
216+
{
217+
return BadRequest();
218+
}
219+
220+
return Ok(customer.Orders);
221+
}
222+
}
223+
224+
public class ContainmentPagingMenusController : ODataController
225+
{
226+
[EnableQuery(PageSize = 2, MaxExpansionDepth = 4)]
227+
public ActionResult Get()
228+
{
229+
return Ok(ContainmentPagingDataSource.Menus);
230+
}
231+
232+
[EnableQuery(PageSize = 2, MaxExpansionDepth = 4)]
233+
public ActionResult GetFromContainmentPagingExtendedMenu()
234+
{
235+
return Ok(ContainmentPagingDataSource.Menus.OfType<ContainmentPagingExtendedMenu>());
236+
}
237+
238+
[EnableQuery(PageSize = 2, MaxExpansionDepth = 4)]
239+
public ActionResult GetTabsFromContainmentPagingExtendedMenu(int key)
240+
{
241+
var menu = ContainmentPagingDataSource.Menus.OfType<ContainmentPagingExtendedMenu>().SingleOrDefault(d => d.Id == key);
242+
243+
if (menu == null)
244+
{
245+
return BadRequest();
246+
}
247+
248+
return Ok(menu.Tabs);
249+
}
250+
251+
[EnableQuery(PageSize = 2, MaxExpansionDepth = 4)]
252+
public ActionResult GetPanelsFromContainmentPagingExtendedMenu(int key)
253+
{
254+
var menu = ContainmentPagingDataSource.Menus.OfType<ContainmentPagingExtendedMenu>().SingleOrDefault(d => d.Id == key);
255+
256+
if (menu == null)
257+
{
258+
return BadRequest();
259+
}
260+
261+
return Ok(menu.Panels);
262+
}
263+
}
264+
265+
public class ContainmentPagingRibbonController : ODataController
266+
{
267+
private static readonly ContainmentPagingMenu ribbon = new ContainmentPagingExtendedMenu
268+
{
269+
Id = 1,
270+
Tabs = ContainmentPagingDataSource.Tabs.Take(ContainmentPagingDataSource.TargetSize).ToList()
271+
};
272+
273+
[EnableQuery(PageSize = 2, MaxExpansionDepth = 4)]
274+
public ActionResult Get()
275+
{
276+
return Ok(ribbon);
277+
}
278+
279+
[EnableQuery(PageSize = 2, MaxExpansionDepth = 4)]
280+
public ActionResult GetFromContainmentPagingExtendedMenu()
281+
{
282+
return Ok(ribbon as ContainmentPagingExtendedMenu);
283+
}
284+
285+
[EnableQuery(PageSize = 2, MaxExpansionDepth = 4)]
286+
[HttpGet("ContainmentPagingRibbon/Microsoft.AspNetCore.OData.E2E.Tests.ServerSidePaging.ContainmentPagingExtendedMenu/Tabs")]
287+
public ActionResult GetTabsFromContainmentPagingExtendedMenu()
288+
{
289+
return Ok((ribbon as ContainmentPagingExtendedMenu).Tabs);
290+
}
291+
}
158292
}

0 commit comments

Comments
 (0)