diff --git a/src/Microsoft.AspNet.OData.Shared/Builder/EdmModelHelperMethods.cs b/src/Microsoft.AspNet.OData.Shared/Builder/EdmModelHelperMethods.cs index 8337a6faa2..dd761fa7f0 100644 --- a/src/Microsoft.AspNet.OData.Shared/Builder/EdmModelHelperMethods.cs +++ b/src/Microsoft.AspNet.OData.Shared/Builder/EdmModelHelperMethods.cs @@ -50,6 +50,9 @@ public static IEdmModel BuildEdmModel(ODataModelBuilder builder) // Build the navigation source map IDictionary 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); @@ -133,30 +136,31 @@ private static IDictionary 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 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 @@ -165,11 +169,42 @@ private static void AddNavigationBindings(EdmTypeMap edmMap, edmNavigationSourceMap[binding.TargetNavigationSource.Name], new EdmPathExpression(bindingPath)); } + } + } + + /// + /// Adds navigation property link builders + /// + /// The model builder. + /// Edm type mappings. + /// The navigation source annotations. + private static void AddNavigationPropertyLinkBuilders( + ODataModelBuilder modelBuilder, + EdmTypeMap edmMap, + IEnumerable navigationSourceAndAnnotations) + { + foreach (NavigationSourceAndAnnotations navigationSourceAndAnnotation in navigationSourceAndAnnotations) + { + NavigationSourceConfiguration navigationSourceConfiguration = navigationSourceAndAnnotation.Configuration; + NavigationSourceLinkBuilderAnnotation linkBuilder = navigationSourceAndAnnotation.LinkBuilder; + + IEnumerable 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); + } + } } } } diff --git a/src/Microsoft.AspNet.OData.Shared/Builder/LinkGenerationHelpers.cs b/src/Microsoft.AspNet.OData.Shared/Builder/LinkGenerationHelpers.cs index 5e4d1c08ad..f638ba297e 100644 --- a/src/Microsoft.AspNet.OData.Shared/Builder/LinkGenerationHelpers.cs +++ b/src/Microsoft.AspNet.OData.Shared/Builder/LinkGenerationHelpers.cs @@ -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; @@ -81,7 +82,16 @@ public static Uri GenerateNavigationPropertyLink(this ResourceContext resourceCo throw Error.Argument("resourceContext", SRResources.UrlHelperNull, typeof(ResourceContext).Name); } - IList navigationPathSegments = resourceContext.GenerateBaseODataPathSegments(); + IList navigationPathSegments; + if (resourceContext.NavigationSource is IEdmContainedEntitySet && + resourceContext.NavigationSource != resourceContext.SerializerContext.Path.NavigationSource) + { + navigationPathSegments = resourceContext.GenerateContainmentODataPathSegments(); + } + else + { + navigationPathSegments = resourceContext.GenerateBaseODataPathSegments(); + } if (includeCast) { @@ -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, @@ -550,5 +559,107 @@ private static void GenerateBaseODataPathSegmentsForFeed( feedContext.EntitySetBase, odataPath); } + + private static IList GenerateContainmentODataPathSegments(this ResourceContext resourceContext) + { + List navigationPathSegments = new List(); + 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 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; + } } } diff --git a/src/Microsoft.AspNet.OData.Shared/Builder/NavigationSourceAndAnnotations.cs b/src/Microsoft.AspNet.OData.Shared/Builder/NavigationSourceAndAnnotations.cs index caec384501..3a0b65b5dd 100644 --- a/src/Microsoft.AspNet.OData.Shared/Builder/NavigationSourceAndAnnotations.cs +++ b/src/Microsoft.AspNet.OData.Shared/Builder/NavigationSourceAndAnnotations.cs @@ -10,8 +10,8 @@ namespace Microsoft.AspNet.OData.Builder { /// - /// 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. /// internal class NavigationSourceAndAnnotations diff --git a/src/Microsoft.AspNet.OData.Shared/Formatter/Serialization/ODataResourceSetSerializer.cs b/src/Microsoft.AspNet.OData.Shared/Formatter/Serialization/ODataResourceSetSerializer.cs index e8a9ef1084..1c99960fc1 100644 --- a/src/Microsoft.AspNet.OData.Shared/Formatter/Serialization/ODataResourceSetSerializer.cs +++ b/src/Microsoft.AspNet.OData.Shared/Formatter/Serialization/ODataResourceSetSerializer.cs @@ -481,26 +481,12 @@ private IEnumerable CreateODataOperations(IEnumerable ServerSidePaging\ServerSidePagingDataModel.cs + + ServerSidePaging\ServerSidePagingDataSource.cs + ServerSidePaging\ServerSidePagingTests.cs diff --git a/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/Build.AspNetCore/Microsoft.Test.E2E.AspNetCore.OData.csproj b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/Build.AspNetCore/Microsoft.Test.E2E.AspNetCore.OData.csproj index 315da72c31..7a39cdaa2d 100644 --- a/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/Build.AspNetCore/Microsoft.Test.E2E.AspNetCore.OData.csproj +++ b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/Build.AspNetCore/Microsoft.Test.E2E.AspNetCore.OData.csproj @@ -1561,6 +1561,9 @@ ServerSidePaging\ServerSidePagingDataModel.cs + + ServerSidePaging\ServerSidePagingDataSource.cs + ServerSidePaging\ServerSidePagingTests.cs diff --git a/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/Build.AspNetCore3x/Microsoft.Test.E2E.AspNetCore3x.OData.csproj b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/Build.AspNetCore3x/Microsoft.Test.E2E.AspNetCore3x.OData.csproj index b912f3ceb9..9245b144a1 100644 --- a/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/Build.AspNetCore3x/Microsoft.Test.E2E.AspNetCore3x.OData.csproj +++ b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/Build.AspNetCore3x/Microsoft.Test.E2E.AspNetCore3x.OData.csproj @@ -1559,6 +1559,9 @@ ServerSidePaging\ServerSidePagingDataModel.cs + + ServerSidePaging\ServerSidePagingDataSource.cs + ServerSidePaging\ServerSidePagingTests.cs diff --git a/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/Containment/ContainmentEdmModels.cs b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/Containment/ContainmentEdmModels.cs index da0bc1c00a..5504e516d3 100644 --- a/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/Containment/ContainmentEdmModels.cs +++ b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/Containment/ContainmentEdmModels.cs @@ -5,6 +5,8 @@ // //------------------------------------------------------------------------------ +using System; +using Microsoft.AspNet.OData; using Microsoft.AspNet.OData.Builder; using Microsoft.OData.Edm; using Microsoft.Test.E2E.AspNet.OData.Common.Execution; @@ -42,15 +44,29 @@ public static IEdmModel GetExplicitModel() statementType.Property(s => s.TransactionDescription); statementType.Property(s => s.Amount); + Func, IEdmNavigationProperty, Uri> navigationPropertyLinkFactory = ( + resourceContext, navigationProperty) => resourceContext.GenerateNavigationPropertyLink(navigationProperty, false); + Func, IEdmNavigationProperty, Uri> navigationPropertyLinkFactoryWithCast = ( + resourceContext, navigationProperty) => resourceContext.GenerateNavigationPropertyLink(navigationProperty, true); + var accounts = builder.EntitySet("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("PaginatedAccounts"); paginatedAccounts.HasIdLink(c => c.GenerateSelfLink(true), true); paginatedAccounts.HasEditLink(c => c.GenerateSelfLink(true), true); - - builder.Singleton("AnonymousAccount"); + paginatedAccounts.HasNavigationPropertyLink(payoutPI, navigationPropertyLinkFactory, followsConventions: true); + paginatedAccounts.HasNavigationPropertyLink(payinPIs, navigationPropertyLinkFactory, followsConventions: true); + paginatedAccounts.HasNavigationPropertyLink(giftCard, navigationPropertyLinkFactoryWithCast, true); + + var accountSingleton = builder.Singleton("AnonymousAccount"); + accountSingleton.HasNavigationPropertyLink(payoutPI, navigationPropertyLinkFactory, followsConventions: true); + accountSingleton.HasNavigationPropertyLink(payinPIs, navigationPropertyLinkFactory, followsConventions: true); + accountSingleton.HasNavigationPropertyLink(giftCard, navigationPropertyLinkFactoryWithCast, true); AddBoundActionsAndFunctions(builder); diff --git a/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/ServerSidePaging/ServerSidePagingControllers.cs b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/ServerSidePaging/ServerSidePagingControllers.cs index a54125ef0f..3b9099d776 100644 --- a/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/ServerSidePaging/ServerSidePagingControllers.cs +++ b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/ServerSidePaging/ServerSidePagingControllers.cs @@ -9,6 +9,7 @@ using System.Collections.Generic; using System.Linq; using Microsoft.AspNet.OData; +using Microsoft.AspNet.OData.Routing; using Microsoft.Test.E2E.AspNet.OData.Common.Controllers; namespace Microsoft.Test.E2E.AspNet.OData.ServerSidePaging @@ -66,7 +67,6 @@ public ITestActionResult GetEmployeesHiredInPeriod([FromODataUri] DateTime fromD } } - public class SkipTokenPagingS1CustomersController : TestODataController { private static readonly List customers = new List @@ -154,4 +154,139 @@ public ITestActionResult Get() return Ok(customers); } } + + public class ContainmentPagingCustomersController : TestODataController + { + [EnableQuery(PageSize = 2)] + public ITestActionResult Get() + { + return Ok(ContainmentPagingDataSource.Customers); + } + + [EnableQuery(PageSize = 2)] + public ITestActionResult GetOrders(int key) + { + var customer = ContainmentPagingDataSource.Customers.SingleOrDefault(d => d.Id == key); + + if (customer == null) + { + return BadRequest(); + } + + return Ok(customer.Orders); + } + } + + public class ContainmentPagingCompanyController : TestODataController + { + private static readonly ContainmentPagingCustomer company = new ContainmentPagingCustomer + { + Id = 1, + Orders = ContainmentPagingDataSource.Orders.Take(ContainmentPagingDataSource.TargetSize).ToList() + }; + + [EnableQuery(PageSize = 2)] + public ITestActionResult Get() + { + return Ok(company); + } + + [EnableQuery(PageSize = 2)] + public ITestActionResult GetOrders() + { + return Ok(company.Orders); + } + } + + public class NoContainmentPagingCustomersController : TestODataController + { + [EnableQuery(PageSize = 2)] + public ITestActionResult Get() + { + return Ok(NoContainmentPagingDataSource.Customers); + } + + [EnableQuery(PageSize = 2)] + public ITestActionResult GetOrders(int key) + { + var customer = NoContainmentPagingDataSource.Customers.SingleOrDefault(d => d.Id == key); + + if (customer == null) + { + return BadRequest(); + } + + return Ok(customer.Orders); + } + } + + public class ContainmentPagingMenusController : TestODataController + { + [EnableQuery(PageSize = 2, MaxExpansionDepth = 4)] + public ITestActionResult Get() + { + return Ok(ContainmentPagingDataSource.Menus); + } + + [EnableQuery(PageSize = 2, MaxExpansionDepth = 4)] + public ITestActionResult GetFromContainmentPagingExtendedMenu() + { + return Ok(ContainmentPagingDataSource.Menus.OfType()); + } + + [EnableQuery(PageSize = 2, MaxExpansionDepth = 4)] + public ITestActionResult GetTabsFromContainmentPagingExtendedMenu(int key) + { + var menu = ContainmentPagingDataSource.Menus.OfType().SingleOrDefault(d => d.Id == key); + + if (menu == null) + { + return BadRequest(); + } + + return Ok(menu.Tabs); + } + + [EnableQuery(PageSize = 2, MaxExpansionDepth = 4)] + public ITestActionResult GetPanelsFromContainmentPagingExtendedMenu(int key) + { + var menu = ContainmentPagingDataSource.Menus.OfType().SingleOrDefault(d => d.Id == key); + + if (menu == null) + { + return BadRequest(); + } + + return Ok(menu.Panels); + } + } + + public class ContainmentPagingRibbonController : TestODataController + { + private static readonly ContainmentPagingMenu ribbon = new ContainmentPagingExtendedMenu + { + Id = 1, + Tabs = ContainmentPagingDataSource.Tabs.Take(ContainmentPagingDataSource.TargetSize).ToList() + }; + + [EnableQuery(PageSize = 2, MaxExpansionDepth = 4)] + public ITestActionResult Get() + { + return Ok(ribbon); + } + + [EnableQuery(PageSize = 2, MaxExpansionDepth = 4)] + public ITestActionResult GetFromContainmentPagingExtendedMenu() + { + return Ok(ribbon as ContainmentPagingExtendedMenu); + } + + [EnableQuery(PageSize = 2, MaxExpansionDepth = 4)] + [HttpGet] + [ODataRoute("ContainmentPagingRibbon/Microsoft.Test.E2E.AspNet.OData.ServerSidePaging.ContainmentPagingExtendedMenu/Tabs")] + public ITestActionResult GetTabsFromContainmentPagingExtendedMenu() + { + return Ok((ribbon as ContainmentPagingExtendedMenu).Tabs); + } + } } diff --git a/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/ServerSidePaging/ServerSidePagingDataModel.cs b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/ServerSidePaging/ServerSidePagingDataModel.cs index 78282bfff9..7953effed5 100644 --- a/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/ServerSidePaging/ServerSidePagingDataModel.cs +++ b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/ServerSidePaging/ServerSidePagingDataModel.cs @@ -8,6 +8,7 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; +using Microsoft.AspNet.OData.Builder; namespace Microsoft.Test.E2E.AspNet.OData.ServerSidePaging { @@ -46,4 +47,91 @@ public class SkipTokenPagingEdgeCase1Customer public int Id { get; set; } public decimal? CreditLimit { get; set; } } + + public class ContainmentPagingCustomer + { + public int Id { get; set; } + [Contained] + public List Orders { get; set; } + } + + public class ContainedPagingOrder + { + public int Id { get; set; } + [Contained] + public List Items { get; set; } + } + + public class ContainedPagingOrderItem + { + public int Id { get; set; } + } + + public class NoContainmentPagingCustomer + { + public int Id { get; set; } + public List Orders { get; set; } + } + + public class NoContainmentPagingOrder + { + public int Id { get; set; } + public List Items { get; set; } + } + + public class NoContainmentPagingOrderItem + { + public int Id { get; set; } + } + + public class ContainmentPagingMenu + { + public int Id { get; set; } + } + + public class ContainmentPagingExtendedMenu : ContainmentPagingMenu + { + [Contained] + public List Tabs { get; set; } + // Non-contained + public List Panels { get; set; } + } + + public class ContainedPagingTab + { + public int Id { get; set; } + } + + public class ContainedPagingExtendedTab : ContainedPagingTab + { + [Contained] + public List Items { get; set; } + } + + public class ContainedPagingItem + { + public int Id { get; set; } + } + + public class ContainedPagingExtendedItem : ContainedPagingItem + { + [Contained] + public List Notes { get; set; } + } + + public class ContainedPagingNote + { + public int Id { get; set; } + } + + public class ContainmentPagingPanel + { + public int Id { get; set; } + } + + public class ContainmentPagingExtendedPanel : ContainmentPagingPanel + { + [Contained] + public List Items { get; set; } + } } diff --git a/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/ServerSidePaging/ServerSidePagingDataSource.cs b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/ServerSidePaging/ServerSidePagingDataSource.cs new file mode 100644 index 0000000000..09201dc3e3 --- /dev/null +++ b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/ServerSidePaging/ServerSidePagingDataSource.cs @@ -0,0 +1,109 @@ +//----------------------------------------------------------------------------- +// +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// See License.txt in the project root for license information. +// +//------------------------------------------------------------------------------ + +using System.Collections.Generic; +using System.Linq; + +namespace Microsoft.Test.E2E.AspNet.OData.ServerSidePaging +{ + public static class ContainmentPagingDataSource + { + internal const int TargetSize = 3; + + private static readonly List orderItems = new List( + Enumerable.Range(1, TargetSize * TargetSize * TargetSize).Select(idx => new ContainedPagingOrderItem + { + Id = idx + })); + + private static readonly List orders = new List( + Enumerable.Range(1, TargetSize * TargetSize).Select(idx => new ContainedPagingOrder + { + Id = idx, + Items = orderItems.Skip((idx - 1) * TargetSize).Take(TargetSize).ToList() + })); + + private static readonly List customers = new List( + Enumerable.Range(1, TargetSize).Select(idx => new ContainmentPagingCustomer + { + Id = idx, + Orders = orders.Skip((idx - 1) * TargetSize).Take(TargetSize).ToList() + })); + + private static readonly List notes = new List( + Enumerable.Range(1, TargetSize * TargetSize * TargetSize * TargetSize).Select(idx => new ContainedPagingNote + { + Id = idx + })); + + private static readonly List items = new List( + Enumerable.Range(1, TargetSize * TargetSize * TargetSize).Select(idx => new ContainedPagingExtendedItem + { + Id = idx, + Notes = notes.Skip((idx - 1) * TargetSize).Take(TargetSize).ToList() + })); + + private static readonly List tabs = new List( + Enumerable.Range(1, TargetSize * TargetSize).Select(idx => new ContainedPagingExtendedTab + { + Id = idx, + Items = items.Skip((idx - 1) * TargetSize).Take(TargetSize).ToList() + })); + + private static readonly List panels = new List( + Enumerable.Range(1, TargetSize * TargetSize).Select(idx => new ContainmentPagingExtendedPanel + { + Id = idx, + Items = items.Skip((idx - 1) * TargetSize).Take(TargetSize).ToList() + })); + + private static readonly List menus = new List( + Enumerable.Range(1, TargetSize).Select(idx => new ContainmentPagingExtendedMenu + { + Id = idx, + Tabs = tabs.Skip((idx - 1) * TargetSize).Take(TargetSize).ToList(), + Panels = panels.Skip((idx - 1) * TargetSize).Take(TargetSize).ToList() + })); + + public static List Customers => customers; + + public static List Orders => orders; + + public static List Menus => menus; + + public static List Tabs => tabs; + } + + public static class NoContainmentPagingDataSource + { + private const int TargetSize = 3; + + private static readonly List orderItems = new List( + Enumerable.Range(1, TargetSize * TargetSize * TargetSize).Select(idx => new NoContainmentPagingOrderItem + { + Id = idx + })); + + private static readonly List orders = new List( + Enumerable.Range(1, TargetSize * TargetSize).Select(idx => new NoContainmentPagingOrder + { + Id = idx, + Items = orderItems.Skip((idx - 1) * TargetSize).Take(TargetSize).ToList() + })); + + private static readonly List customers = new List( + Enumerable.Range(1, TargetSize).Select(idx => new NoContainmentPagingCustomer + { + Id = idx, + Orders = orders.Skip((idx - 1) * TargetSize).Take(TargetSize).ToList() + })); + + public static List Customers => customers; + + public static List Orders => orders; + } +} diff --git a/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/ServerSidePaging/ServerSidePagingTests.cs b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/ServerSidePaging/ServerSidePagingTests.cs index 39cc3de686..1ea98a8433 100644 --- a/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/ServerSidePaging/ServerSidePagingTests.cs +++ b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/ServerSidePaging/ServerSidePagingTests.cs @@ -50,6 +50,14 @@ protected static IEdmModel GetEdmModel(WebRouteConfiguration configuration) ODataModelBuilder builder = configuration.CreateConventionModelBuilder(); builder.EntitySet("ServerSidePagingOrders").EntityType.HasRequired(d => d.ServerSidePagingCustomer); builder.EntitySet("ServerSidePagingCustomers").EntityType.HasMany(d => d.ServerSidePagingOrders); + builder.EntitySet("ContainmentPagingCustomers"); + builder.Singleton("ContainmentPagingCompany"); + builder.EntitySet("NoContainmentPagingCustomers"); + builder.EntitySet("NoContainmentPagingOrders"); + builder.EntitySet("NoContainmentPagingOrderItems"); + builder.EntitySet("ContainmentPagingMenus"); + builder.EntitySet("ContainmentPagingPanels"); + builder.Singleton("ContainmentPagingRibbon"); var getEmployeesHiredInPeriodFunction = builder.EntitySet( "ServerSidePagingEmployees").EntityType.Collection.Function("GetEmployeesHiredInPeriod"); @@ -101,6 +109,295 @@ public async Task VerifyParametersInNextPageLinkInEdmFunctionResponseBodyAreInSa "?%40fromDate=2023-01-07T00%3A00%3A00%2B00%3A00&%40toDate=2023-05-07T00%3A00%3A00%2B00%3A00&$skip=3", content); } + + [Fact] + public async Task VerifyExpectedNextLinksGeneratedForNestedExpandInContainmentScenario() + { + // Arrange + var requestUri = this.BaseAddress + "/prefix/ContainmentPagingCustomers?$expand=Orders($expand=Items)"; + var request = new HttpRequestMessage(HttpMethod.Get, requestUri); + + // Act + var response = await this.Client.SendAsync(request); + var content = await response.Content.ReadAsStringAsync(); + + // Assert + Assert.Contains("/prefix/ContainmentPagingCustomers(1)/Orders(1)/Items?$skip=2", content); + Assert.Contains("/prefix/ContainmentPagingCustomers(1)/Orders(2)/Items?$skip=2", content); + Assert.Contains("/prefix/ContainmentPagingCustomers(1)/Orders?$expand=Items&$skip=2", content); + + Assert.Contains("/prefix/ContainmentPagingCustomers(2)/Orders(4)/Items?$skip=2", content); + Assert.Contains("/prefix/ContainmentPagingCustomers(2)/Orders(5)/Items?$skip=2", content); + Assert.Contains("/prefix/ContainmentPagingCustomers(2)/Orders?$expand=Items&$skip=2", content); + + Assert.Contains("/prefix/ContainmentPagingCustomers?$expand=Orders", content); + } + + [Fact] + public async Task VerifyExpectedNextLinksGeneratedForContainedNavigationPropertyAsODataPathSegment() + { + // Arrange + var requestUri = this.BaseAddress + "/prefix/ContainmentPagingCustomers(2)/Orders?$expand=Items"; + var request = new HttpRequestMessage(HttpMethod.Get, requestUri); + + // Act + var response = await this.Client.SendAsync(request); + var content = await response.Content.ReadAsStringAsync(); + + // Assert + Assert.Contains("/prefix/ContainmentPagingCustomers(2)/Orders(4)/Items?$skip=2", content); + Assert.Contains("/prefix/ContainmentPagingCustomers(2)/Orders(5)/Items?$skip=2", content); + Assert.Contains("/prefix/ContainmentPagingCustomers(2)/Orders?$expand=Items&$skip=2", content); + } + + [Fact] + public async Task VerifyExpectedNextLinksGeneratedForContainedNavigationPropertyInSingletonScenario() + { + // Arrange + var requestUri = this.BaseAddress + "/prefix/ContainmentPagingCompany?$expand=Orders($expand=Items)"; + var request = new HttpRequestMessage(HttpMethod.Get, requestUri); + + // Act + var response = await this.Client.SendAsync(request); + var content = await response.Content.ReadAsStringAsync(); + + // Assert + Assert.Contains("/prefix/ContainmentPagingCompany/Orders(1)/Items?$skip=2", content); + Assert.Contains("/prefix/ContainmentPagingCompany/Orders(2)/Items?$skip=2", content); + Assert.Contains("/prefix/ContainmentPagingCompany/Orders?$expand=Items&$skip=2", content); + } + + [Fact] + public async Task VerifyExpectedNextLinksGeneratedForContainedNavigationPropertyAsODataPathSegmentInSingletonScenario() + { + // Arrange + var requestUri = this.BaseAddress + "/prefix/ContainmentPagingCompany/Orders?$expand=Items"; + var request = new HttpRequestMessage(HttpMethod.Get, requestUri); + + // Act + var response = await this.Client.SendAsync(request); + var content = await response.Content.ReadAsStringAsync(); + + // Assert + Assert.Contains("/prefix/ContainmentPagingCompany/Orders(1)/Items?$skip=2", content); + Assert.Contains("/prefix/ContainmentPagingCompany/Orders(2)/Items?$skip=2", content); + } + + [Fact] + public async Task VerifyExpectedNextLinksGeneratedForNestedExpandInNoContainmentScenario() + { + // Arrange + var requestUri = this.BaseAddress + "/prefix/NoContainmentPagingCustomers?$expand=Orders($expand=Items)"; + var request = new HttpRequestMessage(HttpMethod.Get, requestUri); + + // Act + var response = await this.Client.SendAsync(request); + var content = await response.Content.ReadAsStringAsync(); + + // Assert + Assert.Contains("/prefix/NoContainmentPagingOrders(1)/Items?$skip=2", content); + Assert.Contains("/prefix/NoContainmentPagingOrders(2)/Items?$skip=2", content); + Assert.Contains("/prefix/NoContainmentPagingCustomers(1)/Orders?$expand=Items&$skip=2", content); + + Assert.Contains("/prefix/NoContainmentPagingOrders(4)/Items?$skip=2", content); + Assert.Contains("/prefix/NoContainmentPagingOrders(5)/Items?$skip=2", content); + Assert.Contains("/prefix/NoContainmentPagingCustomers(2)/Orders?$expand=Items&$skip=2", content); + + Assert.Contains("/prefix/NoContainmentPagingCustomers?$expand=Orders", content); + } + + [Fact] + public async Task VerifyExpectedNextLinksGeneratedForNonContainedNavigationPropertyAsODataPathSegment() + { + // Arrange + var requestUri = this.BaseAddress + "/prefix/NoContainmentPagingCustomers(2)/Orders?$expand=Items"; + var request = new HttpRequestMessage(HttpMethod.Get, requestUri); + + // Act + var response = await this.Client.SendAsync(request); + var content = await response.Content.ReadAsStringAsync(); + + // Assert + Assert.Contains("/prefix/NoContainmentPagingOrders(4)/Items?$skip=2", content); + Assert.Contains("/prefix/NoContainmentPagingOrders(5)/Items?$skip=2", content); + Assert.Contains("/prefix/NoContainmentPagingCustomers(2)/Orders?$expand=Items&$skip=2", content); + } + + [Fact] + public async Task VerifyExpectedNextLinksGeneratedForContainedNavigationPropertyDeclaredOnDerivedType() + { + // Arrange + var extendedMenuTypeName = typeof(ContainmentPagingExtendedMenu).FullName; + var extendedTabTypeName = typeof(ContainedPagingExtendedTab).FullName; + var extendedItemTypeName = typeof(ContainedPagingExtendedItem).FullName; + var menusResourcePath = $"/prefix/ContainmentPagingMenus"; + var menu1ResourcePath = $"/prefix/ContainmentPagingMenus(1)/{extendedMenuTypeName}"; + var menu2ResourcePath = $"/prefix/ContainmentPagingMenus(2)/{extendedMenuTypeName}"; + + var requestUri = $"{this.BaseAddress}{menusResourcePath}?$expand={extendedMenuTypeName}/Tabs($expand={extendedTabTypeName}/Items($expand={extendedItemTypeName}/Notes))"; + var request = new HttpRequestMessage(HttpMethod.Get, requestUri); + + // Act + var response = await this.Client.SendAsync(request); + var content = await response.Content.ReadAsStringAsync(); + + // Assert + Assert.Contains($"{menu1ResourcePath}/Tabs(1)/{extendedTabTypeName}/Items(1)/{extendedItemTypeName}/Notes?$skip=2", content); + Assert.Contains($"{menu1ResourcePath}/Tabs(1)/{extendedTabTypeName}/Items(2)/{extendedItemTypeName}/Notes?$skip=2", content); + Assert.Contains($"{menu1ResourcePath}/Tabs(1)/{extendedTabTypeName}/Items?$expand={extendedItemTypeName}%2FNotes&$skip=2", content); + Assert.Contains($"{menu1ResourcePath}/Tabs(2)/{extendedTabTypeName}/Items(4)/{extendedItemTypeName}/Notes?$skip=2", content); + Assert.Contains($"{menu1ResourcePath}/Tabs(2)/{extendedTabTypeName}/Items(5)/{extendedItemTypeName}/Notes?$skip=2", content); + Assert.Contains($"{menu1ResourcePath}/Tabs(2)/{extendedTabTypeName}/Items?$expand={extendedItemTypeName}%2FNotes&$skip=2", content); + Assert.Contains($"{menu1ResourcePath}/Tabs?$expand={extendedTabTypeName}%2FItems%28%24expand%3D{extendedItemTypeName}%2FNotes%29&$skip=2", content); + + Assert.Contains($"{menu2ResourcePath}/Tabs(4)/{extendedTabTypeName}/Items(10)/{extendedItemTypeName}/Notes?$skip=2", content); + Assert.Contains($"{menu2ResourcePath}/Tabs(4)/{extendedTabTypeName}/Items(11)/{extendedItemTypeName}/Notes?$skip=2", content); + Assert.Contains($"{menu2ResourcePath}/Tabs(4)/{extendedTabTypeName}/Items?$expand={extendedItemTypeName}%2FNotes&$skip=2", content); + Assert.Contains($"{menu2ResourcePath}/Tabs(5)/{extendedTabTypeName}/Items(13)/{extendedItemTypeName}/Notes?$skip=2", content); + Assert.Contains($"{menu2ResourcePath}/Tabs(5)/{extendedTabTypeName}/Items(14)/{extendedItemTypeName}/Notes?$skip=2", content); + Assert.Contains($"{menu2ResourcePath}/Tabs(5)/{extendedTabTypeName}/Items?$expand={extendedItemTypeName}%2FNotes&$skip=2", content); + Assert.Contains($"{menu2ResourcePath}/Tabs?$expand={extendedTabTypeName}%2FItems%28%24expand%3D{extendedItemTypeName}%2FNotes%29&$skip=2", content); + } + + [Fact] + public async Task VerifyExpectedNextLinksGeneratedForContainedNavigationPropertyDeclaredOnDerivedTypeAsODataPathSegment() + { + // Arrange + var extendedMenuTypeName = typeof(ContainmentPagingExtendedMenu).FullName; + var extendedTabTypeName = typeof(ContainedPagingExtendedTab).FullName; + var extendedItemTypeName = typeof(ContainedPagingExtendedItem).FullName; + var menu1ResourcePath = $"/prefix/ContainmentPagingMenus(1)/{extendedMenuTypeName}"; + + var requestUri = $"{this.BaseAddress}{menu1ResourcePath}/Tabs?$expand={extendedTabTypeName}/Items($expand={extendedItemTypeName}/Notes)"; + var request = new HttpRequestMessage(HttpMethod.Get, requestUri); + + // Act + var response = await this.Client.SendAsync(request); + var content = await response.Content.ReadAsStringAsync(); + + // Assert + Assert.Contains($"{menu1ResourcePath}/Tabs(1)/{extendedTabTypeName}/Items(1)/{extendedItemTypeName}/Notes?$skip=2", content); + Assert.Contains($"{menu1ResourcePath}/Tabs(1)/{extendedTabTypeName}/Items(2)/{extendedItemTypeName}/Notes?$skip=2", content); + Assert.Contains($"{menu1ResourcePath}/Tabs(1)/{extendedTabTypeName}/Items?$expand={extendedItemTypeName}%2FNotes&$skip=2", content); + Assert.Contains($"{menu1ResourcePath}/Tabs(2)/{extendedTabTypeName}/Items(4)/{extendedItemTypeName}/Notes?$skip=2", content); + Assert.Contains($"{menu1ResourcePath}/Tabs(2)/{extendedTabTypeName}/Items(5)/{extendedItemTypeName}/Notes?$skip=2", content); + Assert.Contains($"{menu1ResourcePath}/Tabs(2)/{extendedTabTypeName}/Items?$expand={extendedItemTypeName}%2FNotes&$skip=2", content); + Assert.Contains($"{menu1ResourcePath}/Tabs?$expand={extendedTabTypeName}%2FItems%28%24expand%3D{extendedItemTypeName}%2FNotes%29&$skip=2", content); + } + + [Fact] + public async Task VerifyExpectedNextLinksGeneratedForContainedNavigationPropertyDeclaredOnDerivedTypeInSingletonScenario() + { + // Arrange + var extendedMenuTypeName = typeof(ContainmentPagingExtendedMenu).FullName; + var extendedTabTypeName = typeof(ContainedPagingExtendedTab).FullName; + var extendedItemTypeName = typeof(ContainedPagingExtendedItem).FullName; + var ribbonResourcePath = $"/prefix/ContainmentPagingRibbon"; + + var requestUri = $"{this.BaseAddress}{ribbonResourcePath}?$expand={extendedMenuTypeName}/Tabs($expand={extendedTabTypeName}/Items($expand={extendedItemTypeName}/Notes))"; + var request = new HttpRequestMessage(HttpMethod.Get, requestUri); + + // Act + var response = await this.Client.SendAsync(request); + var content = await response.Content.ReadAsStringAsync(); + + // Assert + Assert.Contains($"{ribbonResourcePath}/{extendedMenuTypeName}/Tabs(1)/{extendedTabTypeName}/Items(1)/{extendedItemTypeName}/Notes?$skip=2", content); + Assert.Contains($"{ribbonResourcePath}/{extendedMenuTypeName}/Tabs(1)/{extendedTabTypeName}/Items(2)/{extendedItemTypeName}/Notes?$skip=2", content); + Assert.Contains($"{ribbonResourcePath}/{extendedMenuTypeName}/Tabs(1)/{extendedTabTypeName}/Items?$expand={extendedItemTypeName}%2FNotes&$skip=2", content); + Assert.Contains($"{ribbonResourcePath}/{extendedMenuTypeName}/Tabs(2)/{extendedTabTypeName}/Items(4)/{extendedItemTypeName}/Notes?$skip=2", content); + Assert.Contains($"{ribbonResourcePath}/{extendedMenuTypeName}/Tabs(2)/{extendedTabTypeName}/Items(5)/{extendedItemTypeName}/Notes?$skip=2", content); + Assert.Contains($"{ribbonResourcePath}/{extendedMenuTypeName}/Tabs(2)/{extendedTabTypeName}/Items?$expand={extendedItemTypeName}%2FNotes&$skip=2", content); + Assert.Contains($"{ribbonResourcePath}/{extendedMenuTypeName}/Tabs?$expand={extendedTabTypeName}%2FItems%28%24expand%3D{extendedItemTypeName}%2FNotes%29&$skip=2", content); + } + + [Fact] + public async Task VerifyExpectedNextLinksGeneratedForContainedNavigationPropertyDeclaredOnDerivedTypeAsODataPathSegmentInSingletonScenario() + { + // Arrange + var extendedMenuTypeName = typeof(ContainmentPagingExtendedMenu).FullName; + var extendedTabTypeName = typeof(ContainedPagingExtendedTab).FullName; + var extendedItemTypeName = typeof(ContainedPagingExtendedItem).FullName; + var ribbonResourcePath = $"/prefix/ContainmentPagingRibbon/{extendedMenuTypeName}"; + + var requestUri = $"{this.BaseAddress}{ribbonResourcePath}/Tabs?$expand={extendedTabTypeName}/Items($expand={extendedItemTypeName}/Notes)"; + var request = new HttpRequestMessage(HttpMethod.Get, requestUri); + + // Act + var response = await this.Client.SendAsync(request); + var content = await response.Content.ReadAsStringAsync(); + + // Assert + Assert.Contains($"{ribbonResourcePath}/Tabs(1)/{extendedTabTypeName}/Items(1)/{extendedItemTypeName}/Notes?$skip=2", content); + Assert.Contains($"{ribbonResourcePath}/Tabs(1)/{extendedTabTypeName}/Items(2)/{extendedItemTypeName}/Notes?$skip=2", content); + Assert.Contains($"{ribbonResourcePath}/Tabs(1)/{extendedTabTypeName}/Items?$expand={extendedItemTypeName}%2FNotes&$skip=2", content); + Assert.Contains($"{ribbonResourcePath}/Tabs(2)/{extendedTabTypeName}/Items(4)/{extendedItemTypeName}/Notes?$skip=2", content); + Assert.Contains($"{ribbonResourcePath}/Tabs(2)/{extendedTabTypeName}/Items(5)/{extendedItemTypeName}/Notes?$skip=2", content); + Assert.Contains($"{ribbonResourcePath}/Tabs(2)/{extendedTabTypeName}/Items?$expand={extendedItemTypeName}%2FNotes&$skip=2", content); + Assert.Contains($"{ribbonResourcePath}/Tabs?$expand={extendedTabTypeName}%2FItems%28%24expand%3D{extendedItemTypeName}%2FNotes%29&$skip=2", content); + } + + [Fact] + public async Task VerifyExpectedNextLinksGeneratedForNonContainedNavigationPropertyDeclaredOnDerivedType() + { + // Arrange + var extendedMenuTypeName = typeof(ContainmentPagingExtendedMenu).FullName; + var extendedPanelTypeName = typeof(ContainmentPagingExtendedPanel).FullName; + var extendedItemTypeName = typeof(ContainedPagingExtendedItem).FullName; + var menusResourcePath = $"/prefix/ContainmentPagingMenus"; + var menu1ResourcePath = $"/prefix/ContainmentPagingMenus(1)/{extendedMenuTypeName}"; + var menu2ResourcePath = $"/prefix/ContainmentPagingMenus(2)/{extendedMenuTypeName}"; + + var requestUri = $"{this.BaseAddress}{menusResourcePath}?$expand={extendedMenuTypeName}/Panels($expand={extendedPanelTypeName}/Items($expand={extendedItemTypeName}/Notes))"; + var request = new HttpRequestMessage(HttpMethod.Get, requestUri); + + // Act + var response = await this.Client.SendAsync(request); + var content = await response.Content.ReadAsStringAsync(); + + // Assert + Assert.Contains($"/prefix/ContainmentPagingPanels(1)/{extendedPanelTypeName}/Items(1)/{extendedItemTypeName}/Notes?$skip=2", content); + Assert.Contains($"/prefix/ContainmentPagingPanels(1)/{extendedPanelTypeName}/Items(2)/{extendedItemTypeName}/Notes?$skip=2", content); + Assert.Contains($"/prefix/ContainmentPagingPanels(1)/{extendedPanelTypeName}/Items?$expand={extendedItemTypeName}%2FNotes&$skip=2", content); + Assert.Contains($"/prefix/ContainmentPagingPanels(2)/{extendedPanelTypeName}/Items(4)/{extendedItemTypeName}/Notes?$skip=2", content); + Assert.Contains($"/prefix/ContainmentPagingPanels(2)/{extendedPanelTypeName}/Items(5)/{extendedItemTypeName}/Notes?$skip=2", content); + Assert.Contains($"/prefix/ContainmentPagingPanels(2)/{extendedPanelTypeName}/Items?$expand={extendedItemTypeName}%2FNotes&$skip=2", content); + Assert.Contains($"{menu1ResourcePath}/Panels?$expand={extendedPanelTypeName}%2FItems%28%24expand%3D{extendedItemTypeName}%2FNotes%29&$skip=2", content); + + Assert.Contains($"/prefix/ContainmentPagingPanels(4)/{extendedPanelTypeName}/Items(10)/{extendedItemTypeName}/Notes?$skip=2", content); + Assert.Contains($"/prefix/ContainmentPagingPanels(4)/{extendedPanelTypeName}/Items(11)/{extendedItemTypeName}/Notes?$skip=2", content); + Assert.Contains($"/prefix/ContainmentPagingPanels(4)/{extendedPanelTypeName}/Items?$expand={extendedItemTypeName}%2FNotes&$skip=2", content); + Assert.Contains($"/prefix/ContainmentPagingPanels(5)/{extendedPanelTypeName}/Items(13)/{extendedItemTypeName}/Notes?$skip=2", content); + Assert.Contains($"/prefix/ContainmentPagingPanels(5)/{extendedPanelTypeName}/Items(14)/{extendedItemTypeName}/Notes?$skip=2", content); + Assert.Contains($"/prefix/ContainmentPagingPanels(5)/{extendedPanelTypeName}/Items?$expand={extendedItemTypeName}%2FNotes&$skip=2", content); + Assert.Contains($"{menu2ResourcePath}/Panels?$expand={extendedPanelTypeName}%2FItems%28%24expand%3D{extendedItemTypeName}%2FNotes%29&$skip=2", content); + } + + [Fact] + public async Task VerifyExpectedNextLinksGeneratedForNonContainedNavigationPropertyDeclaredOnDerivedTypeAsODataPathSegment() + { + // Arrange + var extendedMenuTypeName = typeof(ContainmentPagingExtendedMenu).FullName; + var extendedPanelTypeName = typeof(ContainmentPagingExtendedPanel).FullName; + var extendedItemTypeName = typeof(ContainedPagingExtendedItem).FullName; + var menu1ResourcePath = $"/prefix/ContainmentPagingMenus(1)/{extendedMenuTypeName}"; + + var requestUri = $"{this.BaseAddress}{menu1ResourcePath}/Panels?$expand={extendedPanelTypeName}/Items($expand={extendedItemTypeName}/Notes)"; + var request = new HttpRequestMessage(HttpMethod.Get, requestUri); + + // Act + var response = await this.Client.SendAsync(request); + var content = await response.Content.ReadAsStringAsync(); + + // Assert + Assert.Contains($"/prefix/ContainmentPagingPanels(1)/{extendedPanelTypeName}/Items(1)/{extendedItemTypeName}/Notes?$skip=2", content); + Assert.Contains($"/prefix/ContainmentPagingPanels(1)/{extendedPanelTypeName}/Items(2)/{extendedItemTypeName}/Notes?$skip=2", content); + Assert.Contains($"/prefix/ContainmentPagingPanels(1)/{extendedPanelTypeName}/Items?$expand={extendedItemTypeName}%2FNotes&$skip=2", content); + Assert.Contains($"/prefix/ContainmentPagingPanels(2)/{extendedPanelTypeName}/Items(4)/{extendedItemTypeName}/Notes?$skip=2", content); + Assert.Contains($"/prefix/ContainmentPagingPanels(2)/{extendedPanelTypeName}/Items(5)/{extendedItemTypeName}/Notes?$skip=2", content); + Assert.Contains($"/prefix/ContainmentPagingPanels(2)/{extendedPanelTypeName}/Items?$expand={extendedItemTypeName}%2FNotes&$skip=2", content); + Assert.Contains($"{menu1ResourcePath}/Panels?$expand={extendedPanelTypeName}%2FItems%28%24expand%3D{extendedItemTypeName}%2FNotes%29&$skip=2", content); + } } public class SkipTokenPagingTests : WebHostTestBase