Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Checking ITruncatedCollecton.IsTruncated after enumeration. Fixes #1384 #1393

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -560,9 +560,9 @@ internal static Func<object, Uri> GetNextLinkGenerator(ODataResourceSetBase reso
{
// nested resourceSet
ITruncatedCollection truncatedCollection = resourceSetInstance as ITruncatedCollection;
if (truncatedCollection != null && truncatedCollection.IsTruncated)
if (truncatedCollection != null)
{
return (obj) => { return GetNestedNextPageLink(writeContext, truncatedCollection.PageSize, obj); };
return (obj) => { return GetNestedNextPageLink(writeContext, truncatedCollection, obj); };
}
}

Expand Down Expand Up @@ -599,9 +599,9 @@ internal static Func<object, Uri> GetNextLinkGenerator(ODataResourceSetBase reso
{
// nested resourceSet
ITruncatedCollection truncatedCollection = resourceSetInstance as ITruncatedCollection;
if (truncatedCollection != null && truncatedCollection.IsTruncated)
if (truncatedCollection != null)
{
return (obj) => { return GetNestedNextPageLink(writeContext, truncatedCollection.PageSize, obj); };
return (obj) => { return GetNestedNextPageLink(writeContext, truncatedCollection, obj); };
}
}

Expand Down Expand Up @@ -695,32 +695,35 @@ private IEnumerable<ODataOperation> CreateODataOperations(IEnumerable<IEdmOperat
}
}

private static Uri GetNestedNextPageLink(ODataSerializerContext writeContext, int pageSize, object obj)
private static Uri GetNestedNextPageLink(ODataSerializerContext writeContext, ITruncatedCollection truncatedCollection, object obj)
{
Contract.Assert(writeContext.ExpandedResource != null);
IEdmNavigationSource sourceNavigationSource = writeContext.ExpandedResource.NavigationSource;
NavigationSourceLinkBuilderAnnotation linkBuilder = writeContext.Model.GetNavigationSourceLinkBuilder(sourceNavigationSource);
if (truncatedCollection.IsTruncated)
{
Contract.Assert(writeContext.ExpandedResource != null);
IEdmNavigationSource sourceNavigationSource = writeContext.ExpandedResource.NavigationSource;
NavigationSourceLinkBuilderAnnotation linkBuilder = writeContext.Model.GetNavigationSourceLinkBuilder(sourceNavigationSource);

Uri navigationLink = linkBuilder.BuildNavigationLink(
writeContext.ExpandedResource,
writeContext.NavigationProperty);
Uri navigationLink = linkBuilder.BuildNavigationLink(
writeContext.ExpandedResource,
writeContext.NavigationProperty);

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

SkipTokenHandler nextLinkGenerator = null;
if (writeContext.QueryContext != null)
{
nextLinkGenerator = writeContext.QueryContext.GetSkipTokenHandler();
}

if (nestedNextLink != null)
{
if (nextLinkGenerator != null)
SkipTokenHandler nextLinkGenerator = null;
if (writeContext.QueryContext != null)
{
return nextLinkGenerator.GenerateNextPageLink(nestedNextLink, pageSize, obj, writeContext);
nextLinkGenerator = writeContext.QueryContext.GetSkipTokenHandler();
}

return GetNextPageHelper.GetNextPageLink(nestedNextLink, pageSize);
if (nestedNextLink != null)
{
if (nextLinkGenerator != null)
{
return nextLinkGenerator.GenerateNextPageLink(nestedNextLink, truncatedCollection.PageSize, obj, writeContext);
}

return GetNextPageHelper.GetNextPageLink(nestedNextLink, truncatedCollection.PageSize);
}
}

return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,11 @@
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.OData.Extensions;
using Microsoft.AspNetCore.OData.Query;
using Microsoft.AspNetCore.OData.Routing.Controllers;
using Microsoft.OData.Edm;
using Microsoft.OData.UriParser;

namespace Microsoft.AspNetCore.OData.E2E.Tests.ServerSidePaging;

Expand Down Expand Up @@ -220,6 +223,62 @@ public ActionResult GetOrders(int key)
return Ok(customer.Orders);
}
}
public class UntypedPagingCustomerOrdersController : ODataController
{
public ActionResult Get()
{
ODataQuerySettings querySettings = new ODataQuerySettings();
IEdmModel model = HttpContext.ODataFeature().Model;
ODataQueryOptions queryOptions = CreateQueryOptions(model);
SetSelectExpandClauseOnODataFeature(model.EntityContainer.FindEntitySet("UntypedPagingCustomerOrders").EntityType, new Dictionary<string, string> { { "$expand", "Orders" } });

return Ok(queryOptions.ApplyTo(NoContainmentPagingDataSource.UntypedCustomerOrders.AsQueryable(), querySettings, AllowedQueryOptions.All & ~AllowedQueryOptions.SkipToken));
}

private ODataQueryOptions CreateQueryOptions(IEdmModel model)
{
ODataQueryContext context = CreateQueryContext(model, Request.ODataFeature().Path);
ODataQueryOptions queryOptions = new ODataQueryOptions(context, Request);
return queryOptions;
}

private static ODataQueryContext CreateQueryContext(IEdmModel model, ODataPath path)
{
IEdmStructuredType edmStructuredType = null;
foreach (var segment in path)
{
if (segment.EdmType is IEdmCollectionType collectionType)
edmStructuredType = collectionType.ElementType.AsEntity().EntityDefinition();
if (segment.EdmType is IEdmEntityType entityType)
edmStructuredType = entityType;
if (segment.EdmType is IEdmComplexType complexType)
edmStructuredType = complexType;
}
if (edmStructuredType == null)
{
throw new ArgumentException("No structured type in path");
}

return new ODataQueryContext(model, edmStructuredType, path);
}

private void SetSelectExpandClauseOnODataFeature(IEdmType edmEntityType, IDictionary<string, string> options = null)
{
if (!Request.IsCountRequest() && Request.ODataFeature().SelectExpandClause == null)
{
SelectExpandClause selectExpand;

ODataPath odataPath = Request.ODataFeature().Path;
var segment = odataPath.FirstSegment as EntitySetSegment;
IEdmNavigationSource source = segment?.EntitySet;
ODataQueryOptionParser parser = new ODataQueryOptionParser(Request.GetModel(), edmEntityType, source, options, Request.ODataFeature().Services);
selectExpand = parser.ParseSelectAndExpand();

//Set the SelectExpand Clause on the ODataFeature otherwise OData formatter won't show the expand and select properties in the response.
Request.ODataFeature().SelectExpandClause = selectExpand;
}
}
}

public class ContainmentPagingMenusController : ODataController
{
Expand Down Expand Up @@ -300,9 +359,9 @@ public class CollectionPagingCustomersController : ODataController
Tags = new List<string> { "Tier 1", "Gen-Z", "HNW" },
Categories = new List<CollectionPagingCategory>
{
CollectionPagingCategory.Retailer,
CollectionPagingCategory.Wholesaler,
CollectionPagingCategory.Distributor
CollectionPagingCategory.Retailer,
CollectionPagingCategory.Wholesaler,
CollectionPagingCategory.Distributor
},
Locations = new List<CollectionPagingLocation>(
Enumerable.Range(1, TargetSize).Select(dx => new CollectionPagingLocation
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@
//------------------------------------------------------------------------------

using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.OData.Query.Container;
using Microsoft.OData.ModelBuilder;

namespace Microsoft.AspNetCore.OData.E2E.Tests.ServerSidePaging;
Expand Down Expand Up @@ -73,6 +75,56 @@ public class NoContainmentPagingCustomer
public List<NoContainmentPagingOrder> Orders { get; set; }
}

public class UntypedPagingCustomerOrder
{
public int Id { get; set; }

public IEnumerable<NoContainmentPagingOrder> Orders { get; } = new TruncatedEnumerable(2);
private class TruncatedEnumerable(int pageSize) : IEnumerable<NoContainmentPagingOrder>, ITruncatedCollection
{
public int PageSize => pageSize;

public bool IsTruncated => enumerator.Position > pageSize;

Enumerator enumerator = new(pageSize);
public IEnumerator GetEnumerator()
{
return enumerator;
}

IEnumerator<NoContainmentPagingOrder> IEnumerable<NoContainmentPagingOrder>.GetEnumerator()
{
return enumerator;
}

private class Enumerator(int pageSize) : IEnumerator<NoContainmentPagingOrder>
{
public int Position { get; set; } = 0;

public NoContainmentPagingOrder Current => new();

object IEnumerator.Current => Current;

public void Dispose()
{
}

public bool MoveNext()
{
// This enumerator simply advances the cursor position and returns false if we have passed the pagesize to stop enumeration
Position++;
return Position <= pageSize;
}

public void Reset()
{
Position = 0;
}
}
}

}

public class NoContainmentPagingOrder
{
public int Id { get; set; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,14 @@ public static class NoContainmentPagingDataSource
Orders = orders.Skip((idx - 1) * TargetSize).Take(TargetSize).ToList()
}));

private static readonly List<UntypedPagingCustomerOrder> untypedCustomerOrders = new List<UntypedPagingCustomerOrder>(
Enumerable.Range(1, TargetSize).Select(idx => new UntypedPagingCustomerOrder
{
Id = idx
}));

public static List<NoContainmentPagingCustomer> Customers => customers;
public static List<UntypedPagingCustomerOrder> UntypedCustomerOrders => untypedCustomerOrders;

public static List<NoContainmentPagingOrder> Orders => orders;
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ protected static void UpdateConfigureServices(IServiceCollection services)
typeof(ContainmentPagingCustomersController),
typeof(ContainmentPagingCompanyController),
typeof(NoContainmentPagingCustomersController),
typeof(UntypedPagingCustomerOrdersController),
typeof(ContainmentPagingMenusController),
typeof(ContainmentPagingRibbonController),
typeof(CollectionPagingCustomersController));
Expand All @@ -56,6 +57,7 @@ protected static IEdmModel GetEdmModel()
builder.EntitySet<ContainmentPagingCustomer>("ContainmentPagingCustomers");
builder.Singleton<ContainmentPagingCustomer>("ContainmentPagingCompany");
builder.EntitySet<NoContainmentPagingCustomer>("NoContainmentPagingCustomers");
builder.EntitySet<UntypedPagingCustomerOrder>("UntypedPagingCustomerOrders");
builder.EntitySet<NoContainmentPagingOrder>("NoContainmentPagingOrders");
builder.EntitySet<NoContainmentPagingOrderItem>("NoContainmentPagingOrderItems");
builder.EntitySet<ContainmentPagingMenu>("ContainmentPagingMenus");
Expand Down Expand Up @@ -241,6 +243,24 @@ public async Task VerifyExpectedNextLinksGeneratedForNestedExpandInNoContainment
Assert.Contains("/prefix/NoContainmentPagingCustomers?$expand=Orders", content);
}

