diff --git a/src/EFCore/Query/ExpressionEqualityComparer.cs b/src/EFCore/Query/ExpressionEqualityComparer.cs index 1db3c2aa588..9919e628ec1 100644 --- a/src/EFCore/Query/ExpressionEqualityComparer.cs +++ b/src/EFCore/Query/ExpressionEqualityComparer.cs @@ -72,10 +72,8 @@ public int GetHashCode(Expression obj) case null: break; - case IStructuralEquatable structuralEquatable: - hash.Add(structuralEquatable.GetHashCode(StructuralComparisons.StructuralEqualityComparer)); - break; - + // Enumerables are compared element-wise, so they must also be hashed element-wise, regardless + // of their concrete type (e.g. an array and a list containing the same elements). case IEnumerable enumerable: foreach (var item in enumerable) { @@ -84,6 +82,10 @@ public int GetHashCode(Expression obj) break; + case IStructuralEquatable structuralEquatable: + hash.Add(structuralEquatable.GetHashCode(StructuralComparisons.StructuralEqualityComparer)); + break; + default: hash.Add(constantExpression.Value); break; @@ -358,17 +360,30 @@ private static bool CompareConstant(ConstantExpression a, ConstantExpression b) { var (v1, v2) = (a.Value, b.Value); - return Equals(v1, v2) - || v1 switch - { - IStructuralEquatable structuralEquatable1 - => structuralEquatable1.Equals(v2, StructuralComparisons.StructuralEqualityComparer), + if (Equals(v1, v2)) + { + return true; + } - IEnumerable enumerable1 when v2 is IEnumerable enumerable2 - => enumerable1.Cast().SequenceEqual(enumerable2.Cast()), + // Queryables are excluded from the hash code, so they can only be equal by reference (checked above). + // They must also not be compared element-wise here, since enumerating them can execute a query. + if (v1 is IQueryable || v2 is IQueryable) + { + return false; + } - _ => false - }; + // Both values are dispatched on, so that the comparison stays symmetric for values of different types + // (e.g. an array and a list containing the same elements). + return (v1, v2) switch + { + (IEnumerable enumerable1, IEnumerable enumerable2) + => enumerable1.Cast().SequenceEqual(enumerable2.Cast()), + + (IStructuralEquatable structuralEquatable1, not null) + => structuralEquatable1.Equals(v2, StructuralComparisons.StructuralEqualityComparer), + + _ => false + }; } private bool CompareGoto(GotoExpression a, GotoExpression b) diff --git a/test/EFCore.Tests/Query/ExpressionEqualityComparerTest.cs b/test/EFCore.Tests/Query/ExpressionEqualityComparerTest.cs index 129f6945dc2..13baccd5552 100644 --- a/test/EFCore.Tests/Query/ExpressionEqualityComparerTest.cs +++ b/test/EFCore.Tests/Query/ExpressionEqualityComparerTest.cs @@ -107,6 +107,31 @@ public void Array_constant_expressions_are_compared_correctly() Assert.NotEqual(expressionComparer.GetHashCode(e1), expressionComparer.GetHashCode(e3)); } + [Fact] + public void Collection_constant_expressions_of_different_types_are_compared_symmetrically() + { + var expressionComparer = ExpressionEqualityComparer.Instance; + + var array = Constant(new[] { 1, 2, 3 }, typeof(IEnumerable)); + var list = Constant(new List { 1, 2, 3 }, typeof(IEnumerable)); + + Assert.True(expressionComparer.Equals(array, list)); + Assert.True(expressionComparer.Equals(list, array)); + + Assert.Equal(expressionComparer.GetHashCode(array), expressionComparer.GetHashCode(list)); + } + + [Fact] + public void Queryable_constant_expressions_are_not_compared_by_their_elements() + { + var expressionComparer = ExpressionEqualityComparer.Instance; + + var queryable = new[] { 1, 2, 3 }.AsQueryable(); + + Assert.True(expressionComparer.Equals(Constant(queryable), Constant(queryable))); + Assert.False(expressionComparer.Equals(Constant(queryable), Constant(new[] { 1, 2, 3 }.AsQueryable()))); + } + [Fact] // #30697 public void Lambda_parameters_names_are_taken_into_account() {