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

Support for $apply/aggregate #1921

Merged

Conversation

gathogojr
Copy link
Contributor

@gathogojr gathogojr commented Nov 11, 2020

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.

    // EntitySet?$apply=aggregate(Prop with average as AverageProp)
    EntitySet.Average(d1 => d1.Prop)

    // EntitySet?$apply=aggregate(Prop with sum as SumProp)
    EntitySet.Sum(d1 => d1.Prop)

    // EntitySet?$apply=aggregate(Prop with min as MinProp)
    EntitySet.Min(d1 => d1.Prop)

    // EntitySet?$apply=aggregate(Prop with max as MaxProp)
    EntitySet.Max(d1 => d1.Prop)

    // EntitySet?$apply=aggregate(Prop with countdistinct as CountDistinctProp)
    EntitySet.CountDistinct(d1 => d1.Prop)

    // EntitySet?$apply=aggregate(NavProp/Prop with average as AverageNavPro_Prop)
    EntitySet.Average(d1 => d1.NavProp.Prop)

    // EntitySet?$apply=aggregate(NavProp/Prop with sum as SumNavPro_Prop)
    EntitySet.Sum(d1 => d1.NavProp.Prop)

    // EntitySet?$apply=aggregate(NavProp/Prop with min as MinNavPro_Prop)
    EntitySet.Min(d1 => d1.NavProp.Prop)

    // EntitySet?$apply=aggregate(NavProp/Prop with max as MaxNavPro_Prop)
    EntitySet.Max(d1 => d1.NavProp.Prop)

    // EntitySet?$apply=aggregate(NavProp/Prop with countdistinct as CountDistinctNavPro_Prop)
    EntitySet.CountDistinct(d1 => d1.NavProp.Prop)

    // NOTE: We support nested navigation properties in the aggregate expression, i.e. d1 => d1.NavProp.NestedNavProp.Prop
    // E.g.
    // EntitySet?$apply=aggregate(NavProp/NestedNavProp/Prop with average as AverageNavProp_NestedNavProp_Prop)
    EntitySet.Average(d1 => d1.NavProp.NestedNavProp.Prop)

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.

    // EntitySet?$apply=filter(Amount gt 1)/aggregate(Prop with average as AverageProp)
    EntitySet.Where(d0 => d0.Amount > 1).Average(d1 => d1.Prop)

Checklist (Uncheck if it is not completed)

  • Test cases added
  • Build and test with one-click build and test script passed

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.

@gathogojr gathogojr force-pushed the feature/596-support-dollar-apply-aggregate branch 2 times, most recently from 47393b6 to dec83ae Compare November 11, 2020 15:58
@gathogojr gathogojr added the Ready for review Use this label if a pull request is ready to be reviewed label Nov 11, 2020
Copy link
Member

@marabooy marabooy left a 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
Copy link
Contributor

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?

Copy link
Contributor Author

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.

Comment on lines 3013 to 3022
// 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));
}
}
}
Copy link
Contributor

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)?

Copy link
Contributor Author

@gathogojr gathogojr Nov 16, 2020

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, ... });

Copy link
Contributor

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.
Copy link
Contributor

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.

Copy link
Contributor Author

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.

Copy link

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

Copy link
Contributor Author

@gathogojr gathogojr Nov 26, 2020

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 and Product are single-valued properties. Reviews is a collection property.
  • On the subject of aggregatable (or is it aggregable?), only Amount fits the bill - think average, sum, min, and max. You can't aggregate Currency - of type string, or Product - of type Product.
  • However, Currency would qualify as aggregatable property when using countdistinct 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.

Comment on lines 231 to 236
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");
Copy link
Contributor

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?

Comment on lines 652 to 690
}
aggregateBuilder.Append(UriHelper.RIGHTPAREN);
Copy link
Contributor

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 }?

Copy link
Contributor

@KenitoInc KenitoInc left a 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))
Copy link
Contributor

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);

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does StripTo do?

Copy link
Contributor Author

@gathogojr gathogojr Nov 16, 2020

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>
Copy link
Contributor

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.

Copy link
Contributor Author

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)
Copy link
Contributor

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?

Copy link
Contributor Author

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.

@gathogojr
Copy link
Contributor Author

gathogojr commented Nov 16, 2020

FYI:

When a Where method call appears before the aggregation method (e.g. Average, Sum, etc.), a filter transformation should be used to restrict the set of data to be aggregated. $filter query option, as well as other query options like $orderby, $expand, $top, $skip, etc., are not allowed before the $apply query option.

