-
Notifications
You must be signed in to change notification settings - Fork 352
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
Support for $apply/aggregate #1921
Support for $apply/aggregate #1921
Conversation
47393b6
to
dec83ae
Compare
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.
LGTM
|
||
ODataMessageReaderSettings messageReaderSettings = new ODataMessageReaderSettings | ||
{ | ||
Validations = ~ValidationKinds.ThrowOnUndeclaredPropertyForNonOpenType |
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.
What does the ~
operator do in this case?
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.
@habbes The ~ operator performs a bitwise complement operation on its operand. In this case, we're configuring the message reader NOT to throw on undeclared property for non-open type.
// Disallow unsupported scenarios like the following: | ||
// - Average(d1 => d1.Prop.Length) | ||
// - Average(d1 => d1.CollectionProp.Count) | ||
MemberExpression parentExpr = StripTo<MemberExpression>(memberExpr.Expression); | ||
if (parentExpr != null) | ||
{ | ||
if (PrimitiveType.IsKnownNullableType(parentExpr.Type)) | ||
{ | ||
throw new NotSupportedException(Strings.ALinq_InvalidAggregateExpression(expr)); | ||
} | ||
|
||
Type collectionType = ClientTypeUtil.GetImplementationType(parentExpr.Type, typeof(ICollection<>)); | ||
if (collectionType != null) | ||
{ | ||
throw new NotSupportedException(Strings.ALinq_InvalidAggregateExpression(expr)); | ||
} | ||
} | ||
} |
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.
What is it that's not supported? Is it accessing the length of properties like strings or collections or is it accessing nested properties? is d1.Prop.ChildProp
supported? and is d1.Rectangle.Length
supported (assuming Square
is not a collection)?
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.
@habbes Nested properties are supported. E.g. If you have a navigation property Product
, you can group by property ProductName
of Product
type as follows:
GroupBy(d1 => d1.Product.ProductName, ... )
What is not supported, is invoking properties of a known primitive type in the grouping expression. E.g. say ProductName
in my example is of type string
, it would be invalid to have
GroupBy(d1 => d1.Product.ProductName.Length, ... )
We throw a NotSupportedExpression
if that is used in the GroupBy
keySelector
expression
Note that this validation does not stop you from doing this:
GroupBy(d1 => d1.Product.ProductName, (d1, d2) => new { ProductNameLength = d1.Length, ... });
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 comment or error-message should be more clear that the scenario being guarded against is accessing properties of known primitive types.
@@ -2910,6 +2988,46 @@ internal static void ValidateExpandPath(Expression input, DataServiceContext con | |||
|
|||
throw new NotSupportedException(Strings.ALinq_InvalidExpressionInNavigationPath(input)); | |||
} | |||
|
|||
/// <summary> | |||
/// Checks whether the specified <paramref name="expr"/> is a valid aggregate 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.
It would be good to know what counts as a valid aggregate 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.
In the specification, the aggregate expression has to meet one of 5 criteria. I'll see how I can improve the docstring to give the user a better idea. The exception message tries to capture what is wrong with the exception.
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.
Have to admit I can't say I fully understand this line :)
An aggregate expression must evaluate to a single-valued property path to an aggregatable property
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.
@odero
Consider the following class:
class Sale
{
public string Currency { get; set; }
public Amount Amount { get; set; }
public Product Product { get; set; }
public Collection<Review> Reviews { get; set; }
}
Currency
,Amount
andProduct
are single-valued properties.Reviews
is a collection property.- On the subject of aggregatable (or is it aggregable?), only
Amount
fits the bill - thinkaverage
,sum
,min
, andmax
. You can't aggregateCurrency
- of type string, orProduct
- of type Product. - However,
Currency
would qualify as aggregatable property when usingcountdistinct
aggregation.
The fact that I have used this many words and an illustration to explain tells you how hard it is to come up with a good docstring for the method :) I resorted to borrowing the words from the specification.
If you go through the method there are helpful comments that explain the scenarios we're validating for.
aggregationMethodMap.Add(AggregationMethod.Sum, "sum"); | ||
aggregationMethodMap.Add(AggregationMethod.Average, "average"); | ||
aggregationMethodMap.Add(AggregationMethod.Min, "min"); | ||
aggregationMethodMap.Add(AggregationMethod.Max, "max"); | ||
aggregationMethodMap.Add(AggregationMethod.CountDistinct, "countdistinct"); | ||
aggregationMethodMap.Add(AggregationMethod.VirtualPropertyCount, "$count"); |
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.
Out of curiosity, why doesn't this use the string constants defined in UriHelper
?
} | ||
aggregateBuilder.Append(UriHelper.RIGHTPAREN); |
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 an empty line after the close }
?
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.
A few comments/questions
}; | ||
|
||
ODataResource entry = default(ODataResource); | ||
using (var messageReader = new ODataMessageReader(httpWebResponseMessage, messageReaderSettings, this.Context.Format.ServiceModel)) |
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 got it from Mike that we should avoid using var
as much as we can. Use the explicit type.
internal static void ValidateAggregateExpression(Expression expr) | ||
{ | ||
MemberExpression memberExpr = StripTo<MemberExpression>(expr); | ||
|
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.
What does StripTo
do?
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.
@KenitoInc It strips the expression of intermediate expressions like Expression.Convert()
and Expression.Quote()
. See more here
/// <typeparam name="TSource">The type of the elements of <paramref name="source"/></typeparam> | ||
/// <typeparam name="TTarget">The type returned by the projection function represented in <paramref name="selector"/>.</typeparam> | ||
/// <param name="source">A sequence of values of type <typeparamref name="TSource"/>.</param> | ||
/// <param name="selector">A projection function to apply to each element.</param> |
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 the CountDistinct
methods, the selector
params description should be different. One is an Expression
, the other is a Func
.
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.
@KenitoInc Note that in the official docs for Queryable
and Enumerable
methods, the descriptions don't differ much
selector Func<TSource,TResult>
- A transform function to apply to each element.
selector Expression<Func<TSource,TResult>>
- A projection function to apply to each element.
// - new List<int> { 1, 2, 1 }.CountDistinct(d => d) | ||
|
||
// Method: Select<TSource,TResult>(IEnumerable<TSource>, Func<TSource,TResult>) | ||
MethodInfo selectMethod = typeof(Enumerable).GetMethods(BindingFlags.Static | BindingFlags.Public) |
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 refractor the GetSelectMethod
and move this logic there?
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.
The logic for getting the Select
defined in Enumerable
type differs a little from the one defined in Queryable
type but I'll see how I can refactor and use distinct names for each.
FYI: When a Example dataServiceContext.Sales.Where(d0 => d0.Amount > 1).Average(d1 => d1.Amount); should be translated into
NOT
Take note of the The |
73c9120
to
868a0b2
Compare
foreach (Expression expr in this.filterExpressions) | ||
{ | ||
if (isFirst) | ||
{ | ||
combinedPredicate = expr; | ||
isFirst = false; | ||
} | ||
else | ||
{ | ||
combinedPredicate = Expression.And(combinedPredicate, expr); | ||
} | ||
} |
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 his could be simplified as
combinedPredicate = filterExpressions.Aggregate((leftExpr, rightExpr) => Expression.And(leftExpr, rightExpr));
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.
If it doesn't negatively impact readability or perf that is.
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 like it :)
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.
tbf I find the previous one more readable
else if (this.Skip != null) | ||
{ | ||
// $skip and $top may be used together with rollup. | ||
// However, support for rollup is currently not implemented in OData WebApi | ||
// If $skip and/or $top appears before $apply, its currently ignored. | ||
// Makes sense to throw an exception to avoid giving a false impression. | ||
throw new NotSupportedException(Strings.ALinq_QueryOptionOutOfOrder("apply", "skip")); |
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.
Should we make the assumption that the client will only be used with servers based on WebAPI?
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.
@habbes I'll sidebar with you on this
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.
@habbes @odero Just to clarify, ODL doesn't support rollup
either.
The rollup
grouping operator allows requesting additional levels of aggregation in addition to the most granular level defined by the grouping properties. It can be used instead of a property path in the first parameter of groupby
. To me its a feature for the future if ever.
Here's a link to the section of the spec that describes how it works http://docs.oasis-open.org/odata/odata-data-aggregation-ext/v4.0/cs02/odata-data-aggregation-ext-v4.0-cs02.html#_Toc435016584
internal ReadOnlyCollection<Expression> PredicateConjuncts | ||
{ | ||
get | ||
{ | ||
return new ReadOnlyCollection<Expression>(this.filterExpressions); | ||
} | ||
} |
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 property accessed multiple times? And if so, is it expected to return a new collection each time?
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.
@habbes ReadOnlyCollection is just a wrapper on top of the filterExpressions
list. It does not create a new list
https://docs.microsoft.com/en-us/dotnet/api/system.collections.objectmodel.readonlycollection-1?view=net-5.0#examples
// The $apply query option is evaluated first, then other query options ($filter, $orderby, $select) are evaluated, | ||
// if applicable, on the result of $apply in their normal order. | ||
// http://docs.oasis-open.org/odata/odata-data-aggregation-ext/v4.0/cs02/odata-data-aggregation-ext-v4.0-cs02.html#_Toc435016590 | ||
|
||
// If a Where appears before an aggregation method (e.g. Average, Sum, etc) or GroupBy, | ||
// the conjuncts of the filter expression will be used to restrict the set of data to be aggregated. | ||
// They will not appear on the $filter query option. Instead, we use them to construct a filter transformation. | ||
// E.g. /Sales?$apply=filter(Amount gt 1)/aggregate(Amount with average as AverageAmount) | ||
|
||
// If a Where appears after an aggregation method or GroupBy, the conjuncts should appear | ||
// on a $filter query option after the $apply. | ||
// E.g. /Sales?$apply=groupby((Product/Color),aggregate(Amount with average as AverageAmount))&$filter=Product/Color eq 'Brown' |
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.
So there's support for having Where clauses both before and after an aggregation? In this case the generated query would contain both a filter transformation in the $apply clause as well as a $filter clause? Like /Sales?$apply=filter(Amount gt 1)/groupby((Product/Color),aggregae(Amount with average as Avg))$filter=Product/Color eq 'Brown'
?
Is this a correct implication of what you've explained here? If so, maybe a test case could be added to verify that i's supported?
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.
@habbes You understood it correctly. A $filter
(as well as $order
and $select
) query option can appear after the $apply
. When it does, it'll be applied to the result of the aggregation.
As a matter of fact, a filter
transformation can also appear after groupby
and aggregate
expression. When it does, it'll be applied to the result of the aggregation, such that the following two expressions give you the same result:
-
/Sales?$apply=filter(Amount gt 1)/groupby((Product/Color),aggregate(Amount with average as AverageAmount))&$filter=Product/Color eq 'Brown'
We're dealing with 2 query params here -$apply
and$filter
- separated by a&
. The$apply
here has 2 transformations -filter
andgroupby
- separated by a forward slash (/). The$filter
will be applied to the aggregated result. -
/Sales?$apply=filter(Amount gt 1)/groupby((Product/Color),aggregate(Amount with average as AverageAmount))/filter(Product/Color eq 'Brown')
We're dealing with a single query param here -$apply
. It has 3 transformations separated by a forward slash (/). Thefilter
transformation beforegroupby
restricts the set of data to be aggregated while the one that appears after is applied to the aggregated result
That said, I deliberately decided to leave out support for filter
transformation appearing after the groupby
and aggregate
transformation as well as system query options appearing after $apply
. The effort required to make them play is not trivial. That work will tackled in a separate work item/pull request
using System; | ||
using System.Collections.Generic; | ||
using System.Collections.ObjectModel; | ||
using System.Linq.Expressions; | ||
using Microsoft.OData.UriParser.Aggregation; |
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.
Fix indentation
/// <summary> | ||
/// The <see cref="ExpressionType"/> of the <see cref="Expression"/>. | ||
public override ExpressionType NodeType |
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.
/// <summary> | |
/// The <see cref="ExpressionType"/> of the <see cref="Expression"/>. | |
public override ExpressionType NodeType | |
/// <summary> | |
/// The <see cref="ExpressionType"/> of the <see cref="Expression"/>. | |
/// </summary> | |
public override ExpressionType NodeType | |
this.Expression = exp; | ||
this.AggregationMethod = aggregationMethod; | ||
this.AggregationAlias = 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.
this.Expression = exp; | |
this.AggregationMethod = aggregationMethod; | |
this.AggregationAlias = string.Empty; | |
Aggregation(exp, aggregationMethod, string.Empty) | |
private static Expression AnalyzeAggregation(MethodCallExpression methodCallExpr, AggregationMethod aggregationMethod) | ||
{ | ||
Debug.Assert(methodCallExpr != null, "methodCallExpr != null"); | ||
if (methodCallExpr.Arguments.Count != 2) |
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.
Why must the number of args be == 2?
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.
@odero Aggregation methods - Average
, Sum
, Min
, Max
) defined in Queryable
and Enumerable
classes have two sets of overloads for each aggregable type (double
, double?
, float
, float?
, int
, int?
, long
, long?
, decimal
, decimal?
)
- The first overload takes one parameter. Examples:
Average(IQueryable<Decimal>)
orAverage(IEnumerable<Decimal>)
.
To use this overload, you'd write the expression as follows:context.Sales.Select(d1 => d1.Amount).Average(); // => Average(IEnumerable<Decimal>)
- The second overload takes two parameters. Example:
Average<TSource>(IQueryable<TSource>, Expression<Func<TSource,Decimal>>)
.
To use this overload, you'd write the expression as follows:context.Sales.Average(d1 => d1.Amount);
I implemented support for the second overload in this pull request. That's the reason we're debug-asserting to verify that we're dealing with the second overload. It is succinct and requires the least amount of effort and almost no refactor to fit into the existing logic.
Implementing the first overload would be a lot of pain with little or no additional gain. Among other reasons, the Select()
method is currently translated into $select
expression. We'd need to do quite a bit of messy plumbing and refactor to handle Select()
differently depending on whether or not its followed by an aggregation method, i.e.:
- Translate
Select(d1 => d1.Amount)
into$select=Amount
, and - Translate
Select(d1 => d1.Amount).Average()
into$apply=aggregate(Amount with average as AverageAmount)
The strategy would be to "look ahead" when doing the translation to determine which expression to translate into.
That support could be implemented at a later date. The effort would not be trivial so we'd need to have a strong justification.
We'd obviously need to document what is supported for the benefit of the users.
@@ -2910,6 +2988,46 @@ internal static void ValidateExpandPath(Expression input, DataServiceContext con | |||
|
|||
throw new NotSupportedException(Strings.ALinq_InvalidExpressionInNavigationPath(input)); | |||
} | |||
|
|||
/// <summary> | |||
/// Checks whether the specified <paramref name="expr"/> is a valid aggregate 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.
Have to admit I can't say I fully understand this line :)
An aggregate expression must evaluate to a single-valued property path to an aggregatable property
MemberExpression memberExpr = StripTo<MemberExpression>(expr); | ||
|
||
// ResourceBinder's VisitMemberAccess override transforms member access expressions | ||
// involving properties of known primitive types into their method method equivalent |
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.
// involving properties of known primitive types into their method method equivalent | |
// involving properties of known primitive types into their method equivalent | |
return string.Empty; | ||
} | ||
|
||
return "filter(" + this.ExpressionToString(applyQueryOptionExpr.GetPredicate(), /*inPath*/ false) + ")"; ; |
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.
return "filter(" + this.ExpressionToString(applyQueryOptionExpr.GetPredicate(), /*inPath*/ false) + ")"; ; | |
return "filter(" + this.ExpressionToString(applyQueryOptionExpr.GetPredicate(), /*inPath*/ false) + ")"; | |
this.getResponseStream = getResponseStream; | ||
} | ||
|
||
#if (NETCOREAPP1_0 || NETCOREAPP2_0) |
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 check necessary? What about core 3+?
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.
@odero Maybe not... I'll remove it and see how the tests behave. We're overriding GetResponse()
for Core 3+ but looking at HttpWebRequestMessage
class, I think the override should apply for all target versions
3a1166f
to
cd58440
Compare
return methodCallExpr; | ||
} | ||
|
||
QueryableResourceExpression input; |
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.
input [](start = 40, length = 5)
maybe rename it as "output", it's weird to use "input" as output. maybe other name
@@ -386,17 +384,10 @@ public virtual DataServiceQuery<TElement> AddQueryOption(string name, object val | |||
|
|||
/// <summary>Executes the query and returns the results as a collection.</summary> | |||
/// <returns>A typed enumerator over the results in which TElement represents the type of the query results.</returns> | |||
#if !PORTABLELIB // Synchronous methods not available |
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.
#if !PORTABLELIB [](start = 0, length = 16)
thanks for removing this
{ | ||
MethodCallExpression countMethodExpr; | ||
|
||
if (source.Provider.GetType().Equals(typeof(DataServiceQueryProvider))) |
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.
source.Provider.GetType().Equals(typeof(DataServiceQueryProvider))) [](start = 16, length = 67)
Could the provider type be the sub type of "DataServiceQueryProvider"?
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.
@xuzhg Thanks to your observation here. As a result of looking at it afresh I was able to significantly reduce the code in the extension method
/// <summary> | ||
/// Returns Distinct method defined in <paramref name="declaringType"/>. | ||
/// </summary> | ||
private static MethodInfo GetDistinctMethod(Type declaringType, Type sourceType) |
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.
GetDistinctMethod [](start = 34, length = 17)
should be static (cache) it?
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.
@xuzhg Used SimpleLazy
class to initialize a private static readonly field with the result of the method call. It should ensure that we only call the method once. Let me know if its a good fit
/// <param name="source">A sequence of values of type <typeparamref name="TSource"/>.</param> | ||
/// <param name="selector">A transform function to apply to each element.</param> | ||
/// <returns>Distinct count of elements in a sequence after applying the projection function to each element.</returns> | ||
public static int CountDistinct<TSource, TTarget>(this IEnumerable<TSource> source, Func<TSource, TTarget> selector) |
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.
IEnumerable source [](start = 63, length = 27)
should we check the null?
{ | ||
MethodCallExpression countMethodExpr; | ||
|
||
if (source.Provider.GetType().Equals(typeof(DataServiceQueryProvider))) |
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.
should we check the null?
namespace Microsoft.OData.Client.Tests | ||
{ | ||
public class TestHttpWebRequestMessage : HttpWebRequestMessage | ||
{ |
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.
align
/// <summary>aggregate</summary> | ||
internal const string AGGREGATE = "aggregate"; | ||
|
||
/// <summary>aggregate</summary> |
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.
with
as
internal const string COUNTDISTINCT = "countdistinct"; | ||
|
||
/// <summary>$count</summary> | ||
internal const string VIRTUALPROPERTYCOUNT = "$count"; |
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.
VIRTUALPROPERTYCOUNT [](start = 30, length = 20)
why has a name "virtual property count" equals "$count"?
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.
@xuzhg I borrowed this one from ODL
VirtualPropertyCount, |
aggregateBuilder.Append(UriHelper.LEFTPAREN); | ||
int i = 0; | ||
|
||
while (true) |
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.
while (true) [](start = 11, length = 13)
why don't use foreach (...) ?
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.
@xuzhg This is one scenario where traversing the collection using the indexer worked better. In a single statement, we're able increment the counter and check if its time to exit to avoid adding a trailing comma. The trailing comma would make the Uri invalid. Re:
if (++i == aggregations.Count)
{
break;
}
aggregateBuilder.Append(UriHelper.COMMA);
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.
me too, In reply to: 530944405 [](ancestors = 530944405) |
cd58440
to
47c8fcb
Compare
Issues
This pull request is 1 of 3 from splitting this huge pull request to make it easier to review.
Description
The simple aggregations below are covered. The scope of this PR is aggregations that return a single scalar result.
The filter transformation used together with aggregation is also covered. A filter transformation is used (not $filter) to restrict the set of data to be aggregated.
Checklist (Uncheck if it is not completed)
Additional work necessary
If documentation update is needed, please add "Docs Needed" label to the issue and provide details about the required document change in the issue.