[Fact]
public async Task VerifyExpectedNextLinksGeneratedForNestedExpandUntypedOrders()
{
// Arrange
var requestUri = "/prefix/UntypedPagingCustomerOrders?$expand=Orders";
var request = new HttpRequestMessage(HttpMethod.Get, requestUri);
var client = CreateClient();

// Act
var response = await client.SendAsync(request);
var content = await response.Content.ReadAsStringAsync();

// Assert
Assert.Contains("/prefix/UntypedPagingCustomerOrders/1/Orders?$skip=2", content);
Assert.Contains("/prefix/UntypedPagingCustomerOrders/2/Orders?$skip=2", content);
Assert.Contains("/prefix/UntypedPagingCustomerOrders/3/Orders?$skip=2", content);
}

[Fact]
public async Task VerifyExpectedNextLinksGeneratedForNonContainedNavigationPropertyAsODataPathSegment()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,13 @@
using Microsoft.AspNetCore.OData.Formatter;
using Microsoft.AspNetCore.OData.Formatter.Serialization;
using Microsoft.AspNetCore.OData.Formatter.Value;
using Microsoft.AspNetCore.OData.Query.Container;
using Microsoft.AspNetCore.OData.Results;
using Microsoft.AspNetCore.OData.Tests.Commons;
using Microsoft.AspNetCore.OData.Tests.Edm;
using Microsoft.AspNetCore.OData.Tests.Extensions;
using Microsoft.AspNetCore.OData.Tests.Formatter.Models;
using Microsoft.AspNetCore.OData.Tests.Query.Container;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.OData;
using Microsoft.OData.Edm;
Expand Down Expand Up @@ -611,6 +613,87 @@ public async Task WriteObjectInlineAsync_Sets_NextPageLink_OnWriteEndAsync()
mockWriter.Verify();
}

