From c41406cc9cc168357e45924dfd7992d06f854242 Mon Sep 17 00:00:00 2001 From: BurakBebek1 Date: Sat, 31 Jan 2026 19:25:35 +0300 Subject: [PATCH 1/2] Fix #37494: Handle null values in IN expressions when using OPENJSON on SQL Server --- .../SqlServerSqlNullabilityProcessor.cs | 75 ++++++++++++------- .../PrimitiveCollectionsQuerySqlServerTest.cs | 22 +++++- 2 files changed, 67 insertions(+), 30 deletions(-) diff --git a/src/EFCore.SqlServer/Query/Internal/SqlServerSqlNullabilityProcessor.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerSqlNullabilityProcessor.cs index 372d6ba06d0..4ccc0e207d9 100644 --- a/src/EFCore.SqlServer/Query/Internal/SqlServerSqlNullabilityProcessor.cs +++ b/src/EFCore.SqlServer/Query/Internal/SqlServerSqlNullabilityProcessor.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Collections; @@ -28,6 +28,7 @@ public class SqlServerSqlNullabilityProcessor : SqlNullabilityProcessor public const string OpenJsonParameterTableName = "__openjson"; private readonly ISqlServerSingletonOptions _sqlServerSingletonOptions; + private readonly ISqlExpressionFactory _sqlExpressionFactory; private int _openJsonAliasCounter; private int _totalParameterCount; @@ -39,11 +40,14 @@ public class SqlServerSqlNullabilityProcessor : SqlNullabilityProcessor /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public SqlServerSqlNullabilityProcessor( - RelationalParameterBasedSqlProcessorDependencies dependencies, - RelationalParameterBasedSqlProcessorParameters parameters, - ISqlServerSingletonOptions sqlServerSingletonOptions) - : base(dependencies, parameters) - => _sqlServerSingletonOptions = sqlServerSingletonOptions; + RelationalParameterBasedSqlProcessorDependencies dependencies, + RelationalParameterBasedSqlProcessorParameters parameters, + ISqlServerSingletonOptions sqlServerSingletonOptions) + : base(dependencies, parameters) + { + _sqlServerSingletonOptions = sqlServerSingletonOptions; + _sqlExpressionFactory = dependencies.SqlExpressionFactory; + } /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -248,6 +252,7 @@ protected override SqlExpression VisitIn(InExpression inExpression, bool allowOp { Check.DebugAssert(valuesParameter.TypeMapping is not null); Check.DebugAssert(valuesParameter.TypeMapping.ElementTypeMapping is not null); + var elementTypeMapping = (RelationalTypeMapping)valuesParameter.TypeMapping.ElementTypeMapping; if (TryHandleOverLimitParameters( @@ -258,30 +263,42 @@ protected override SqlExpression VisitIn(InExpression inExpression, bool allowOp out var constants, out var containsNulls)) { - inExpression = (openJson, constants) switch + if (openJson != null) { - (not null, null) - => inExpression.Update( - inExpression.Item, - SelectExpression.CreateImmutable( - null!, - [openJson], - [ - new ProjectionExpression( - new ColumnExpression( - "value", - openJson.Alias, - valuesParameter.Type.GetSequenceType(), - elementTypeMapping, - containsNulls!.Value), - "value") - ], - null!)), - - (null, not null) => inExpression.Update(inExpression.Item, constants), - - _ => throw new UnreachableException(), - }; + var column = new ColumnExpression( + "value", + openJson.Alias, + valuesParameter.Type.GetSequenceType().UnwrapNullableType(), + elementTypeMapping, + containsNulls!.Value); + + var subquery = SelectExpression.CreateImmutable( + null!, + [openJson], + [new ProjectionExpression(column, "value")], + null!); + + nullable = false; + + var translatedIn = inExpression.Update(inExpression.Item, subquery); + + if (containsNulls.GetValueOrDefault()) + { + return _sqlExpressionFactory.OrElse( + translatedIn, + _sqlExpressionFactory.IsNull(inExpression.Item)); + } + + return translatedIn; + } + + if (constants != null) + { + nullable = false; + return inExpression.Update(inExpression.Item, constants); + } + + throw new UnreachableException("TryHandleOverLimitParameters should return either openJson or constants."); } return base.VisitIn(inExpression, allowOptimizedExpansion, out nullable); diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerTest.cs index 8824c821189..e6b76455bf1 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerTest.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. namespace Microsoft.EntityFrameworkCore.Query; @@ -2568,6 +2568,26 @@ SELECT COUNT(*) """); } + [ConditionalFact] + public virtual async Task Parameter_collection_with_null_value_Contains_null_element_Real_Check() + { + using var context = Fixture.CreateContext(); + + var values = Enumerable.Range(1, 2200).Select(i => (int?)i).ToList(); + values.Add(null); + + var queryResults = await context.Set() + .Where(e => values.Contains(e.NullableInt)) + .ToListAsync(); + + var sql = Fixture.TestSqlLoggerFactory.SqlStatements.Last(); + + Assert.NotEmpty(queryResults); + Assert.Contains(queryResults, e => e.NullableInt == null); + Assert.Contains("OPENJSON", sql); + Assert.Contains("IS NULL", sql); + } + [ConditionalFact] public virtual async Task Parameter_collection_of_ints_Contains_int_2071_values() { From 327b65d9ac99cef919715b09e9a32830813ac0aa Mon Sep 17 00:00:00 2001 From: Burak Bebek <40890619+BurakBebek1@users.noreply.github.com> Date: Wed, 4 Feb 2026 23:01:06 +0300 Subject: [PATCH 2/2] Update SqlServerSqlNullabilityProcessor.cs --- .../SqlServerSqlNullabilityProcessor.cs | 20 ++----------------- 1 file changed, 2 insertions(+), 18 deletions(-) diff --git a/src/EFCore.SqlServer/Query/Internal/SqlServerSqlNullabilityProcessor.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerSqlNullabilityProcessor.cs index 097e06a5a4f..a44d8389410 100644 --- a/src/EFCore.SqlServer/Query/Internal/SqlServerSqlNullabilityProcessor.cs +++ b/src/EFCore.SqlServer/Query/Internal/SqlServerSqlNullabilityProcessor.cs @@ -31,28 +31,12 @@ public class SqlServerSqlNullabilityProcessor( [EntityFrameworkInternal] public const string OpenJsonParameterTableName = "__openjson"; - private readonly ISqlServerSingletonOptions _sqlServerSingletonOptions; - private readonly ISqlExpressionFactory _sqlExpressionFactory; + private readonly ISqlServerSingletonOptions _sqlServerSingletonOptions = sqlServerSingletonOptions; + private readonly ISqlExpressionFactory _sqlExpressionFactory = dependencies.SqlExpressionFactory; private int _openJsonAliasCounter; private int _totalParameterCount; - /// - /// 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. - /// - public SqlServerSqlNullabilityProcessor( - RelationalParameterBasedSqlProcessorDependencies dependencies, - RelationalParameterBasedSqlProcessorParameters parameters, - ISqlServerSingletonOptions sqlServerSingletonOptions) - : base(dependencies, parameters) - { - _sqlServerSingletonOptions = sqlServerSingletonOptions; - _sqlExpressionFactory = dependencies.SqlExpressionFactory; - } - /// /// 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