Skip to content

Commit

Permalink
Fix invalid partial json generated when serverside paging is applied …
Browse files Browse the repository at this point in the history
…in multi-level containment scenarios (#926)
  • Loading branch information
gathogojr authored May 18, 2023
1 parent fa255ff commit c103a30
Show file tree
Hide file tree
Showing 6 changed files with 763 additions and 21 deletions.
117 changes: 114 additions & 3 deletions src/Microsoft.AspNetCore.OData/Formatter/LinkGenerationHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Diagnostics.Contracts;
using System.Linq;
using Microsoft.AspNetCore.OData.Edm;
Expand Down Expand Up @@ -69,7 +70,16 @@ public static Uri GenerateNavigationPropertyLink(this ResourceContext resourceCo
throw Error.ArgumentNull(nameof(resourceContext));
}

IList<ODataPathSegment> navigationPathSegments = resourceContext.GenerateBaseODataPathSegments();
IList<ODataPathSegment> navigationPathSegments;
if (resourceContext.NavigationSource is IEdmContainedEntitySet &&
resourceContext.NavigationSource != resourceContext.SerializerContext.Path.NavigationSource())
{
navigationPathSegments = resourceContext.GenerateContainmentODataPathSegments();
}
else
{
navigationPathSegments = resourceContext.GenerateBaseODataPathSegments();
}

if (includeCast)
{
Expand Down Expand Up @@ -429,8 +439,7 @@ private static void GenerateBaseODataPathSegments(
// the case.
odataPath.Clear();

IEdmContainedEntitySet containmnent = navigationSource as IEdmContainedEntitySet;
if (containmnent != null)
if (navigationSource is IEdmContainedEntitySet)
{
EdmEntityContainer container = new EdmEntityContainer("NS", "Default");
IEdmEntitySet entitySet = new EdmEntitySet(container, navigationSource.Name,
Expand Down Expand Up @@ -465,5 +474,107 @@ private static void GenerateBaseODataPathSegmentsForFeed(
feedContext.EntitySetBase,
odataPath);
}

private static IList<ODataPathSegment> GenerateContainmentODataPathSegments(this ResourceContext resourceContext)
{
List<ODataPathSegment> navigationPathSegments = new List<ODataPathSegment>();
ResourceContext currentResourceContext = resourceContext;

// We loop till the base of the $expand expression then use GenerateBaseODataPathSegments to generate the base path segments
// For instance, given $expand=Tabs($expand=Items($expand=Notes($expand=Tips))), we loop until we get to Tabs at the base
while (currentResourceContext != null && currentResourceContext.NavigationSource != resourceContext.SerializerContext.Path.NavigationSource())
{
if (currentResourceContext.NavigationSource is IEdmContainedEntitySet containedEntitySet)
{
// Type-cast segment for the expanded resource that is passed into the method is added by the caller
if (currentResourceContext != resourceContext && currentResourceContext.StructuredType != containedEntitySet.EntityType())
{
navigationPathSegments.Add(new TypeSegment(currentResourceContext.StructuredType, currentResourceContext.NavigationSource));
}

KeySegment keySegment = new KeySegment(
ConventionsHelpers.GetEntityKey(currentResourceContext),
currentResourceContext.StructuredType as IEdmEntityType,
navigationSource: currentResourceContext.NavigationSource);
navigationPathSegments.Add(keySegment);

NavigationPropertySegment navPropertySegment = new NavigationPropertySegment(
containedEntitySet.NavigationProperty,
containedEntitySet.ParentNavigationSource);
navigationPathSegments.Add(navPropertySegment);
}
else if (currentResourceContext.NavigationSource is IEdmEntitySet entitySet)
{
// We will get here if there's a non-contained entity set on the $expand expression
if (currentResourceContext.StructuredType != entitySet.EntityType())
{
navigationPathSegments.Add(new TypeSegment(currentResourceContext.StructuredType, currentResourceContext.NavigationSource));
}

KeySegment keySegment = new KeySegment(
ConventionsHelpers.GetEntityKey(currentResourceContext),
currentResourceContext.StructuredType as IEdmEntityType,
currentResourceContext.NavigationSource);
navigationPathSegments.Add(keySegment);

EntitySetSegment entitySetSegment = new EntitySetSegment(entitySet);
navigationPathSegments.Add(entitySetSegment);

// Reverse the list such that the segments are in the right order
navigationPathSegments.Reverse();
return navigationPathSegments;
}
else if (currentResourceContext.NavigationSource is IEdmSingleton singleton)
{
// We will get here if there's a singleton on the $expand expression
if (currentResourceContext.StructuredType != singleton.EntityType())
{
navigationPathSegments.Add(new TypeSegment(currentResourceContext.StructuredType, currentResourceContext.NavigationSource));
}

SingletonSegment singletonSegment = new SingletonSegment(singleton);
navigationPathSegments.Add(singletonSegment);

// Reverse the list such that the segments are in the right order
navigationPathSegments.Reverse();
return navigationPathSegments;
}

currentResourceContext = currentResourceContext.SerializerContext.ExpandedResource;
}

Debug.Assert(currentResourceContext != null, "currentResourceContext != null");
// Once we are at the base of the $expand expression, we call GenerateBaseODataPathSegments to generate the base path segments
IList<ODataPathSegment> pathSegments = currentResourceContext.GenerateBaseODataPathSegments();

Debug.Assert(pathSegments.Count > 0, "pathSegments.Count > 0");

ODataPathSegment lastNonKeySegment;

if (pathSegments.Count == 1)
{
lastNonKeySegment = pathSegments[0];
Debug.Assert(lastNonKeySegment is SingletonSegment, "lastNonKeySegment is SingletonSegment");
}
else
{
Debug.Assert(pathSegments[pathSegments.Count - 1] is KeySegment, "pathSegments[pathSegments.Count - 1] is KeySegment");
// 2nd last segment would be NavigationPathSegment or EntitySetSegment
lastNonKeySegment = pathSegments[pathSegments.Count - 2];
}

if (currentResourceContext.StructuredType != lastNonKeySegment.EdmType.AsElementType())
{
pathSegments.Add(new TypeSegment(currentResourceContext.StructuredType, currentResourceContext.NavigationSource));
}

// Add the segments from the $expand expression in reverse order
for (int i = navigationPathSegments.Count - 1; i >= 0; i--)
{
pathSegments.Add(navigationPathSegments[i]);
}

return pathSegments;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -530,26 +530,12 @@ private IEnumerable<ODataOperation> CreateODataOperations(IEnumerable<IEdmOperat
private static Uri GetNestedNextPageLink(ODataSerializerContext writeContext, int pageSize, object obj)
{
Contract.Assert(writeContext.ExpandedResource != null);
Uri navigationLink;
IEdmNavigationSource sourceNavigationSource = writeContext.ExpandedResource.NavigationSource;
NavigationSourceLinkBuilderAnnotation linkBuilder = writeContext.Model.GetNavigationSourceLinkBuilder(sourceNavigationSource);

// In Contained Navigation, we don't have navigation property binding,
// Hence we cannot get the NavigationLink from the NavigationLinkBuilder
if (writeContext.NavigationSource.NavigationSourceKind() == EdmNavigationSourceKind.ContainedEntitySet)
{
// Contained navigation.
Uri idlink = linkBuilder.BuildIdLink(writeContext.ExpandedResource);

var link = idlink.ToString() + "/" + writeContext.NavigationProperty.Name;
navigationLink = new Uri(link);
}
else
{
// Non-Contained navigation.
navigationLink =
linkBuilder.BuildNavigationLink(writeContext.ExpandedResource, writeContext.NavigationProperty);
}
Uri navigationLink = linkBuilder.BuildNavigationLink(
writeContext.ExpandedResource,
writeContext.NavigationProperty);

Uri nestedNextLink = GenerateQueryFromExpandedItem(writeContext, navigationLink);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -155,4 +155,138 @@ public ActionResult<IEnumerable<SkipTokenPagingEdgeCase1Customer>> Get()
return customers;
}
}

public class ContainmentPagingCustomersController : ODataController
{
[EnableQuery(PageSize = 2)]
public ActionResult Get()
{
return Ok(ContainmentPagingDataSource.Customers);
}

[EnableQuery(PageSize = 2)]
public ActionResult GetOrders(int key)
{
var customer = ContainmentPagingDataSource.Customers.SingleOrDefault(d => d.Id == key);

if (customer == null)
{
return BadRequest();
}

return Ok(customer.Orders);
}
}

public class ContainmentPagingCompanyController : ODataController
{
private static readonly ContainmentPagingCustomer company = new ContainmentPagingCustomer
{
Id = 1,
Orders = ContainmentPagingDataSource.Orders.Take(ContainmentPagingDataSource.TargetSize).ToList()
};

[EnableQuery(PageSize = 2)]
public ActionResult Get()
{
return Ok(company);
}

[EnableQuery(PageSize = 2)]
public ActionResult GetOrders()
{
return Ok(company.Orders);
}
}

public class NoContainmentPagingCustomersController : ODataController
{
[EnableQuery(PageSize = 2)]
public ActionResult Get()
{
return Ok(NoContainmentPagingDataSource.Customers);
}

[EnableQuery(PageSize = 2)]
public ActionResult GetOrders(int key)
{
var customer = NoContainmentPagingDataSource.Customers.SingleOrDefault(d => d.Id == key);

if (customer == null)
{
return BadRequest();
}

return Ok(customer.Orders);
}
}

public class ContainmentPagingMenusController : ODataController
{
[EnableQuery(PageSize = 2, MaxExpansionDepth = 4)]
public ActionResult Get()
{
return Ok(ContainmentPagingDataSource.Menus);
}

[EnableQuery(PageSize = 2, MaxExpansionDepth = 4)]
public ActionResult GetFromContainmentPagingExtendedMenu()
{
return Ok(ContainmentPagingDataSource.Menus.OfType<ContainmentPagingExtendedMenu>());
}

[EnableQuery(PageSize = 2, MaxExpansionDepth = 4)]
public ActionResult GetTabsFromContainmentPagingExtendedMenu(int key)
{
var menu = ContainmentPagingDataSource.Menus.OfType<ContainmentPagingExtendedMenu>().SingleOrDefault(d => d.Id == key);

if (menu == null)
{
return BadRequest();
}

return Ok(menu.Tabs);
}

[EnableQuery(PageSize = 2, MaxExpansionDepth = 4)]
public ActionResult GetPanelsFromContainmentPagingExtendedMenu(int key)
{
var menu = ContainmentPagingDataSource.Menus.OfType<ContainmentPagingExtendedMenu>().SingleOrDefault(d => d.Id == key);

if (menu == null)
{
return BadRequest();
}

return Ok(menu.Panels);
}
}

public class ContainmentPagingRibbonController : ODataController
{
private static readonly ContainmentPagingMenu ribbon = new ContainmentPagingExtendedMenu
{
Id = 1,
Tabs = ContainmentPagingDataSource.Tabs.Take(ContainmentPagingDataSource.TargetSize).ToList()
};

[EnableQuery(PageSize = 2, MaxExpansionDepth = 4)]
public ActionResult Get()
{
return Ok(ribbon);
}

[EnableQuery(PageSize = 2, MaxExpansionDepth = 4)]
public ActionResult GetFromContainmentPagingExtendedMenu()
{
return Ok(ribbon as ContainmentPagingExtendedMenu);
}

[EnableQuery(PageSize = 2, MaxExpansionDepth = 4)]
[HttpGet("ContainmentPagingRibbon/Microsoft.AspNetCore.OData.E2E.Tests.ServerSidePaging.ContainmentPagingExtendedMenu/Tabs")]
public ActionResult GetTabsFromContainmentPagingExtendedMenu()
{
return Ok((ribbon as ContainmentPagingExtendedMenu).Tabs);
}
}
}
Loading

0 comments on commit c103a30

Please sign in to comment.