-
Notifications
You must be signed in to change notification settings - Fork 934
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
Conversation
@@ -67,8 +75,8 @@ protected override Expression VisitMember(MemberExpression expression) | |||
return base.VisitMember(expression); | |||
} | |||
|
|||
if ((elementSelector is NewExpression || elementSelector.NodeType == ExpressionType.Convert) |
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.
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.
The last commit resolves the issue with |
|
||
// 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) |
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 only way to test this was to include and use Microsoft.AspNetCore.OData
package as this cannot be reproduced by creating classes in code.
src/NHibernate/Linq/Visitors/TransparentIdentifierRemovingExpressionVisitor.cs
Show resolved
Hide resolved
.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 |
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's all good for now, but probably one of 2 things should happen:
- This fixed in OData provider so it generates the correct expression.
- This fixed in ReLinq so we do not copy code from there and just use fixed version.
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.
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.
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 } |
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.
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;
}
}
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 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.
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 think this might be restricted to some known types (potentially a configurable option) and some heuristics (like looking for [CompilerGeneratedAttribute]).
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 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.
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.
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.
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 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.
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.
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.
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 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
.
src/NHibernate/Linq/Visitors/TransparentIdentifierRemovingExpressionVisitor.cs
Show resolved
Hide resolved
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.) |
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) |
Co-Authored-By: Frédéric Delaporte <12201973+fredericDelaporte@users.noreply.github.com>
This comment has been minimized.
This comment has been minimized.
Oh.. Somehow missed your commit. So yes I'm for your suggestion with separate commits. |
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. |
So I have updated the description for avoiding closing #2334 when merging this PR. |
Fixes #2221
Fixes part of #2334
Fixes #2135