diff --git a/src/EFCore.Relational/Query/SqlNullabilityProcessor.cs b/src/EFCore.Relational/Query/SqlNullabilityProcessor.cs index 93b12e4c483..bec7c0e3c29 100644 --- a/src/EFCore.Relational/Query/SqlNullabilityProcessor.cs +++ b/src/EFCore.Relational/Query/SqlNullabilityProcessor.cs @@ -1921,7 +1921,11 @@ protected virtual bool TryMakeNonNullable( { if (processedValues is null) { - var elementClrType = values.GetType().GetSequenceType(); + // We found the first null value - we need to start copying values to a new list which will be used for the rewritten parameter. + // The type of the new list must match that of the original enumerable parameter, as there may be value converters involved which + // rely on the precise element type (see #37605). We therefore get the type of the element from the original list if it implements + // IEnumerable, or default to object. + var elementClrType = enumerable.GetType().TryGetElementType(typeof(IEnumerable<>)) ?? typeof(object); processedValues = (IList)Activator.CreateInstance(typeof(List<>).MakeGenericType(elementClrType), values.Count)!; for (var j = 0; j < i; j++) { diff --git a/src/EFCore.SqlServer/Query/Internal/SqlServerSqlNullabilityProcessor.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerSqlNullabilityProcessor.cs index 7b180848e63..d364e55a0fb 100644 --- a/src/EFCore.SqlServer/Query/Internal/SqlServerSqlNullabilityProcessor.cs +++ b/src/EFCore.SqlServer/Query/Internal/SqlServerSqlNullabilityProcessor.cs @@ -36,7 +36,6 @@ public class SqlServerSqlNullabilityProcessor( 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 @@ -300,6 +299,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( @@ -325,7 +325,7 @@ protected override SqlExpression VisitIn(InExpression inExpression, bool allowOp new ColumnExpression( columnName, openJson.Alias, - valuesParameter.Type.GetSequenceType(), + valuesParameter.Type.GetSequenceType().UnwrapNullableType(), elementTypeMapping, containsNulls!.Value), columnName) diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/PrimitiveCollectionsQueryCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/PrimitiveCollectionsQueryCosmosTest.cs index 29a503642f2..159a308ed5c 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/PrimitiveCollectionsQueryCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/PrimitiveCollectionsQueryCosmosTest.cs @@ -713,6 +713,20 @@ WHERE NOT(ARRAY_CONTAINS(@nullableInts, c["NullableInt"])) """); } + public override async Task Parameter_collection_of_nullable_ints_Contains_nullable_int_with_EF_Parameter() + { + await base.Parameter_collection_of_nullable_ints_Contains_nullable_int_with_EF_Parameter(); + + AssertSql( + """ +@nullableInts='[null,999]' + +SELECT VALUE c +FROM root c +WHERE ARRAY_CONTAINS(@nullableInts, c["NullableInt"]) +"""); + } + public override async Task Parameter_collection_of_strings_Contains_string() { await base.Parameter_collection_of_strings_Contains_string(); diff --git a/test/EFCore.Specification.Tests/Query/PrimitiveCollectionsQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/PrimitiveCollectionsQueryTestBase.cs index f09570e1633..10d029ddf98 100644 --- a/test/EFCore.Specification.Tests/Query/PrimitiveCollectionsQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/PrimitiveCollectionsQueryTestBase.cs @@ -345,6 +345,16 @@ public virtual async Task Parameter_collection_of_nullable_ints_Contains_nullabl await AssertQuery(ss => ss.Set().Where(c => !nullableInts.Contains(c.NullableInt))); } + [ConditionalFact] // #37605 + public virtual async Task Parameter_collection_of_nullable_ints_Contains_nullable_int_with_EF_Parameter() + { + var nullableInts = new int?[] { null, 999 }; + + await AssertQuery( + ss => ss.Set().Where(c => EF.Parameter(nullableInts).Contains(c.NullableInt)), + ss => ss.Set().Where(c => nullableInts.Contains(c.NullableInt))); + } + [ConditionalFact] public virtual async Task Parameter_collection_of_structs_Contains_struct() { diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQueryOldSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQueryOldSqlServerTest.cs index cc2ac1642ef..315c75603fa 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQueryOldSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQueryOldSqlServerTest.cs @@ -695,6 +695,14 @@ WHERE [p].[NullableInt] IS NOT NULL AND [p].[NullableInt] <> @nullableInts1 """); } + public override async Task Parameter_collection_of_nullable_ints_Contains_nullable_int_with_EF_Parameter() + { + // EF.Parameter() on primitive collection (OPENJSON on SQL Server) not supported on old versions of SQL Server. + await Assert.ThrowsAsync(base.Parameter_collection_of_nullable_ints_Contains_nullable_int_with_EF_Parameter); + + AssertSql(); + } + public override async Task Parameter_collection_of_strings_Contains_string() { await base.Parameter_collection_of_strings_Contains_string(); @@ -1806,6 +1814,19 @@ SELECT COUNT(*) """); } + [ConditionalFact] // #37605 + public virtual async Task Parameter_collection_with_null_value_Contains_null_2201_values() + { + using var context = Fixture.CreateContext(); + + var values = Enumerable.Range(1, 2200).Select(i => (int?)i).ToList(); + values.Add(null); + + await AssertQuery(ss => ss.Set().Where(e => values.Contains(e.NullableInt))); + + // No SQL assertion as the SQL is huge + } + [ConditionalFact] public virtual void Check_all_tests_overridden() => TestHelpers.AssertAllMethodsOverridden(GetType()); diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServer160Test.cs b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServer160Test.cs index a99fb74283e..01f4775669a 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServer160Test.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServer160Test.cs @@ -703,6 +703,23 @@ WHERE [p].[NullableInt] IS NOT NULL AND [p].[NullableInt] <> @nullableInts1 """); } + public override async Task Parameter_collection_of_nullable_ints_Contains_nullable_int_with_EF_Parameter() + { + await base.Parameter_collection_of_nullable_ints_Contains_nullable_int_with_EF_Parameter(); + + AssertSql( + """ +@nullableInts_without_nulls='[999]' (Size = 4000) + +SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[NullableString], [p].[NullableStrings], [p].[NullableWrappedId], [p].[NullableWrappedIdWithNullableComparer], [p].[String], [p].[Strings], [p].[WrappedId] +FROM [PrimitiveCollectionsEntity] AS [p] +WHERE [p].[NullableInt] IN ( + SELECT [n].[value] + FROM OPENJSON(@nullableInts_without_nulls) AS [n] +) OR [p].[NullableInt] IS NULL +"""); + } + public override async Task Parameter_collection_of_strings_Contains_string() { await base.Parameter_collection_of_strings_Contains_string(); diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerJsonTypeTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerJsonTypeTest.cs index 27fe40e40aa..bd72ea86a27 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerJsonTypeTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerJsonTypeTest.cs @@ -872,6 +872,23 @@ WHERE [p].[NullableInt] IS NOT NULL AND [p].[NullableInt] <> @nullableInts1 """); } + public override async Task Parameter_collection_of_nullable_ints_Contains_nullable_int_with_EF_Parameter() + { + await base.Parameter_collection_of_nullable_ints_Contains_nullable_int_with_EF_Parameter(); + + AssertSql( + """ +@nullableInts_without_nulls='[999]' (Size = 5) + +SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[NullableString], [p].[NullableStrings], [p].[NullableWrappedId], [p].[NullableWrappedIdWithNullableComparer], [p].[String], [p].[Strings], [p].[WrappedId] +FROM [PrimitiveCollectionsEntity] AS [p] +WHERE [p].[NullableInt] IN ( + SELECT [n].[value] + FROM OPENJSON(@nullableInts_without_nulls) AS [n] +) OR [p].[NullableInt] IS NULL +"""); + } + public override async Task Parameter_collection_of_structs_Contains_struct() { await base.Parameter_collection_of_structs_Contains_struct(); diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerTest.cs index c1948687da3..a3a67c1e08f 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerTest.cs @@ -726,6 +726,23 @@ WHERE [p].[NullableInt] IS NOT NULL AND [p].[NullableInt] <> @nullableInts1 """); } + public override async Task Parameter_collection_of_nullable_ints_Contains_nullable_int_with_EF_Parameter() + { + await base.Parameter_collection_of_nullable_ints_Contains_nullable_int_with_EF_Parameter(); + + AssertSql( + """ +@nullableInts_without_nulls='[999]' (Size = 4000) + +SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[NullableString], [p].[NullableStrings], [p].[NullableWrappedId], [p].[NullableWrappedIdWithNullableComparer], [p].[String], [p].[Strings], [p].[WrappedId] +FROM [PrimitiveCollectionsEntity] AS [p] +WHERE [p].[NullableInt] IN ( + SELECT [n].[value] + FROM OPENJSON(@nullableInts_without_nulls) AS [n] +) OR [p].[NullableInt] IS NULL +"""); + } + public override async Task Parameter_collection_of_strings_Contains_string() { await base.Parameter_collection_of_strings_Contains_string(); @@ -2568,6 +2585,19 @@ SELECT COUNT(*) """); } + [ConditionalFact] // #37605 + public virtual async Task Parameter_collection_with_null_value_Contains_null_2201_values() + { + using var context = Fixture.CreateContext(); + + var values = Enumerable.Range(1, 2200).Select(i => (int?)i).ToList(); + values.Add(null); + + await AssertQuery(ss => ss.Set().Where(e => values.Contains(e.NullableInt))); + + // No SQL assertion as the SQL is huge + } + [ConditionalFact] public virtual async Task Parameter_collection_of_ints_Contains_int_2071_values() { diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/PrimitiveCollectionsQuerySqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/PrimitiveCollectionsQuerySqliteTest.cs index 78304738e5f..54f81d7abd9 100644 --- a/test/EFCore.Sqlite.FunctionalTests/Query/PrimitiveCollectionsQuerySqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/Query/PrimitiveCollectionsQuerySqliteTest.cs @@ -714,6 +714,23 @@ public override async Task Parameter_collection_of_nullable_ints_Contains_nullab """); } + public override async Task Parameter_collection_of_nullable_ints_Contains_nullable_int_with_EF_Parameter() + { + await base.Parameter_collection_of_nullable_ints_Contains_nullable_int_with_EF_Parameter(); + + AssertSql( + """ +@nullableInts_without_nulls='[999]' (Size = 5) + +SELECT "p"."Id", "p"."Bool", "p"."Bools", "p"."DateTime", "p"."DateTimes", "p"."Enum", "p"."Enums", "p"."Int", "p"."Ints", "p"."NullableInt", "p"."NullableInts", "p"."NullableString", "p"."NullableStrings", "p"."NullableWrappedId", "p"."NullableWrappedIdWithNullableComparer", "p"."String", "p"."Strings", "p"."WrappedId" +FROM "PrimitiveCollectionsEntity" AS "p" +WHERE "p"."NullableInt" IN ( + SELECT "n"."value" + FROM json_each(@nullableInts_without_nulls) AS "n" +) OR "p"."NullableInt" IS NULL +"""); + } + public override async Task Parameter_collection_of_strings_Contains_string() { await base.Parameter_collection_of_strings_Contains_string();