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
Original file line number Diff line number Diff line change
Expand Up @@ -951,18 +951,32 @@ private SqlExpression CreateJoinPredicate(
: _sqlExpressionFactory.AndAlso(result, joinPredicate);
}

// In LINQ equijoins, null is not equal null, just like in SQL
// (https://learn.microsoft.com/dotnet/csharp/language-reference/keywords/join-clause#the-equals-operator)
// As a result, in SqlNullabilityProcessor.ProcessJoinPredicate(), we have special handling for an equality
// immediately inside a join predicate - we bypass null compensation for that, to make sure the SQL behavior
// matches the LINQ behavior.
// However, when two anonymous types are being compared, the LINQ behavior *does* treat nulls as equal; as a result, in
// SqlNullabilityProcessor.ProcessJoinPredicate() we differentiate between a single top-level comparison
// and multiple comparisons with ANDs.
// Unfortunately, when we have a an anonymous type with a single property (on new { Foo = x } equals new { Foo = y }),
// we produce the same predicate as the single comparison case (without an anonymous type), bypassing the null
// compensation and generating incorrect results.
// To work around this, we add an always-true predicate here, and the AND will cause
// SqlNullabilityProcessor.ProcessJoinPredicate() to go into the multiple-property anonymous type logic,
// and not bypass null compensation.
if (outerNew.Arguments.Count == 1)
{
result = _sqlExpressionFactory.AndAlso(
result!,
CreateJoinPredicate(Expression.Constant(true), Expression.Constant(true)));
}

return result!;
}
return result ?? _sqlExpressionFactory.Constant(true);

private SqlExpression CreateJoinPredicate(Expression outerKey, Expression innerKey)
=> TranslateExpression(Infrastructure.ExpressionExtensions.CreateEqualsExpression(outerKey, innerKey))!;
SqlExpression CreateJoinPredicate(Expression outerKey, Expression innerKey)
=> TranslateExpression(Infrastructure.ExpressionExtensions.CreateEqualsExpression(outerKey, innerKey))!;
}

/// <inheritdoc />
protected override ShapedQueryExpression? TranslateLastOrDefault(
Expand Down
10 changes: 10 additions & 0 deletions src/EFCore.Relational/Query/SqlNullabilityProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,13 @@ protected override Expression VisitExtension(Expression node)

SqlExpression ProcessJoinPredicate(SqlExpression predicate)
{
// In LINQ equijoins, null is not equal null, just like in SQL
// (https://learn.microsoft.com/dotnet/csharp/language-reference/keywords/join-clause#the-equals-operator).
// As a result, we handle top-level join predicate equality in a special way, not using the generic Visit logic
// (which would add null compensation).
// However, when two anonymous types are being compared, the LINQ behavior *does* treat nulls as equal.
// As a result, we differentiate between a single top-level comparison and multiple comparisons with ANDs.
// See additional notes in RelationalQueryableMethodTranslatingExpressionVisitor.CreateJoinPredicate().
switch (predicate)
{
case SqlBinaryExpression { OperatorType: ExpressionType.Equal } binary:
Expand Down Expand Up @@ -250,6 +257,9 @@ or ExpressionType.LessThanOrEqual
} binary:
return Visit(binary, allowOptimizedExpansion: true, out _);

case SqlConstantExpression { Value: bool }:
return predicate;

default:
throw new InvalidOperationException(
RelationalStrings.UnhandledExpressionInVisitor(predicate, predicate.GetType(), nameof(SqlNullabilityProcessor)));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2006,8 +2006,8 @@ public virtual Task Where_on_multilevel_reference_in_subquery_with_outer_project
.Take(10)
.Select(l3 => l3.Name));

[ConditionalTheory, MemberData(nameof(IsAsyncData))]
public virtual Task Join_condition_optimizations_applied_correctly_when_anonymous_type_with_single_property(bool async)
[ConditionalTheory, MemberData(nameof(IsAsyncData))] // #18808
public virtual Task Join_on_anonymous_type_with_single_property(bool async)
=> AssertQuery(
async,
ss => from l1 in ss.Set<Level1>()
Expand All @@ -2017,7 +2017,7 @@ join l2 in ss.Set<Level2>()
select l1);

[ConditionalTheory, MemberData(nameof(IsAsyncData))]
public virtual Task Join_condition_optimizations_applied_correctly_when_anonymous_type_with_multiple_properties(bool async)
public virtual Task Join_on_anonymous_type_with_multiple_properties(bool async)
=> AssertQuery(
async,
ss =>
Expand All @@ -2030,7 +2030,8 @@ join l2 in ss.Set<Level2>()
}
equals new
{
A = EF.Property<int?>(l2, "Level1_Optional_Id"), B = EF.Property<int?>(l2, "OneToMany_Optional_Self_Inverse2Id")
A = EF.Property<int?>(l2, "Level1_Optional_Id"),
B = EF.Property<int?>(l2, "OneToMany_Optional_Self_Inverse2Id")
}
select l1);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,13 @@ public abstract class ComplexNavigationsSharedTypeQueryTestBase<TFixture>(TFixtu
public override Task Join_navigation_self_ref(bool async)
=> AssertTranslationFailed(() => base.Join_navigation_self_ref(async));

public override Task Join_condition_optimizations_applied_correctly_when_anonymous_type_with_multiple_properties(bool async)
public override Task Join_on_anonymous_type_with_multiple_properties(bool async)
=> AssertUnableToTranslateEFProperty(()
=> base.Join_condition_optimizations_applied_correctly_when_anonymous_type_with_multiple_properties(async));
=> base.Join_on_anonymous_type_with_multiple_properties(async));