http://docs.oasis-open.org/odata/odata-data-aggregation-ext/v4.0/cs02/odata-data-aggregation-ext-v4.0-cs02.html#_Toc435016585

Example

dataServiceContext.Sales.Where(d0 => d0.Amount > 1).Average(d1 => d1.Amount);

should be translated into

/Sales?$apply=filter(Amount gt 1)/aggregate(Amount with average as AverageAmount)

NOT

/Sales?$filter=Amount gt 1&$apply=aggregate(Amount with average as AverageAmount)

Take note of the filter transformation (under $apply with no $ sign). I'll be checking in the logic to support that.

The expand transformation will be handled in a separate pull request

@gathogojr gathogojr force-pushed the feature/596-support-dollar-apply-aggregate branch from 73c9120 to 868a0b2 Compare November 25, 2020 07:23
Comment on lines 73 to 84
foreach (Expression expr in this.filterExpressions)
{
if (isFirst)
{
combinedPredicate = expr;
isFirst = false;
}
else
{
combinedPredicate = Expression.And(combinedPredicate, expr);
}
}
Copy link
Contributor

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));

Copy link
Contributor

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like it :)

Copy link

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

Comment on lines +380 to +386
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"));
Copy link
Contributor

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?

Copy link
Contributor Author

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

Copy link
Contributor Author

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

Comment on lines 56 to 62
internal ReadOnlyCollection<Expression> PredicateConjuncts
{
get
{
return new ReadOnlyCollection<Expression>(this.filterExpressions);
}
}
Copy link
Contributor

@habbes habbes Nov 25, 2020

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?

Copy link
Contributor Author

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

Comment on lines +400 to +411
// 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'
Copy link
Contributor

@habbes habbes Nov 25, 2020

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?

Copy link
Contributor Author

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:

  1. /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 and groupby - separated by a forward slash (/). The $filter will be applied to the aggregated result.

  2. /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 (/). The filter transformation before groupby 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

Comment on lines 9 to 13
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq.Expressions;
using Microsoft.OData.UriParser.Aggregation;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fix indentation

Comment on lines 36 to 38
/// <summary>
/// The <see cref="ExpressionType"/> of the <see cref="Expression"/>.
public override ExpressionType NodeType
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// <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

Comment on lines 116 to 118
this.Expression = exp;
this.AggregationMethod = aggregationMethod;
this.AggregationAlias = string.Empty;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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)
Copy link

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?

Copy link
Contributor Author

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>) or Average(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.
Copy link

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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// 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) + ")"; ;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return "filter(" + this.ExpressionToString(applyQueryOptionExpr.GetPredicate(), /*inPath*/ false) + ")"; ;
return "filter(" + this.ExpressionToString(applyQueryOptionExpr.GetPredicate(), /*inPath*/ false) + ")";

this.getResponseStream = getResponseStream;
}

#if (NETCOREAPP1_0 || NETCOREAPP2_0)
Copy link

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+?

Copy link
Contributor Author

@gathogojr gathogojr Nov 26, 2020

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

@gathogojr gathogojr force-pushed the feature/596-support-dollar-apply-aggregate branch from 3a1166f to cd58440 Compare November 30, 2020 18:05
return methodCallExpr;
}

QueryableResourceExpression input;
Copy link
Member

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
Copy link
Member

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)))
Copy link
Member

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"?

Copy link
Contributor Author

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)
Copy link
Member

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?

Copy link
Contributor Author

@gathogojr gathogojr Dec 7, 2020

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)
Copy link
Member

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)))
Copy link
Member

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
{
Copy link
Member

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>
Copy link
Member

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";
Copy link
Member

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"?

Copy link
Contributor Author

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

aggregateBuilder.Append(UriHelper.LEFTPAREN);
int i = 0;

while (true)
Copy link
Member

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 (...) ?

Copy link
Contributor Author

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);

Copy link
Member

@xuzhg xuzhg left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:shipit:

@xuzhg
Copy link
Member

xuzhg commented Dec 3, 2020

me too,


In reply to: 530944405 [](ancestors = 530944405)

@gathogojr gathogojr force-pushed the feature/596-support-dollar-apply-aggregate branch from cd58440 to 47c8fcb Compare December 7, 2020 17:37
@gathogojr gathogojr merged commit 37689e1 into OData:master Dec 8, 2020
@gathogojr gathogojr deleted the feature/596-support-dollar-apply-aggregate branch June 14, 2022 11:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Ready for review Use this label if a pull request is ready to be reviewed
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants