Skip to content
Merged
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
102 changes: 87 additions & 15 deletions src/Microsoft.AspNetCore.OData/Query/Query/DefaultSkipTokenHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@
using Microsoft.AspNetCore.OData.Query.Wrapper;
using Microsoft.OData;
using Microsoft.OData.Edm;
using Microsoft.OData.ModelBuilder.Config;
using Microsoft.OData.UriParser;

namespace Microsoft.AspNetCore.OData.Query
Expand Down Expand Up @@ -267,14 +266,13 @@ private static IQueryable ApplyToCore(IQueryable query, ODataQuerySettings query
directionMap = new Dictionary<string, OrderByDirection>();
}

IDictionary<string, object> propertyValuePairs = PopulatePropertyValuePairs(skipTokenRawValue, context);
IDictionary<string, Tuple<object, Type>> propertyValuePairs = PopulatePropertyValuePairs(skipTokenRawValue, context);

if (propertyValuePairs.Count == 0)
{
throw Error.InvalidOperation("Unable to get property values from the skiptoken value.");
}

// ExpressionBinderBase binder = context.GetFilterBinder(querySettings);
bool parameterizeConstant = querySettings.EnableConstantParameterization;
ParameterExpression param = Expression.Parameter(context.ElementClrType);
Expression where = null;
Expand All @@ -288,40 +286,112 @@ private static IQueryable ApplyToCore(IQueryable query, ODataQuerySettings query
Expression lastEquality = null;
bool firstProperty = true;

foreach (KeyValuePair<string, object> item in propertyValuePairs)
foreach (KeyValuePair<string, Tuple<object, Type>> item in propertyValuePairs)
{
string key = item.Key;
MemberExpression property = Expression.Property(param, key);
object value = item.Value;

object value = item.Value.Item1;

Type propertyType = item.Value.Item2 ?? value.GetType();
bool propertyIsNullable = propertyType.IsNullable();

Expression compare = null;
ODataEnumValue enumValue = value as ODataEnumValue;
if (enumValue != null)
if (value is ODataEnumValue enumValue)
{
value = enumValue.Value;
propertyType = value.GetType();
}
else if (value is ODataNullValue)
{
value = null;
}

Expression constant = parameterizeConstant ? LinqParameterContainer.Parameterize(value.GetType(), value) : Expression.Constant(value);
Expression constant = parameterizeConstant ? LinqParameterContainer.Parameterize(propertyType, value) : Expression.Constant(value);

if (directionMap.ContainsKey(key) && directionMap[key] == OrderByDirection.Descending)
{
compare = ExpressionBinderHelper.CreateBinaryExpression(BinaryOperatorKind.LessThan, property, constant, true, querySettings);
// Prop < Value
compare = ExpressionBinderHelper.CreateBinaryExpression(
binaryOperator: BinaryOperatorKind.LessThan,
left: property,
right: constant,
liftToNull: propertyIsNullable ? false : true,
querySettings: querySettings);

if (propertyIsNullable && value != null)
{
// Prop == null

// We only do this when value is NOT null since
// ((Prop1 < null) OR (Prop1 == null)) OR ((Prop1 == null) AND (Prop2 > Value2))
// doesn't make logical sense
Expression condition = ExpressionBinderHelper.CreateBinaryExpression(
binaryOperator: BinaryOperatorKind.Equal,
left: property,
right: parameterizeConstant ? LinqParameterContainer.Parameterize(propertyType, null) : Expression.Constant(null),
liftToNull: false,
querySettings: querySettings);

// (Prop < Value) OR (Prop == null)
compare = Expression.OrElse(compare, condition);
}
}
else
{
compare = ExpressionBinderHelper.CreateBinaryExpression(BinaryOperatorKind.GreaterThan, property, constant, true, querySettings);
if (value == null)
{
// Prop != null

// We are aiming for the following expression
// when value is null in the ascending order scenario:
// (Prop1 != null) OR ((Prop1 == null) AND (Prop2 > Value2)) ...
compare = ExpressionBinderHelper.CreateBinaryExpression(
binaryOperator: BinaryOperatorKind.NotEqual,
left: property,
right: constant,
liftToNull: false,
querySettings: querySettings);
}
else
{
// Prop > Value

// We are aiming for the following expression
// when value is NOT null in the ascending order scenario:
// (Prop1 > Value1) OR ((Prop1 == Value1) AND (Prop2 > Value2)) ...
compare = ExpressionBinderHelper.CreateBinaryExpression(
binaryOperator: BinaryOperatorKind.GreaterThan,
left: property,
right: constant,
liftToNull: propertyIsNullable ? false : true,
querySettings: querySettings);
}
}

if (firstProperty)
{
lastEquality = ExpressionBinderHelper.CreateBinaryExpression(BinaryOperatorKind.Equal, property, constant, true, querySettings);
lastEquality = ExpressionBinderHelper.CreateBinaryExpression(
binaryOperator: BinaryOperatorKind.Equal,
left: property,
right: constant,
liftToNull: propertyIsNullable ? false : true,
querySettings: querySettings);
where = compare;
firstProperty = false;
}
else
{
Expression condition = Expression.AndAlso(lastEquality, compare);
where = Expression.OrElse(where, condition);
lastEquality = Expression.AndAlso(lastEquality, ExpressionBinderHelper.CreateBinaryExpression(BinaryOperatorKind.Equal, property, constant, true, querySettings));
lastEquality = Expression.AndAlso(
lastEquality,
ExpressionBinderHelper.CreateBinaryExpression(
binaryOperator: BinaryOperatorKind.Equal,
left: property,
right: constant,
liftToNull: propertyIsNullable ? false : true,
querySettings: querySettings));
}
}

Expand All @@ -335,11 +405,11 @@ private static IQueryable ApplyToCore(IQueryable query, ODataQuerySettings query
/// <param name="value">The skiptoken string value.</param>
/// <param name="context">The <see cref="ODataQueryContext"/> which contains the <see cref="IEdmModel"/> and some type information</param>
/// <returns>Dictionary with property name and property value in the skiptoken value.</returns>
internal static IDictionary<string, object> PopulatePropertyValuePairs(string value, ODataQueryContext context)
internal static IDictionary<string, Tuple<object, Type>> PopulatePropertyValuePairs(string value, ODataQueryContext context)
{
Contract.Assert(context != null);

IDictionary<string, object> propertyValuePairs = new Dictionary<string, object>();
IDictionary<string, Tuple<object, Type>> propertyValuePairs = new Dictionary<string, Tuple<object, Type>>();
IList<string> keyValuesPairs = ParseValue(value, CommaDelimiter);

IEdmStructuredType type = context.ElementType as IEdmStructuredType;
Expand All @@ -354,13 +424,15 @@ internal static IDictionary<string, object> PopulatePropertyValuePairs(string va

IEdmTypeReference propertyType = null;
IEdmProperty property = type.FindProperty(pieces[0]);
Type propertyClrType = null;
if (property != null)
{
propertyType = property.Type;
propertyClrType = context.Model.GetClrType(propertyType);
}

propValue = ODataUriUtils.ConvertFromUriLiteral(pieces[1], ODataVersion.V401, context.Model, propertyType);
propertyValuePairs.Add(pieces[0], propValue);
propertyValuePairs.Add(pieces[0], Tuple.Create(propValue, propertyClrType));
}
else
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,4 +67,52 @@ public IActionResult GetEmployeesHiredInPeriod([FromRoute] DateTime fromDate, [F
return Ok(hiredInPeriod);
}
}

public class SkipTokenPagingS1CustomersController : ODataController
{
private static readonly List<SkipTokenPagingCustomer> customers = new List<SkipTokenPagingCustomer>
{
new SkipTokenPagingCustomer { Id = 1, CreditLimit = null },
new SkipTokenPagingCustomer { Id = 2, CreditLimit = 2 },
new SkipTokenPagingCustomer { Id = 3, CreditLimit = null },
new SkipTokenPagingCustomer { Id = 4, CreditLimit = 30 },
new SkipTokenPagingCustomer { Id = 5, CreditLimit = null },
new SkipTokenPagingCustomer { Id = 6, CreditLimit = 35 },
new SkipTokenPagingCustomer { Id = 7, CreditLimit = 5 },
new SkipTokenPagingCustomer { Id = 8, CreditLimit = 50 },
new SkipTokenPagingCustomer { Id = 9, CreditLimit = 25 },
};

[EnableQuery(PageSize = 2)]
public ActionResult<IEnumerable<SkipTokenPagingCustomer>> Get()
{
return customers;
}
}

public class SkipTokenPagingS2CustomersController : ODataController
{
private readonly List<SkipTokenPagingCustomer> customers = new List<SkipTokenPagingCustomer>
{
new SkipTokenPagingCustomer { Id = 1, Grade = "A", CreditLimit = null },
new SkipTokenPagingCustomer { Id = 2, Grade = "B", CreditLimit = null },
new SkipTokenPagingCustomer { Id = 3, Grade = "A", CreditLimit = 10 },
new SkipTokenPagingCustomer { Id = 4, Grade = "C", CreditLimit = null },
new SkipTokenPagingCustomer { Id = 5, Grade = "A", CreditLimit = 30 },
new SkipTokenPagingCustomer { Id = 6, Grade = "C", CreditLimit = null },
new SkipTokenPagingCustomer { Id = 7, Grade = "B", CreditLimit = 5 },
new SkipTokenPagingCustomer { Id = 8, Grade = "C", CreditLimit = 25 },
new SkipTokenPagingCustomer { Id = 9, Grade = "B", CreditLimit = 50 },
new SkipTokenPagingCustomer { Id = 10, Grade = "D", CreditLimit = 50 },
new SkipTokenPagingCustomer { Id = 11, Grade = "F", CreditLimit = 35 },
new SkipTokenPagingCustomer { Id = 12, Grade = "F", CreditLimit = 30 },
new SkipTokenPagingCustomer { Id = 13, Grade = "F", CreditLimit = 55 }
};

[EnableQuery(PageSize = 4)]
public ActionResult<IEnumerable<SkipTokenPagingCustomer>> Get()
{
return customers;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,11 @@ public class ServerSidePagingEmployee
public int Id { get; set; }
public DateTime HireDate { get; set; }
}

public class SkipTokenPagingCustomer
{
public int Id { get; set; }
public string Grade { get; set; }
public decimal? CreditLimit { get; set; }
}
}
Loading