public override Task Join_condition_optimizations_applied_correctly_when_anonymous_type_with_single_property(bool async)
public override Task Join_on_anonymous_type_with_single_property(bool async)
=> AssertUnableToTranslateEFProperty(()
=> base.Join_condition_optimizations_applied_correctly_when_anonymous_type_with_single_property(async));
=> base.Join_on_anonymous_type_with_single_property(async));

public override Task Multiple_SelectMany_with_nested_navigations_and_explicit_DefaultIfEmpty_joined_together(bool async)
=> AssertTranslationFailed(()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2645,9 +2645,9 @@ OFFSET @p ROWS FETCH NEXT @p1 ROWS ONLY
""");
}

public override async Task Join_condition_optimizations_applied_correctly_when_anonymous_type_with_single_property(bool async)
public override async Task Join_on_anonymous_type_with_single_property(bool async)
{
await base.Join_condition_optimizations_applied_correctly_when_anonymous_type_with_single_property(async);
await base.Join_on_anonymous_type_with_single_property(async);

AssertSql(
"""
Expand All @@ -2657,9 +2657,9 @@ FROM [LevelOne] AS [l]
""");
}

public override async Task Join_condition_optimizations_applied_correctly_when_anonymous_type_with_multiple_properties(bool async)
public override async Task Join_on_anonymous_type_with_multiple_properties(bool async)
{
await base.Join_condition_optimizations_applied_correctly_when_anonymous_type_with_multiple_properties(async);
await base.Join_on_anonymous_type_with_multiple_properties(async);

AssertSql(
"""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2645,9 +2645,9 @@ OFFSET @p ROWS FETCH NEXT @p1 ROWS ONLY
""");
}

public override async Task Join_condition_optimizations_applied_correctly_when_anonymous_type_with_single_property(bool async)
public override async Task Join_on_anonymous_type_with_single_property(bool async)
{
await base.Join_condition_optimizations_applied_correctly_when_anonymous_type_with_single_property(async);
await base.Join_on_anonymous_type_with_single_property(async);

AssertSql(
"""
Expand All @@ -2657,9 +2657,9 @@ FROM [LevelOne] AS [l]
""");
}

public override async Task Join_condition_optimizations_applied_correctly_when_anonymous_type_with_multiple_properties(bool async)
public override async Task Join_on_anonymous_type_with_multiple_properties(bool async)
{
await base.Join_condition_optimizations_applied_correctly_when_anonymous_type_with_multiple_properties(async);
await base.Join_on_anonymous_type_with_multiple_properties(async);

AssertSql(
"""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1406,16 +1406,16 @@ public override async Task Join_navigation_self_ref(bool async)
AssertSql();
}

public override async Task Join_condition_optimizations_applied_correctly_when_anonymous_type_with_multiple_properties(bool async)
public override async Task Join_on_anonymous_type_with_multiple_properties(bool async)
{
await base.Join_condition_optimizations_applied_correctly_when_anonymous_type_with_multiple_properties(async);
await base.Join_on_anonymous_type_with_multiple_properties(async);

AssertSql();
}

public override async Task Join_condition_optimizations_applied_correctly_when_anonymous_type_with_single_property(bool async)
public override async Task Join_on_anonymous_type_with_single_property(bool async)
{
await base.Join_condition_optimizations_applied_correctly_when_anonymous_type_with_single_property(async);
await base.Join_on_anonymous_type_with_single_property(async);

AssertSql();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1408,16 +1408,16 @@ public override async Task Join_navigation_self_ref(bool async)
AssertSql();
}

public override async Task Join_condition_optimizations_applied_correctly_when_anonymous_type_with_multiple_properties(bool async)
public override async Task Join_on_anonymous_type_with_multiple_properties(bool async)
{
await base.Join_condition_optimizations_applied_correctly_when_anonymous_type_with_multiple_properties(async);
await base.Join_on_anonymous_type_with_multiple_properties(async);

AssertSql();
}

public override async Task Join_condition_optimizations_applied_correctly_when_anonymous_type_with_single_property(bool async)
public override async Task Join_on_anonymous_type_with_single_property(bool async)
{
await base.Join_condition_optimizations_applied_correctly_when_anonymous_type_with_single_property(async);
await base.Join_on_anonymous_type_with_single_property(async);

AssertSql();
}
Expand Down