-
Notifications
You must be signed in to change notification settings - Fork 160
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
Fixes #1147: Enable skiptoken handler to handler advanced orderby clause #1164
Conversation
{ | ||
if (lastMember == null) | ||
{ | ||
return string.Empty; | ||
} | ||
|
||
IEnumerable<IEdmProperty> propertiesForSkipToken = GetPropertiesForSkipToken(lastMember, model, orderByNodes); | ||
StringBuilder skipTokenBuilder = new StringBuilder(String.Empty); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here's the part 1 refactor. #WontFix
{ | ||
Contract.Assert(query != null); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here's the part 2 refactor #WontFix
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Partial review. Will share more feedback later.
/// </summary> | ||
/// <param name="clause">The input orderby clause, the 'ThenBy' in each node does matter.</param> | ||
/// <returns>The output orderby clauses, the 'ThenBy' in each node does NOT matter.</returns> | ||
public static IList<OrderByClause> ToList(this OrderByClause clause) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since this class is internal, I would suggest returning a List<T>
instead of IList<T>
since using List<T>
directly is more efficient. For example when doing a foreach
against an IList<T>
it will box and allocate the enumerator to the heap, but when using List<T>
directly you avoid boxing the enumerator. #Resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Keep eyes on List is more efficent than IList. Since it's internal, I am ok to replace it using List.
} | ||
|
||
// Avoid duplicated binding. | ||
ISet<string> usedPropertyNames = new HashSet<string>(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For internal, private or local variables, I suggest we use concrete types. Using interfaces lead to more boxing of enumerators and methods are less likely to get inlined by the JIT.
We should consider interfaces for public APIs or for scenarios where we actually want to support different implementations. But in this case, we have no reason to support other implementations than HashSet<string>
.
#Resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe you can give us a 'session' about this.
|
||
Expression propertyName; | ||
Expression propertyValue; | ||
IList<string> names = new List<string>(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IList<string> names = new List<string>(); | |
List<string> names = new List<string>(); | |
``` #Resolved |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it possible to estimate the capacity of the list from the input so that we avoid resizing?
} | ||
} | ||
|
||
private static string GetOrderByName(ISet<string> usedPropertyNames, ref int start) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
private static string GetOrderByName(ISet<string> usedPropertyNames, ref int start) | |
private static string GetOrderByName(HashSet<string> usedPropertyNames, ref int start) | |
``` #Resolved |
|
||
name = GetOrderByName(usedPropertyNames, ref start); | ||
names.Add(name); | ||
usedPropertyNames.Add(name); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Given that each name is generated by adding an incrementing counter, each iteration will produce a unique name. In that case, do we still need to add to these auto-generated names to the usedPropertyNames
set? #Resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good Suggestion. Let me comment it out and leave a comment for it.
Expression propertyValue; | ||
IList<string> names = new List<string>(); | ||
string name; | ||
int start = 1; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
int start = 1; | |
int index = 1; | |
``` #Resolved |
@@ -679,6 +680,9 @@ private static IEnumerable<IEdmStructuralProperty> GetAvailableOrderByProperties | |||
: entityType | |||
.StructuralProperties() | |||
.Where(property => property.Type.IsPrimitive() && !property.Type.IsStream()) | |||
|
|||
// @discuss: Orderby the keys using the key name alphabet order doesn't make sense for me. | |||
// Since we don't have the 'Order' value, we should keep the order same as definition order in the schema? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe the goal was to ensure the properties are always returned in a consistent order. Maybe any order is valid so long as it's consistent between calls. But I don't know the context.
But if there's something we can do to void sorting, I'd vouch for that. We saw some a lot of cycles from sorting and other LINQ operations related to ETag properties, and I think we should avoid that where possible. #Pending
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd like to here input from @mikepizzo also.
It's just for discussion. I will leave this code unchanged since change it is a behavior breaking changes.
IEnumerable<IEdmProperty> propertiesForSkipToken = GetPropertiesForSkipToken(lastMember, model, orderByNodes); | ||
StringBuilder skipTokenBuilder = new StringBuilder(String.Empty); | ||
if (propertiesForSkipToken == null) | ||
IList<OrderByClause> clauses = GetOrderByClauses(lastMember, model, clause); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IList<OrderByClause> clauses = GetOrderByClauses(lastMember, model, clause); | |
List<OrderByClause> clauses = GetOrderByClauses(lastMember, model, clause); | |
``` #Resolved |
} | ||
|
||
return skipTokenBuilder.ToString(); | ||
} | ||
|
||
private static IList<KeyValuePair<string, object>> GetPropertyValues(object source, IEdmModel model, IList<OrderByClause> clauses, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
private static IList<KeyValuePair<string, object>> GetPropertyValues(object source, IEdmModel model, IList<OrderByClause> clauses, | |
private static List<KeyValuePair<string, object>> GetPropertyValues(object source, IEdmModel model, List<OrderByClause> clauses, | |
``` #Resolved |
IList<OrderByClause> clauses = GetOrderByClauses(lastMember, model, clause); | ||
|
||
TimeZoneInfo timeZoneInfo = context?.TimeZone; | ||
IList<KeyValuePair<string, object>> values = GetPropertyValues(lastMember, model, clauses, context); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IList<KeyValuePair<string, object>> values = GetPropertyValues(lastMember, model, clauses, context); | |
IList<KeyValuePair<string, object>> values = GetPropertyValues(lastMember, model, clauses, context); |
IList<KeyValuePair<string, object>> values = GetPropertyValues(lastMember, model, clauses, context); | |
List<KeyValuePair<string, object>> values = GetPropertyValues(lastMember, model, clauses, context); | |
``` #Resolved |
|
||
structuredObj.TryGetPropertyValue(OrderByClauseHelpers.OrderByGlobalNameKey, out object orderByNameObject); | ||
|
||
IList<KeyValuePair<string, object>> values = new List<KeyValuePair<string, object>>(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IList<KeyValuePair<string, object>> values = new List<KeyValuePair<string, object>>(); | |
List<KeyValuePair<string, object>> values = new List<KeyValuePair<string, object>>(); | |
``` #Resolved |
} | ||
|
||
return skipTokenBuilder.ToString(); | ||
} | ||
|
||
private static IList<KeyValuePair<string, object>> GetPropertyValues(object source, IEdmModel model, IList<OrderByClause> clauses, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this method called when consuming skip tokens? #WontFix
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, It's for 'Part-1' to generate the Skip Token string.
@@ -464,28 +456,35 @@ internal static IEnumerable<IEdmProperty> GetPropertiesForSkipToken(object lastM | |||
return null; | |||
} | |||
|
|||
IEnumerable<IEdmProperty> key = entity.Key(); | |||
if (orderByNodes != null) | |||
IList<OrderByClause> orderByClauses = new List<OrderByClause>(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IList<OrderByClause> orderByClauses = new List<OrderByClause>(); | |
List<OrderByClause> orderByClauses = new List<OrderByClause>(); | |
``` #Resolved |
IEnumerable<IEdmProperty> key = entity.Key(); | ||
if (orderByNodes != null) | ||
IList<OrderByClause> orderByClauses = new List<OrderByClause>(); | ||
ISet<IEdmProperty> properties = new HashSet<IEdmProperty>(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ISet<IEdmProperty> properties = new HashSet<IEdmProperty>(); | |
HashSet<IEdmProperty> properties = new HashSet<IEdmProperty>(); | |
``` #Resolved |
#region Data | ||
static SkipTokenControllers() | ||
{ | ||
IList<StAddress> stAddresses = new List<StAddress>() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you have test cases with dates and negative numbers? #Resolved
// Since we don't have such methods, let's simply split the request raw value. | ||
IList<string> orderBys = orderByOption.RawValue.Split(','); | ||
IList<OrderByClause> orderByClauses = orderByOption.OrderByClause.ToList(); | ||
IList<string> tokenValueParis = PopulatePropertyValuePairs(skipTokenRawValue); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IList<string> tokenValueParis = PopulatePropertyValuePairs(skipTokenRawValue); | |
IList<string> tokenValuePairs = PopulatePropertyValuePairs(skipTokenRawValue); | |
``` #Resolved |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, since this list only contains the values (without keys), why not just tokenValues
?
IList<string> tokenValueParis = PopulatePropertyValuePairs(skipTokenRawValue); | |
List<string> tokenValues = PopulatePropertyValuePairs(skipTokenRawValue); |
|
||
foreach (KeyValuePair<string, (object PropertyValue, Type PropertyType)> item in propertyValuePairs) | ||
string where = string.Empty; | ||
string lastEquality = null; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: The name lastEquality
might give the impression that this refers to the last or final equality in the loop, but actually it refers to the equality generated in the previous iteration.
string lastEquality = null; | |
string previousEquality = null; | |
``` #Resolved |
IDictionary<string, (object PropertyValue, Type PropertyType)> propertyValuePairs = PopulatePropertyValuePairs(skipTokenRawValue, context); | ||
// It's better to visit the nodes of OrderByClause reclusively to get the orderby raw value. | ||
// Since we don't have such methods, let's simply split the request raw value. | ||
IList<string> orderBys = orderByOption.RawValue.Split(','); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This will fail if for non-trivial orderby expressions. For example, if you have a function call with multiple parameters like $orderby=substring(Field, 1),Id
, this will splitinto: ["substring(Field", "1)", "Id"]
which is invalid and will lead to an argument out of range exception because orderBys
list will have more items than orderByClauses
list.
Did you intend to support these scenarios in this PR or in a future one? If you want to add support for that in the future then we should detect such cases and throw a meaningful exception that lets the user know that operation is not supported, and then we should create an issue to support that. Otherwise we should handle it better in this PR. Do we have an method that can generate a string representation of an orderByClause.Expression
that is a valid OData query expression?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed. Can you help to test it again?
src/Microsoft.AspNetCore.OData/Query/Query/DefaultSkipTokenHandler.cs
Outdated
Show resolved
Hide resolved
1) Can handle the 'asc' suffix 2) Can handle the ',' within expression
Fixes #1147
Two parts:
1. How to generate the skip token when we have the advanced $orderby?
2. How to consume the skip token when apply the $skiptoken?
Basically, the skiptoken is coverted to a 'Where(...)' clause (Linq Where function call). Behind the scene, it's a $filter clause.
The existing implementation cannot handle all and it contains errors.
This PR is to change it and based on the above truth, we should use our
IFilterBinder
to handle the $skiptoken.