Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 28 additions & 13 deletions src/EFCore/Query/ExpressionEqualityComparer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand All @@ -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;
Expand Down Expand Up @@ -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<object?>().SequenceEqual(enumerable2.Cast<object?>()),
// 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<object?>().SequenceEqual(enumerable2.Cast<object?>()),

(IStructuralEquatable structuralEquatable1, not null)
=> structuralEquatable1.Equals(v2, StructuralComparisons.StructuralEqualityComparer),

_ => false
};
}

private bool CompareGoto(GotoExpression a, GotoExpression b)
Expand Down
25 changes: 25 additions & 0 deletions test/EFCore.Tests/Query/ExpressionEqualityComparerTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<int>));
var list = Constant(new List<int> { 1, 2, 3 }, typeof(IEnumerable<int>));

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()
{
Expand Down
Loading