From 2d38615d75e6e5e6ebec8dcfa02da2cfa5b87e1d Mon Sep 17 00:00:00 2001 From: Pavel Pachobut Date: Mon, 19 Jan 2026 23:36:31 -0500 Subject: [PATCH] Fix null value handling for primitive collections with nullable type, introduced in #37257 - Checking for IEnumerable before IEnumerable - Using generic ToList() to preserve collection element type - processedValues should now have the same element type as the input collection (as was before #37257) Fixes #37537 --- .../Query/SqlNullabilityProcessor.cs | 15 ++++++++++++++- .../PrimitiveCollectionsQueryCosmosTest.cs | 14 ++++++++++++++ .../Query/PrimitiveCollectionsQueryTestBase.cs | 10 ++++++++++ ...PrimitiveCollectionsQueryOldSqlServerTest.cs | 3 +++ ...PrimitiveCollectionsQuerySqlServer160Test.cs | 17 +++++++++++++++++ ...tiveCollectionsQuerySqlServerJsonTypeTest.cs | 17 +++++++++++++++++ .../PrimitiveCollectionsQuerySqlServerTest.cs | 17 +++++++++++++++++ .../PrimitiveCollectionsQuerySqliteTest.cs | 17 +++++++++++++++++ 8 files changed, 109 insertions(+), 1 deletion(-) diff --git a/src/EFCore.Relational/Query/SqlNullabilityProcessor.cs b/src/EFCore.Relational/Query/SqlNullabilityProcessor.cs index 93b12e4c483..ff516d83e02 100644 --- a/src/EFCore.Relational/Query/SqlNullabilityProcessor.cs +++ b/src/EFCore.Relational/Query/SqlNullabilityProcessor.cs @@ -1910,7 +1910,20 @@ protected virtual bool TryMakeNonNullable( throw new UnreachableException($"Parameter '{collectionParameter.Name}' is not an IEnumerable."); } - var values = enumerable.Cast().ToList(); + IList values; + if (enumerable.GetType().GetInterfaces().FirstOrDefault( + i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IEnumerable<>) + ) is { } genericEnumerableInterface) + { + var elementType = genericEnumerableInterface.GetGenericArguments()[0]; + var toListMethod = typeof(Enumerable).GetMethod(nameof(Enumerable.ToList))!.MakeGenericMethod(elementType); + values = (IList)toListMethod.Invoke(null, [ enumerable ])!; + } + else + { + values = enumerable.Cast().ToList(); + } + IList? processedValues = null; for (var i = 0; i < values.Count; i++) diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/PrimitiveCollectionsQueryCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/PrimitiveCollectionsQueryCosmosTest.cs index 29a503642f2..43a7a212cde 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/PrimitiveCollectionsQueryCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/PrimitiveCollectionsQueryCosmosTest.cs @@ -481,6 +481,20 @@ WHERE ARRAY_CONTAINS(@Select, c["NullableString"]) """); } + public override async Task Inline_collection_Contains_with_Nullable_Int_IEnumerable_Array_Containing_Null_EF_Parameter() + { + await base.Inline_collection_Contains_with_Nullable_Int_IEnumerable_Array_Containing_Null_EF_Parameter(); + + AssertSql( +""" +@data='[null,1]' + +SELECT VALUE c +FROM root c +WHERE ARRAY_CONTAINS(@data, c["NullableInt"]) +"""); + } + public override async Task Inline_collection_Count_with_column_predicate_with_EF_Parameter() { await base.Inline_collection_Count_with_column_predicate_with_EF_Parameter(); diff --git a/test/EFCore.Specification.Tests/Query/PrimitiveCollectionsQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/PrimitiveCollectionsQueryTestBase.cs index f09570e1633..dd0b6b02c2b 100644 --- a/test/EFCore.Specification.Tests/Query/PrimitiveCollectionsQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/PrimitiveCollectionsQueryTestBase.cs @@ -245,6 +245,16 @@ public virtual Task Inline_collection_Contains_with_IEnumerable_EF_Parameter() ss => ss.Set().Where(c => data.Select(x => x).Contains(c.NullableString))); } + [ConditionalFact] + public virtual Task Inline_collection_Contains_with_Nullable_Int_IEnumerable_Array_Containing_Null_EF_Parameter() + { + int?[] data = [ null, 1 ]; + + return AssertQuery( + ss => ss.Set().Where(c => EF.Parameter(data).Contains(c.NullableInt)), + ss => ss.Set().Where(c => data.Contains(c.NullableInt))); + } + [ConditionalFact] public virtual Task Inline_collection_Count_with_column_predicate_with_EF_Parameter() => AssertQuery( diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQueryOldSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQueryOldSqlServerTest.cs index cc2ac1642ef..761264725a7 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQueryOldSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQueryOldSqlServerTest.cs @@ -460,6 +460,9 @@ public override Task Inline_collection_Contains_with_EF_Parameter() public override Task Inline_collection_Contains_with_IEnumerable_EF_Parameter() => AssertCompatibilityLevelTooLow(() => base.Inline_collection_Contains_with_IEnumerable_EF_Parameter()); + public override Task Inline_collection_Contains_with_Nullable_Int_IEnumerable_Array_Containing_Null_EF_Parameter() + => AssertCompatibilityLevelTooLow(() => base.Inline_collection_Contains_with_Nullable_Int_IEnumerable_Array_Containing_Null_EF_Parameter()); + public override Task Inline_collection_Count_with_column_predicate_with_EF_Parameter() => AssertCompatibilityLevelTooLow(() => base.Inline_collection_Count_with_column_predicate_with_EF_Parameter()); diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServer160Test.cs b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServer160Test.cs index bada435ef43..59da814e9e8 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServer160Test.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServer160Test.cs @@ -454,6 +454,23 @@ FROM OPENJSON(@Select) WITH ([value] nvarchar(max) '$') AS [s] """); } + public override async Task Inline_collection_Contains_with_Nullable_Int_IEnumerable_Array_Containing_Null_EF_Parameter() + { + await base.Inline_collection_Contains_with_Nullable_Int_IEnumerable_Array_Containing_Null_EF_Parameter(); + + AssertSql( +""" +@data_without_nulls='[1]' (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 [d].[value] + FROM OPENJSON(@data_without_nulls) AS [d] +) OR [p].[NullableInt] IS NULL +"""); + } + public override async Task Inline_collection_Count_with_column_predicate_with_EF_Parameter() { await base.Inline_collection_Count_with_column_predicate_with_EF_Parameter(); diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerJsonTypeTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerJsonTypeTest.cs index 37589e77a17..f5ea62d2b51 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerJsonTypeTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerJsonTypeTest.cs @@ -598,6 +598,23 @@ FROM OPENJSON(@Select) WITH ([value] nvarchar(max) '$') AS [s] """); } + public override async Task Inline_collection_Contains_with_Nullable_Int_IEnumerable_Array_Containing_Null_EF_Parameter() + { + await base.Inline_collection_Contains_with_Nullable_Int_IEnumerable_Array_Containing_Null_EF_Parameter(); + + AssertSql( +""" +@data_without_nulls='[1]' (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 [d].[value] + FROM OPENJSON(@data_without_nulls) AS [d] +) OR [p].[NullableInt] IS NULL +"""); + } + public override async Task Inline_collection_Count_with_column_predicate_with_EF_Parameter() { await base.Inline_collection_Count_with_column_predicate_with_EF_Parameter(); diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerTest.cs index 8824c821189..0c6e52ab423 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerTest.cs @@ -477,6 +477,23 @@ FROM OPENJSON(@Select) WITH ([value] nvarchar(max) '$') AS [s] """); } + public override async Task Inline_collection_Contains_with_Nullable_Int_IEnumerable_Array_Containing_Null_EF_Parameter() + { + await base.Inline_collection_Contains_with_Nullable_Int_IEnumerable_Array_Containing_Null_EF_Parameter(); + + AssertSql( +""" +@data_without_nulls='[1]' (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 [d].[value] + FROM OPENJSON(@data_without_nulls) AS [d] +) OR [p].[NullableInt] IS NULL +"""); + } + public override async Task Inline_collection_Count_with_column_predicate_with_EF_Parameter() { await base.Inline_collection_Count_with_column_predicate_with_EF_Parameter(); diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/PrimitiveCollectionsQuerySqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/PrimitiveCollectionsQuerySqliteTest.cs index 78304738e5f..bb7ee9ce10f 100644 --- a/test/EFCore.Sqlite.FunctionalTests/Query/PrimitiveCollectionsQuerySqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/Query/PrimitiveCollectionsQuerySqliteTest.cs @@ -465,6 +465,23 @@ FROM json_each(@Select) AS "s" """); } + public override async Task Inline_collection_Contains_with_Nullable_Int_IEnumerable_Array_Containing_Null_EF_Parameter() + { + await base.Inline_collection_Contains_with_Nullable_Int_IEnumerable_Array_Containing_Null_EF_Parameter(); + + AssertSql( +""" +@data_without_nulls='[1]' (Size = 3) + +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 "d"."value" + FROM json_each(@data_without_nulls) AS "d" +) OR "p"."NullableInt" IS NULL +"""); + } + public override async Task Inline_collection_Count_with_column_predicate_with_EF_Parameter() { await base.Inline_collection_Count_with_column_predicate_with_EF_Parameter();