diff --git a/src/EFCore.SqlServer/Query/Internal/SearchConditionConverter.cs b/src/EFCore.SqlServer/Query/Internal/SearchConditionConverter.cs
index 5a27b5e09a2..6d0d2ac69b9 100644
--- a/src/EFCore.SqlServer/Query/Internal/SearchConditionConverter.cs
+++ b/src/EFCore.SqlServer/Query/Internal/SearchConditionConverter.cs
@@ -24,6 +24,17 @@ namespace Microsoft.EntityFrameworkCore.SqlServer.Query.Internal;
///
public class SearchConditionConverter(ISqlExpressionFactory sqlExpressionFactory) : ExpressionVisitor
{
+ ///
+ /// Tracks whether we're in the larger context of a predicate, even the immediate context is not a search condition.
+ ///
+ ///
+ /// This visitor tracks whether the immediate context is a search condition by passing a boolean flag down during visitation,
+ /// and making adjustments so that the SQL is correct. However, in some cases it's useful to know whether we're in the
+ /// larger context of a predicate - even if the immediate context isn't a search condition; we use this to make sure the
+ /// SQL is efficient (as opposed to correct), refraining from transformations which could prevent index usage.
+ ///
+ private bool _inLargerPredicateContext;
+
///
/// 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
@@ -42,7 +53,14 @@ public class SearchConditionConverter(ISqlExpressionFactory sqlExpressionFactory
///
[return: NotNullIfNotNull(nameof(expression))]
protected virtual Expression? Visit(Expression? expression, bool inSearchConditionContext)
- => expression switch
+ {
+ var parentOptimizeForPredicateContext = _inLargerPredicateContext;
+ if (inSearchConditionContext)
+ {
+ _inLargerPredicateContext = true;
+ }
+
+ var result = expression switch
{
CaseExpression e => VisitCase(e, inSearchConditionContext),
SelectExpression e => VisitSelect(e),
@@ -62,6 +80,10 @@ SqlExpression e and
_ => base.Visit(expression)
};
+ _inLargerPredicateContext = parentOptimizeForPredicateContext;
+ return result;
+ }
+
private SqlExpression ApplyConversion(SqlExpression sqlExpression, bool inSearchConditionContext, bool isExpressionSearchCondition)
=> (inSearchCondition: inSearchConditionContext, isExpressionSearchCondition) switch
{
@@ -178,6 +200,9 @@ protected virtual Expression VisitPredicateJoin(PredicateJoinExpressionBase join
///
protected virtual Expression VisitSelect(SelectExpression select)
{
+ var parentOptimizeForPredicateContext = _inLargerPredicateContext;
+ _inLargerPredicateContext = false;
+
var tables = this.VisitAndConvert(select.Tables);
var predicate = (SqlExpression?)Visit(select.Predicate, inSearchConditionContext: true);
var groupBy = this.VisitAndConvert(select.GroupBy);
@@ -187,6 +212,8 @@ protected virtual Expression VisitSelect(SelectExpression select)
var offset = (SqlExpression?)Visit(select.Offset);
var limit = (SqlExpression?)Visit(select.Limit);
+ _inLargerPredicateContext = parentOptimizeForPredicateContext;
+
return select.Update(tables, predicate, groupBy, havingExpression, projections, orderings, offset, limit);
}
@@ -208,7 +235,14 @@ protected virtual Expression VisitSqlBinary(SqlBinaryExpression binary, bool inS
{
var leftType = newLeft.TypeMapping?.Converter?.ProviderClrType ?? newLeft.Type;
var rightType = newRight.TypeMapping?.Converter?.ProviderClrType ?? newRight.Type;
- if (!inSearchConditionContext
+
+ // Transform equality and inequality over bool/integer to bitwise operators:
+ // x != y => CAST(x ^ y AS BIT)
+ // x == y => CAST(~(x ^ y) AS BIT)
+ // However, refrain from doing this if we're within a predicate - even if the immediate context is not a search condition -
+ // since this might prevent index usage (e.g. we don't want this to happen within CASE THEN clauses, which aren't search
+ // conditions but are seen through by SQL Server when in the WHERE clause - see #36291).
+ if (!_inLargerPredicateContext
&& (leftType == typeof(bool) || leftType.IsInteger())
&& (rightType == typeof(bool) || rightType.IsInteger()))
{
diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindWhereQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindWhereQuerySqlServerTest.cs
index bd0137646c4..9cc830a069f 100644
--- a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindWhereQuerySqlServerTest.cs
+++ b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindWhereQuerySqlServerTest.cs
@@ -1101,9 +1101,12 @@ public override async Task Where_bool_member_and_parameter_compared_to_binary_ex
SELECT [p].[ProductID], [p].[Discontinued], [p].[ProductName], [p].[SupplierID], [p].[UnitPrice], [p].[UnitsInStock]
FROM [Products] AS [p]
WHERE [p].[Discontinued] = CASE
- WHEN [p].[ProductID] > 50 THEN CAST(1 AS bit)
+ WHEN CASE
+ WHEN [p].[ProductID] > 50 THEN CAST(1 AS bit)
+ ELSE CAST(0 AS bit)
+ END <> @prm THEN CAST(1 AS bit)
ELSE CAST(0 AS bit)
-END ^ @prm
+END
""");
}
diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NullSemanticsQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NullSemanticsQuerySqlServerTest.cs
index 8e5143af447..14d5da01cd5 100644
--- a/test/EFCore.SqlServer.FunctionalTests/Query/NullSemanticsQuerySqlServerTest.cs
+++ b/test/EFCore.SqlServer.FunctionalTests/Query/NullSemanticsQuerySqlServerTest.cs
@@ -2466,7 +2466,13 @@ public override async Task Compare_complex_equal_equal_equal(bool async)
"""
SELECT [e].[Id]
FROM [Entities1] AS [e]
-WHERE [e].[BoolA] ^ [e].[BoolB] = CAST([e].[IntA] ^ [e].[IntB] AS bit)
+WHERE CASE
+ WHEN [e].[BoolA] = [e].[BoolB] THEN CAST(1 AS bit)
+ ELSE CAST(0 AS bit)
+END = CASE
+ WHEN [e].[IntA] = [e].[IntB] THEN CAST(1 AS bit)
+ ELSE CAST(0 AS bit)
+END
""",
//
"""
@@ -2502,7 +2508,13 @@ public override async Task Compare_complex_equal_not_equal_equal(bool async)
"""
SELECT [e].[Id]
FROM [Entities1] AS [e]
-WHERE [e].[BoolA] ^ [e].[BoolB] <> CAST([e].[IntA] ^ [e].[IntB] AS bit)
+WHERE CASE
+ WHEN [e].[BoolA] = [e].[BoolB] THEN CAST(1 AS bit)
+ ELSE CAST(0 AS bit)
+END <> CASE
+ WHEN [e].[IntA] = [e].[IntB] THEN CAST(1 AS bit)
+ ELSE CAST(0 AS bit)
+END
""",
//
"""
@@ -2538,7 +2550,13 @@ public override async Task Compare_complex_not_equal_equal_equal(bool async)
"""
SELECT [e].[Id]
FROM [Entities1] AS [e]
-WHERE [e].[BoolA] ^ [e].[BoolB] = ~CAST([e].[IntA] ^ [e].[IntB] AS bit)
+WHERE CASE
+ WHEN [e].[BoolA] <> [e].[BoolB] THEN CAST(1 AS bit)
+ ELSE CAST(0 AS bit)
+END = CASE
+ WHEN [e].[IntA] = [e].[IntB] THEN CAST(1 AS bit)
+ ELSE CAST(0 AS bit)
+END
""",
//
"""
@@ -2574,7 +2592,13 @@ public override async Task Compare_complex_not_equal_not_equal_equal(bool async)
"""
SELECT [e].[Id]
FROM [Entities1] AS [e]
-WHERE [e].[BoolA] ^ [e].[BoolB] <> ~CAST([e].[IntA] ^ [e].[IntB] AS bit)
+WHERE CASE
+ WHEN [e].[BoolA] <> [e].[BoolB] THEN CAST(1 AS bit)
+ ELSE CAST(0 AS bit)
+END <> CASE
+ WHEN [e].[IntA] = [e].[IntB] THEN CAST(1 AS bit)
+ ELSE CAST(0 AS bit)
+END
""",
//
"""
@@ -2610,7 +2634,13 @@ public override async Task Compare_complex_not_equal_equal_not_equal(bool async)
"""
SELECT [e].[Id]
FROM [Entities1] AS [e]
-WHERE [e].[BoolA] ^ [e].[BoolB] = CAST([e].[IntA] ^ [e].[IntB] AS bit)
+WHERE CASE
+ WHEN [e].[BoolA] <> [e].[BoolB] THEN CAST(1 AS bit)
+ ELSE CAST(0 AS bit)
+END = CASE
+ WHEN [e].[IntA] <> [e].[IntB] THEN CAST(1 AS bit)
+ ELSE CAST(0 AS bit)
+END
""",
//
"""
@@ -2646,7 +2676,13 @@ public override async Task Compare_complex_not_equal_not_equal_not_equal(bool as
"""
SELECT [e].[Id]
FROM [Entities1] AS [e]
-WHERE [e].[BoolA] ^ [e].[BoolB] <> CAST([e].[IntA] ^ [e].[IntB] AS bit)
+WHERE CASE
+ WHEN [e].[BoolA] <> [e].[BoolB] THEN CAST(1 AS bit)
+ ELSE CAST(0 AS bit)
+END <> CASE
+ WHEN [e].[IntA] <> [e].[IntB] THEN CAST(1 AS bit)
+ ELSE CAST(0 AS bit)
+END
""",
//
"""
@@ -4934,7 +4970,10 @@ public override async Task Comparison_compared_to_null_check_on_bool(bool async)
"""
SELECT [e].[Id], [e].[BoolA], [e].[BoolB], [e].[BoolC], [e].[IntA], [e].[IntB], [e].[IntC], [e].[NullableBoolA], [e].[NullableBoolB], [e].[NullableBoolC], [e].[NullableIntA], [e].[NullableIntB], [e].[NullableIntC], [e].[NullableStringA], [e].[NullableStringB], [e].[NullableStringC], [e].[StringA], [e].[StringB], [e].[StringC]
FROM [Entities1] AS [e]
-WHERE ~CAST([e].[IntA] ^ [e].[IntB] AS bit) <> CASE
+WHERE CASE
+ WHEN [e].[IntA] = [e].[IntB] THEN CAST(1 AS bit)
+ ELSE CAST(0 AS bit)
+END <> CASE
WHEN [e].[NullableBoolA] IS NOT NULL THEN CAST(1 AS bit)
ELSE CAST(0 AS bit)
END
@@ -4943,7 +4982,10 @@ ELSE CAST(0 AS bit)
"""
SELECT [e].[Id], [e].[BoolA], [e].[BoolB], [e].[BoolC], [e].[IntA], [e].[IntB], [e].[IntC], [e].[NullableBoolA], [e].[NullableBoolB], [e].[NullableBoolC], [e].[NullableIntA], [e].[NullableIntB], [e].[NullableIntC], [e].[NullableStringA], [e].[NullableStringB], [e].[NullableStringC], [e].[StringA], [e].[StringB], [e].[StringC]
FROM [Entities1] AS [e]
-WHERE CAST([e].[IntA] ^ [e].[IntB] AS bit) = CASE
+WHERE CASE
+ WHEN [e].[IntA] <> [e].[IntB] THEN CAST(1 AS bit)
+ ELSE CAST(0 AS bit)
+END = CASE
WHEN [e].[NullableBoolA] IS NOT NULL THEN CAST(1 AS bit)
ELSE CAST(0 AS bit)
END
@@ -5087,7 +5129,10 @@ public override async Task Is_null_on_column_followed_by_OrElse_optimizes_nullab
SELECT [e].[Id], [e].[BoolA], [e].[BoolB], [e].[BoolC], [e].[IntA], [e].[IntB], [e].[IntC], [e].[NullableBoolA], [e].[NullableBoolB], [e].[NullableBoolC], [e].[NullableIntA], [e].[NullableIntB], [e].[NullableIntC], [e].[NullableStringA], [e].[NullableStringB], [e].[NullableStringC], [e].[StringA], [e].[StringB], [e].[StringC]
FROM [Entities1] AS [e]
WHERE CASE
- WHEN [e].[NullableBoolA] IS NULL THEN ~([e].[BoolA] ^ [e].[BoolB])
+ WHEN [e].[NullableBoolA] IS NULL THEN CASE
+ WHEN [e].[BoolA] = [e].[BoolB] THEN CAST(1 AS bit)
+ ELSE CAST(0 AS bit)
+ END
WHEN [e].[NullableBoolC] IS NULL THEN CASE
WHEN ([e].[NullableBoolA] <> [e].[NullableBoolC] OR [e].[NullableBoolA] IS NULL OR [e].[NullableBoolC] IS NULL) AND ([e].[NullableBoolA] IS NOT NULL OR [e].[NullableBoolC] IS NOT NULL) THEN CAST(1 AS bit)
ELSE CAST(0 AS bit)
diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/Translations/Operators/BitwiseOperatorTranslationsSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/Translations/Operators/BitwiseOperatorTranslationsSqlServerTest.cs
index 5ccc529caf9..0804fcdd2c5 100644
--- a/test/EFCore.SqlServer.FunctionalTests/Query/Translations/Operators/BitwiseOperatorTranslationsSqlServerTest.cs
+++ b/test/EFCore.SqlServer.FunctionalTests/Query/Translations/Operators/BitwiseOperatorTranslationsSqlServerTest.cs
@@ -123,7 +123,10 @@ public override async Task Xor_over_boolean()
"""
SELECT [b].[Id], [b].[Bool], [b].[Byte], [b].[ByteArray], [b].[DateOnly], [b].[DateTime], [b].[DateTimeOffset], [b].[Decimal], [b].[Double], [b].[Enum], [b].[FlagsEnum], [b].[Float], [b].[Guid], [b].[Int], [b].[Long], [b].[Short], [b].[String], [b].[TimeOnly], [b].[TimeSpan]
FROM [BasicTypesEntities] AS [b]
-WHERE ~CAST([b].[Int] ^ [b].[Short] AS bit) ^ CASE
+WHERE CASE
+ WHEN [b].[Int] = [b].[Short] THEN CAST(1 AS bit)
+ ELSE CAST(0 AS bit)
+END ^ CASE
WHEN [b].[String] = N'Seattle' THEN CAST(1 AS bit)
ELSE CAST(0 AS bit)
END = CAST(1 AS bit)