diff --git a/src/EFCore/Query/Internal/NullCheckRemovingExpressionVisitor.cs b/src/EFCore/Query/Internal/NullCheckRemovingExpressionVisitor.cs
index 3cc7bcd804d..f07c6e65f7b 100644
--- a/src/EFCore/Query/Internal/NullCheckRemovingExpressionVisitor.cs
+++ b/src/EFCore/Query/Internal/NullCheckRemovingExpressionVisitor.cs
@@ -18,6 +18,19 @@ public class NullCheckRemovingExpressionVisitor : ExpressionVisitor
private readonly NullSafeAccessVerifyingExpressionVisitor _nullSafeAccessVerifyingExpressionVisitor
= new NullSafeAccessVerifyingExpressionVisitor();
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ protected override Expression VisitBinary(BinaryExpression binaryExpression)
+ {
+ var visitedExpression = base.VisitBinary(binaryExpression);
+
+ return TryOptimizeConditionalEquality(visitedExpression) ?? visitedExpression;
+ }
+
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
@@ -60,6 +73,42 @@ protected override Expression VisitConditional(ConditionalExpression conditional
return base.VisitConditional(conditionalExpression);
}
+ private Expression TryOptimizeConditionalEquality(Expression expression)
+ {
+ // Simplify (a ? b : null) == null => a
+ // Simplify (a ? null : c) == null => !a
+ if (expression is BinaryExpression binaryExpression
+ && binaryExpression.NodeType == ExpressionType.Equal
+ && (binaryExpression.Left is ConditionalExpression
+ || binaryExpression.Right is ConditionalExpression))
+ {
+ Expression comparedExpression;
+ if (binaryExpression.Left is ConditionalExpression conditionalExpression)
+ {
+ comparedExpression = binaryExpression.Right;
+ }
+ else
+ {
+ conditionalExpression = binaryExpression.Right as ConditionalExpression;
+ comparedExpression = binaryExpression.Left;
+ }
+
+ if (conditionalExpression.IfTrue.IsNullConstantExpression()
+ && comparedExpression.IsNullConstantExpression())
+ {
+ return conditionalExpression.Test;
+ }
+
+ if (conditionalExpression.IfFalse.IsNullConstantExpression()
+ && comparedExpression.IsNullConstantExpression())
+ {
+ return Expression.Not(conditionalExpression.Test);
+ }
+ }
+
+ return null;
+ }
+
private sealed class NullSafeAccessVerifyingExpressionVisitor : ExpressionVisitor
{
private readonly ISet _nullSafeAccesses = new HashSet(ExpressionEqualityComparer.Instance);
diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindMiscellaneousQueryCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindMiscellaneousQueryCosmosTest.cs
index 3af01b1b23e..9a05efea82c 100644
--- a/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindMiscellaneousQueryCosmosTest.cs
+++ b/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindMiscellaneousQueryCosmosTest.cs
@@ -4178,6 +4178,12 @@ public override Task DefaultIfEmpty_Sum_over_collection_navigation(bool async)
return base.DefaultIfEmpty_Sum_over_collection_navigation(async);
}
+ [ConditionalTheory(Skip = "Non embedded collection subquery Issue#17246")]
+ public override Task Entity_equality_on_subquery_with_null_check(bool async)
+ {
+ return base.Entity_equality_on_subquery_with_null_check(async);
+ }
+
private void AssertSql(params string[] expected)
=> Fixture.TestSqlLoggerFactory.AssertBaseline(expected);
diff --git a/test/EFCore.InMemory.FunctionalTests/Query/GearsOfWarQueryInMemoryTest.cs b/test/EFCore.InMemory.FunctionalTests/Query/GearsOfWarQueryInMemoryTest.cs
index 6ea8401ca47..cb2c0799ee0 100644
--- a/test/EFCore.InMemory.FunctionalTests/Query/GearsOfWarQueryInMemoryTest.cs
+++ b/test/EFCore.InMemory.FunctionalTests/Query/GearsOfWarQueryInMemoryTest.cs
@@ -27,10 +27,6 @@ public override Task Client_side_equality_with_parameter_works_with_optional_nav
public override Task Where_coalesce_with_anonymous_types(bool async)
=> base.Where_coalesce_with_anonymous_types(async);
- [ConditionalTheory(Skip = "issue #17386")]
- public override Task Where_conditional_with_anonymous_type(bool async)
- => base.Where_conditional_with_anonymous_type(async);
-
[ConditionalTheory(Skip = "issue #17386")]
public override Task GetValueOrDefault_on_DateTimeOffset(bool async)
=> base.GetValueOrDefault_on_DateTimeOffset(async);
diff --git a/test/EFCore.Specification.Tests/Query/GearsOfWarQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/GearsOfWarQueryTestBase.cs
index a8efc03c05c..f05bf046491 100644
--- a/test/EFCore.Specification.Tests/Query/GearsOfWarQueryTestBase.cs
+++ b/test/EFCore.Specification.Tests/Query/GearsOfWarQueryTestBase.cs
@@ -1192,17 +1192,16 @@ orderby g.Nickname
[MemberData(nameof(IsAsyncData))]
public virtual Task Where_conditional_with_anonymous_type(bool async)
{
- return AssertTranslationFailed(
- () => AssertQuery(
- async,
- ss => from g in ss.Set()
- orderby g.Nickname
- where (g.LeaderNickname != null
- ? new { g.HasSoulPatch }
- : null)
- == null
- select g.Nickname,
- assertOrder: true));
+ return AssertQuery(
+ async,
+ ss => from g in ss.Set()
+ orderby g.Nickname
+ where (g.LeaderNickname != null
+ ? new { g.HasSoulPatch }
+ : null)
+ == null
+ select g.Nickname,
+ assertOrder: true);
}
[ConditionalTheory]
diff --git a/test/EFCore.Specification.Tests/Query/NorthwindMiscellaneousQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/NorthwindMiscellaneousQueryTestBase.cs
index 22252b9fc89..23c5e49c633 100644
--- a/test/EFCore.Specification.Tests/Query/NorthwindMiscellaneousQueryTestBase.cs
+++ b/test/EFCore.Specification.Tests/Query/NorthwindMiscellaneousQueryTestBase.cs
@@ -794,7 +794,8 @@ public virtual Task Ternary_should_not_evaluate_both_sides_with_parameter(bool a
o => new
{
// ReSharper disable SimplifyConditionalTernaryExpression
- Data1 = param != null ? o.OrderDate == param.Value : true, Data2 = param == null ? true : o.OrderDate == param.Value
+ Data1 = param != null ? o.OrderDate == param.Value : true,
+ Data2 = param == null ? true : o.OrderDate == param.Value
// ReSharper restore SimplifyConditionalTernaryExpression
}));
}
@@ -3441,7 +3442,7 @@ public virtual void Select_Where_Subquery_Equality()
using var context = CreateContext();
var orders
= (from o in context.Orders.OrderBy(o => o.OrderID).Take(1)
- // ReSharper disable once UseMethodAny.0
+ // ReSharper disable once UseMethodAny.0
where (from od in context.OrderDetails.OrderBy(od => od.OrderID).Take(2)
where (from c in context.Set()
where c.CustomerID == o.CustomerID
@@ -6241,5 +6242,23 @@ public virtual Task DefaultIfEmpty_Sum_over_collection_navigation(bool async)
.Select(c => new { c.CustomerID, Sum = c.Orders.Select(o => o.OrderID).DefaultIfEmpty().Sum() }),
elementSorter: c => c.CustomerID);
}
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Entity_equality_on_subquery_with_null_check(bool async)
+ {
+ return AssertQuery(
+ async,
+ ss => ss.Set()
+ .Select(c => new
+ {
+ c.CustomerID,
+ Order = (c.Orders.Any() ? c.Orders.FirstOrDefault() : null) == null
+ ? null
+ : new { c.Orders.FirstOrDefault().OrderDate }
+ }),
+ elementSorter: c => c.CustomerID,
+ elementAsserter: (e, a) => AssertEqual(e.Order, a.Order));
+ }
}
}
diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerTest.cs
index 52a9f50bd30..cd4f2bbfed6 100644
--- a/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerTest.cs
+++ b/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerTest.cs
@@ -1119,6 +1119,17 @@ FROM [Gears] AS [g]
ORDER BY [g].[Nickname]");
}
+ public override async Task Where_conditional_with_anonymous_type(bool async)
+ {
+ await base.Where_conditional_with_anonymous_type(async);
+
+ AssertSql(
+ @"SELECT [g].[Nickname]
+FROM [Gears] AS [g]
+WHERE [g].[LeaderNickname] IS NULL
+ORDER BY [g].[Nickname]");
+ }
+
public override async Task Select_coalesce_with_anonymous_types(bool async)
{
await base.Select_coalesce_with_anonymous_types(async);
diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindMiscellaneousQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindMiscellaneousQuerySqlServerTest.cs
index 652a3a5849a..d7448ff7117 100644
--- a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindMiscellaneousQuerySqlServerTest.cs
+++ b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindMiscellaneousQuerySqlServerTest.cs
@@ -5202,6 +5202,24 @@ FROM [Orders] AS [o]
FROM [Customers] AS [c]");
}
+ public override async Task Entity_equality_on_subquery_with_null_check(bool async)
+ {
+ await base.Entity_equality_on_subquery_with_null_check(async);
+
+ AssertSql(
+ @"SELECT [c].[CustomerID], CASE
+ WHEN NOT (EXISTS (
+ SELECT 1
+ FROM [Orders] AS [o]
+ WHERE [c].[CustomerID] = [o].[CustomerID])) THEN CAST(1 AS bit)
+ ELSE CAST(0 AS bit)
+END, (
+ SELECT TOP(1) [o0].[OrderDate]
+ FROM [Orders] AS [o0]
+ WHERE [c].[CustomerID] = [o0].[CustomerID])
+FROM [Customers] AS [c]");
+ }
+
private void AssertSql(params string[] expected)
=> Fixture.TestSqlLoggerFactory.AssertBaseline(expected);
diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/TPTGearsOfWarQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/TPTGearsOfWarQuerySqlServerTest.cs
index 439ff8e286d..b78995573be 100644
--- a/test/EFCore.SqlServer.FunctionalTests/Query/TPTGearsOfWarQuerySqlServerTest.cs
+++ b/test/EFCore.SqlServer.FunctionalTests/Query/TPTGearsOfWarQuerySqlServerTest.cs
@@ -1363,6 +1363,17 @@ FROM [Gears] AS [g]
ORDER BY [g].[Nickname]");
}
+ public override async Task Where_conditional_with_anonymous_type(bool async)
+ {
+ await base.Where_conditional_with_anonymous_type(async);
+
+ AssertSql(
+ @"SELECT [g].[Nickname]
+FROM [Gears] AS [g]
+WHERE [g].[LeaderNickname] IS NULL
+ORDER BY [g].[Nickname]");
+ }
+
public override async Task Select_coalesce_with_anonymous_types(bool async)
{
await base.Select_coalesce_with_anonymous_types(async);