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 scenario (#2771)
  • Loading branch information
gathogojr authored May 16, 2023
1 parent 78b8383 commit 1130c74
Show file tree
Hide file tree
Showing 12 changed files with 821 additions and 35 deletions.
55 changes: 45 additions & 10 deletions src/Microsoft.AspNet.OData.Shared/Builder/EdmModelHelperMethods.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ public static IEdmModel BuildEdmModel(ODataModelBuilder builder)
// Build the navigation source map
IDictionary<string, EdmNavigationSource> navigationSourceMap = model.GetNavigationSourceMap(edmMap, navigationSources);

// Add navigation property link builders
AddNavigationPropertyLinkBuilders(builder, edmMap, navigationSources);

// Add the core and validation vocabulary annotations
model.AddCoreAndValidationVocabularyAnnotations(navigationSources, edmMap);

Expand Down Expand Up @@ -133,30 +136,31 @@ private static IDictionary<string, EdmNavigationSource> GetNavigationSourceMap(t
model.SetAnnotationValue(navigationSource, navigationSourceAndAnnotation.Url);
model.SetNavigationSourceLinkBuilder(navigationSource, navigationSourceAndAnnotation.LinkBuilder);

AddNavigationBindings(edmMap, navigationSourceAndAnnotation.Configuration, navigationSource, navigationSourceAndAnnotation.LinkBuilder,
edmNavigationSourceMap);
AddNavigationBindings(edmMap, navigationSourceAndAnnotation.Configuration, navigationSource, edmNavigationSourceMap);
}

return edmNavigationSourceMap;
}