private class TruncatedEnumerable(int pageSize) : IEnumerable, ITruncatedCollection
{
public int PageSize => pageSize;

public bool IsTruncated => enumerator.Position > pageSize;

Enumerator enumerator = new(pageSize);
public IEnumerator GetEnumerator()
{
return enumerator;
}

private class Enumerator(int pageSize) : IEnumerator
{
public int Position { get; set; } = 0;

public object Current => new();

public bool MoveNext()
{
// This enumerator simply advances the cursor position and returns false if we have passed the pagesize to stop enumeration
Position++;
return Position <= pageSize;
}

public void Reset()
{
throw new NotImplementedException();
}
}
}

[Fact]
public async Task WriteObjectInlineAsync_Sets_NextPageLink_OnWriteEndAsync_ForInnerResourceSets()
{
// Arange
IEnumerable instance = new TruncatedEnumerable(2);

var request = RequestFactory.Create("GET", "http://localhost/Customers?$expend=Orders", opt => opt.AddRouteComponents(_model));

IEdmNavigationProperty navProp = _customerSet.EntityType.NavigationProperties().First();
SelectExpandClause selectExpandClause = new SelectExpandClause(new SelectItem[0], allSelected: true);
ResourceContext entity = new ResourceContext
{
SerializerContext =
new ODataSerializerContext { Request = request, NavigationSource = _customerSet, Model = _model }
};
ODataSerializerContext nestedContext = new ODataSerializerContext(entity, selectExpandClause, navProp);
nestedContext.ExpandedResource.StructuredType = _customerSet.EntityType;
object idValue = 1;
var edmCustomer = new Mock<EdmEntityObject>(_customerSet.EntityType);
edmCustomer.Setup(e => e.TryGetPropertyValue("ID", out idValue)).Returns(true);
nestedContext.ExpandedResource.EdmObject = edmCustomer.Object;

ODataResourceSet resourceSet = new ODataResourceSet();
Mock<IODataSerializerProvider> serializerProvider = new Mock<IODataSerializerProvider>();
Mock<ODataResourceSerializer> resourceSerializer = new Mock<ODataResourceSerializer>(serializerProvider.Object);
serializerProvider.Setup(s => s.GetEdmTypeSerializer(It.IsAny<IEdmTypeReference>())).Returns(resourceSerializer.Object);
Mock<ODataResourceSetSerializer> serializer = new Mock<ODataResourceSetSerializer>(serializerProvider.Object);
serializer.CallBase = true;
serializer.Setup(s => s.CreateResourceSet(instance, _customersType, nestedContext)).Returns(resourceSet);
var mockWriter = new Mock<ODataWriter>();

mockWriter.Setup(m => m.WriteStartAsync(It.Is<ODataResourceSet>(f => f.NextPageLink == null))).Verifiable();
mockWriter
.Setup(m => m.WriteEndAsync())
.Callback(() =>
{
//Assert
Assert.Equal("http://localhost/Customers/1/Orders?$skip=2", resourceSet.NextPageLink?.AbsoluteUri);
})
.Returns(Task.CompletedTask)
.Verifiable();

// Act
await serializer.Object.WriteObjectInlineAsync(instance, _customersType, mockWriter.Object, nestedContext);

// Assert
mockWriter.Verify();
}

[Fact]
public void CreateResource_Sets_CountValueForPageResult()
{
Expand Down
Loading