Skip to content

Add support for OData group by queries #2322

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

Merged
merged 2 commits into from
Apr 14, 2020

Conversation

maca88
Copy link
Contributor

@maca88 maca88 commented Mar 6, 2020

Fixes #2221
Fixes part of #2334
Fixes #2135

@@ -67,8 +75,8 @@ protected override Expression VisitMember(MemberExpression expression)
return base.VisitMember(expression);
}

if ((elementSelector is NewExpression || elementSelector.NodeType == ExpressionType.Convert)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Instead of checking whether it is a ExpressionType.Convert expression, its operand is checked in order to verify whether it is actually supported. The ExpressionType.Convert was used for ExpressionType.ArrayIndex expression which was added by #433.

@maca88
Copy link
Contributor Author

maca88 commented Mar 30, 2020

The last commit resolves the issue with group by reported in #2334.


// Added logic: In some cases (e.g OData), the member can be from a different derived class, in such case
// we need to check the member DeclaringType instead of ReflectedType
if (matchingAssignment == null && memberExpression.Expression.NodeType == ExpressionType.MemberInit)
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 only way to test this was to include and use Microsoft.AspNetCore.OData package as this cannot be reproduced by creating classes in code.

@maca88 maca88 changed the title Add support for MemberInit expression in GroupBySelectClauseRewriter Add support for OData group by queries Mar 30, 2020
.LastOrDefault();

// Added logic: In some cases (e.g OData), the member can be from a different derived class, in such case
// we need to check the member DeclaringType instead of ReflectedType
Copy link
Member

Choose a reason for hiding this comment

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

It's all good for now, but probably one of 2 things should happen:

  1. This fixed in OData provider so it generates the correct expression.
  2. This fixed in ReLinq so we do not copy code from there and just use fixed version.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

but probably one of 2 things should happen:

I do agree and created an issue for ODAta, which I thnik is where the issue should be fixed.

@hazzik
Copy link
Member

hazzik commented Mar 31, 2020

@maca88 what about #2135?

if (expr is NewArrayExpression)
return (expr as NewArrayExpression).Expressions.SelectMany(ExtractKeyExpressions);
// --> new List<string> { x.A, x.B }
// --> new CustomType(x.A, x.B) { C = x.C }
Copy link
Member

@hazzik hazzik Mar 31, 2020

Choose a reason for hiding this comment

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

Not sure if this is correct. There are examples when this assumption is not working.

public class CustomType1
{
    public int A => 0;
    public CustomType(int a)
    {
    }
}
public class CustomType2
{
    public int A { get; }
    public int B { get; }
    public CustomType(int a, int b)
    {
        A = b;
        B = a;
    }
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It would be incorrect for Linq to Objects, but in our scenario the group by is done on the database, so the constructor of CustomType2 will never be called. In our case the following expressions will produce the same result:

GroupBy(x => new List<string> { x.A, x.B, x.C })
GroupBy(x => new CustomType(x.A, x.B) { C = x.C })
GroupBy(x => new { x.A x.B, x.C })

The idea of this method is to collect all mapped properties inside the expression and use them as keys for grouping.

Copy link
Member

Choose a reason for hiding this comment

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

I think this might be restricted to some known types (potentially a configurable option) and some heuristics (like looking for [CompilerGeneratedAttribute]).

Copy link
Member

@bahusoid bahusoid Apr 10, 2020

Choose a reason for hiding this comment

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

so the constructor of CustomType2 will never be called.

What about case when group by key is selected?

GroupBy(x => new CustomType(x.A, x.B) { C = x.C }).Select(x => x.Key);

In any case I think it's better keep the code simpler and allow it without any added heuristics (and I've seen multiple requests specifically to support ctors for user DTOs for whatever legacy reasons).

There are examples when this assumption is not working.

Well it applies for all cases with user types - user can override equality methods or getters with some strange logic and that's it.

Copy link
Member

Choose a reason for hiding this comment

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

There are examples when this assumption is not working.

This concern already holds true for current master state. I do not think we should consider this as added or worsen by this PR. So I think it should be addressed in another PR, if it actually needs to be addressed.

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 think this might be restricted to some known types (potentially a configurable option) and some heuristics (like looking for [CompilerGeneratedAttribute])

What would happen when the restriction is not met? For example the following query:

_session.Query<X>().GroupBy(x => new CustomType(x.A, x.B) { C = x.C }).ToList();

if the restriction is not met we could either throw an exception or execute _session.Query<X>() and do the rest on the client, which I don't thnik we should do.

like looking for [CompilerGeneratedAttribute]

In OData, the wrappers that they use have only JsonConverter attribute which I don't think we should check. The only restriction that we may add, is to check whether the type overriders Equals and GetHashCode methods and throw when it does. But even that restriction does not make much sense as grouping in Linq To Objects by a type that does not override those methods won't group any items together, where in our case it may group from zero to all items together. In my opinion we should by default allow all types and optionally having a configurable option that would throw when the given restriction is not met.

What about case when group by key is selected?

In that case the constructor will be called for each group. The following query produces the same result:

GroupBy(x => new {x.A, x.B, x.C }).Select(x => new CustomType(x.Key.A, x.Key.B) { C = x.Key.C }); 

When the user wants to return all groups as a list of CustomType then IMO your query is better as it is shorter to write.

Copy link
Member

@bahusoid bahusoid Apr 13, 2020

Choose a reason for hiding this comment

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

to check whether the type overrides Equals and GetHashCode methods and throw when it does

I don't get it. Why? Wouldn't it throw for all anonymous types? Did you mean and throw when it **doesn't"**? If I got it right the main concern was about difference between Linq to Objects and NHibernate results. And from this context user type must override equality members to have the same results.
But there is so many ways that user can break the difference in results so I wouldn't implement any additional restrictions at all.

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 don't get it. Why?

Whether we would want to prevent the user from thinking that the Equals and GetHashCode would be used for grouping.

Did you mean and throw when it doesn't"?

No, I ment when it does override those methods it would throw.

But there is so many ways that user can break the difference in results so I wouldn't implement any additional restrictions at all.

True, that is why I also agree that we should not add any restriction by default.

Also, I have to correct myself as in the following case the constructor will also be called:

_session.Query<X>().GroupBy(x => new CustomType(x.A, x.B) { C = x.C }).Select(o => new { o.Key.A, o.Key.B, o.Key.C }).ToList();

beacuse there is no explicit link in the expression between o.Key.A\o.Key.B and x.A\x.B, due to the fact that x.A and x.B are passed to the constructor.
Which means that it will work correctly even when x.A is set in the constructor to property CustomType.B.

@maca88
Copy link
Contributor Author

maca88 commented Apr 1, 2020

@maca88 what about #2135?

This PR solves it as well, added it in the description.

@fredericDelaporte fredericDelaporte added this to the 5.3 milestone Apr 13, 2020
@fredericDelaporte
Copy link
Member

fredericDelaporte commented Apr 13, 2020

Either we squash it all for merging, or the last three commits should be squashed together for then merging the two remaining commits. (No need to keep me as a co-author for just a todo comment, by the way.)

@bahusoid
Copy link
Member

Either we squash it all for merging, or the last three commits should be squashed together for then merging the two remaining commits.

I think changes that would be no longer necessary after OData/WebApi#2108 is fixed should go as separate commit. And I think we should also add TODO in code with link to opened ODATA issue and suggestion to drop this code when it's fixed (if it's really only a hack for ODATA)

maca88 and others added 2 commits April 13, 2020 14:37
@fredericDelaporte

This comment has been minimized.

@bahusoid
Copy link
Member

Oh.. Somehow missed your commit. So yes I'm for your suggestion with separate commits.

@maca88
Copy link
Contributor Author

maca88 commented Apr 13, 2020

Squashed into two commits and preserved @fredericDelaporte as a co-author. I didn't tag #2334 in the commit as the issue is also about selection which is not fixed by this PR.

@fredericDelaporte
Copy link
Member

So I have updated the description for avoiding closing #2334 when merging this PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Support MemberInit expression in group by Support OData GroupBy/Aggregate
4 participants