private static void AddNavigationBindings(EdmTypeMap edmMap,
private static void AddNavigationBindings(
EdmTypeMap edmMap,
NavigationSourceConfiguration navigationSourceConfiguration,
EdmNavigationSource navigationSource,
NavigationSourceLinkBuilderAnnotation linkBuilder,
Dictionary<string, EdmNavigationSource> edmNavigationSourceMap)
{
foreach (var binding in navigationSourceConfiguration.Bindings)
foreach (NavigationPropertyBindingConfiguration binding in navigationSourceConfiguration.Bindings)
{
NavigationPropertyConfiguration navigationProperty = binding.NavigationProperty;
bool isContained = navigationProperty.ContainsTarget;

IEdmType edmType = edmMap.EdmTypes[navigationProperty.DeclaringType.ClrType];
IEdmStructuredType structuraType = edmType as IEdmStructuredType;
IEdmNavigationProperty edmNavigationProperty = structuraType.NavigationProperties()
IEdmStructuredType structuralType = edmType as IEdmStructuredType;
IEdmNavigationProperty edmNavigationProperty = structuralType.NavigationProperties()
.Single(np => np.Name == navigationProperty.Name);

string bindingPath = ConvertBindingPath(edmMap, binding);
// This check for whether navigation property is contained is possibly redundant
// Unless contained navigation properties can have navigation property bindings...
if (!isContained)
{
// calculate the binding path
Expand All @@ -165,11 +169,42 @@ private static void AddNavigationBindings(EdmTypeMap edmMap,
edmNavigationSourceMap[binding.TargetNavigationSource.Name],
new EdmPathExpression(bindingPath));
}
}
}

/// <summary>
/// Adds navigation property link builders
/// </summary>
/// <param name="modelBuilder">The model builder.</param>
/// <param name="edmMap">Edm type mappings.</param>
/// <param name="navigationSourceAndAnnotations">The navigation source annotations.</param>
private static void AddNavigationPropertyLinkBuilders(
ODataModelBuilder modelBuilder,
EdmTypeMap edmMap,
IEnumerable<NavigationSourceAndAnnotations> navigationSourceAndAnnotations)
{
foreach (NavigationSourceAndAnnotations navigationSourceAndAnnotation in navigationSourceAndAnnotations)
{
NavigationSourceConfiguration navigationSourceConfiguration = navigationSourceAndAnnotation.Configuration;
NavigationSourceLinkBuilderAnnotation linkBuilder = navigationSourceAndAnnotation.LinkBuilder;

IEnumerable<EntityTypeConfiguration> derivedEntityTypeConfigurations = modelBuilder.DerivedTypes(navigationSourceConfiguration.EntityType);

NavigationLinkBuilder linkBuilderFunc = navigationSourceConfiguration.GetNavigationPropertyLink(navigationProperty);
if (linkBuilderFunc != null)
foreach (EntityTypeConfiguration entityTypeConfiguration in new[] { navigationSourceConfiguration.EntityType }.Concat(derivedEntityTypeConfigurations))
{
linkBuilder.AddNavigationPropertyLinkBuilder(edmNavigationProperty, linkBuilderFunc);
foreach (NavigationPropertyConfiguration navigationProperty in entityTypeConfiguration.NavigationProperties)
{
IEdmType edmType = edmMap.EdmTypes[navigationProperty.DeclaringType.ClrType];
IEdmStructuredType structuralType = edmType as IEdmStructuredType;
IEdmNavigationProperty edmNavigationProperty = structuralType.NavigationProperties()
.First(np => np.Name == navigationProperty.Name);

NavigationLinkBuilder linkBuilderFunc = navigationSourceConfiguration.GetNavigationPropertyLink(navigationProperty);
if (linkBuilderFunc != null)
{
linkBuilder.AddNavigationPropertyLinkBuilder(edmNavigationProperty, linkBuilderFunc);
}
}
}
}
}
Expand Down
117 changes: 114 additions & 3 deletions src/Microsoft.AspNet.OData.Shared/Builder/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.AspNet.OData.Builder.Conventions;
Expand Down Expand Up @@ -81,7 +82,16 @@ public static Uri GenerateNavigationPropertyLink(this ResourceContext resourceCo
throw Error.Argument("resourceContext", SRResources.UrlHelperNull, typeof(ResourceContext).Name);
}

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 @@ -514,8 +524,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 @@ -550,5 +559,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.InternalRequest.Context.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 @@ -10,8 +10,8 @@
namespace Microsoft.AspNet.OData.Builder
{
/// <summary>
/// This class is used in internal as a helper class to build the Edm model.
/// This class wrappers a relationship between Edm type and the CLR type configuration.
/// This class is used internally as a helper class to build the Edm model.
/// This class wraps a relationship between Edm type and the CLR type configuration.
/// This relationship is used to builder the navigation property and the corresponding links.
/// </summary>
internal class NavigationSourceAndAnnotations
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -481,26 +481,12 @@ private IEnumerable<ODataOperation> CreateODataOperations(IEnumerable<IEdmOperat
private static Uri GetNestedNextPageLink(ODataSerializerContext writeContext, int pageSize, object obj)
{
Contract.Assert(writeContext.ExpandedResource != null);
Uri navigationLink = null;
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 @@ -1705,6 +1705,9 @@
<Compile Include="..\ServerSidePaging\ServerSidePagingDataModel.cs">
<Link>ServerSidePaging\ServerSidePagingDataModel.cs</Link>
</Compile>
<Compile Include="..\ServerSidePaging\ServerSidePagingDataSource.cs">
<Link>ServerSidePaging\ServerSidePagingDataSource.cs</Link>
</Compile>
<Compile Include="..\ServerSidePaging\ServerSidePagingTests.cs">
<Link>ServerSidePaging\ServerSidePagingTests.cs</Link>
</Compile>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1561,6 +1561,9 @@
<Compile Include="..\ServerSidePaging\ServerSidePagingDataModel.cs">
<Link>ServerSidePaging\ServerSidePagingDataModel.cs</Link>
</Compile>
<Compile Include="..\ServerSidePaging\ServerSidePagingDataSource.cs">
<Link>ServerSidePaging\ServerSidePagingDataSource.cs</Link>
</Compile>
<Compile Include="..\ServerSidePaging\ServerSidePagingTests.cs">
<Link>ServerSidePaging\ServerSidePagingTests.cs</Link>
</Compile>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1559,6 +1559,9 @@
<Compile Include="..\ServerSidePaging\ServerSidePagingDataModel.cs">
<Link>ServerSidePaging\ServerSidePagingDataModel.cs</Link>
</Compile>
<Compile Include="..\ServerSidePaging\ServerSidePagingDataSource.cs">
<Link>ServerSidePaging\ServerSidePagingDataSource.cs</Link>
</Compile>
<Compile Include="..\ServerSidePaging\ServerSidePagingTests.cs">
<Link>ServerSidePaging\ServerSidePagingTests.cs</Link>
</Compile>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
// </copyright>
//------------------------------------------------------------------------------

using System;
using Microsoft.AspNet.OData;
using Microsoft.AspNet.OData.Builder;
using Microsoft.OData.Edm;
using Microsoft.Test.E2E.AspNet.OData.Common.Execution;
Expand Down Expand Up @@ -42,15 +44,29 @@ public static IEdmModel GetExplicitModel()
statementType.Property(s => s.TransactionDescription);
statementType.Property(s => s.Amount);

Func<ResourceContext<Account>, IEdmNavigationProperty, Uri> navigationPropertyLinkFactory = (
resourceContext, navigationProperty) => resourceContext.GenerateNavigationPropertyLink(navigationProperty, false);
Func<ResourceContext<Account>, IEdmNavigationProperty, Uri> navigationPropertyLinkFactoryWithCast = (
resourceContext, navigationProperty) => resourceContext.GenerateNavigationPropertyLink(navigationProperty, true);

var accounts = builder.EntitySet<Account>("Accounts");
accounts.HasIdLink(c => c.GenerateSelfLink(false), true);
accounts.HasEditLink(c => c.GenerateSelfLink(true), true);
accounts.HasNavigationPropertyLink(payoutPI, navigationPropertyLinkFactory, followsConventions: true);
accounts.HasNavigationPropertyLink(payinPIs, navigationPropertyLinkFactory, followsConventions: true);
accounts.HasNavigationPropertyLink(giftCard, navigationPropertyLinkFactoryWithCast, true);

var paginatedAccounts = builder.EntitySet<Account>("PaginatedAccounts");
paginatedAccounts.HasIdLink(c => c.GenerateSelfLink(true), true);
paginatedAccounts.HasEditLink(c => c.GenerateSelfLink(true), true);

builder.Singleton<Account>("AnonymousAccount");
paginatedAccounts.HasNavigationPropertyLink(payoutPI, navigationPropertyLinkFactory, followsConventions: true);
paginatedAccounts.HasNavigationPropertyLink(payinPIs, navigationPropertyLinkFactory, followsConventions: true);
paginatedAccounts.HasNavigationPropertyLink(giftCard, navigationPropertyLinkFactoryWithCast, true);

var accountSingleton = builder.Singleton<Account>("AnonymousAccount");
accountSingleton.HasNavigationPropertyLink(payoutPI, navigationPropertyLinkFactory, followsConventions: true);
accountSingleton.HasNavigationPropertyLink(payinPIs, navigationPropertyLinkFactory, followsConventions: true);
accountSingleton.HasNavigationPropertyLink(giftCard, navigationPropertyLinkFactoryWithCast, true);

AddBoundActionsAndFunctions(builder);

Expand Down
Loading

0 comments on commit 1130c74

Please sign in to comment.