From 74afc7274ddcdfeac15ff589b95b4826509847ba Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Mon, 13 Apr 2026 20:00:47 +0300 Subject: [PATCH 1/6] [release/10.0] Fix InvalidCastException when query parameter is IEnumerable with mismatched enum types (#38021) * Fix InvalidCastException when query parameter is IEnumerable with mismatched enum types ValueConverter.Sanitize used Convert.ChangeType for type mismatches, which cannot convert between different enum types. Use Enum.ToObject instead when the target type is an enum, which handles conversion from different enum types with the same underlying type. Fixes #38008 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix tests. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Jiri Cincura --- .../ValueConversion/ValueConverter`.cs | 19 ++++++- ...itiveCollectionsQueryRelationalTestBase.cs | 52 +++++++++++++++++ ...dPrimitiveCollectionsQuerySqlServerTest.cs | 57 +++++++++++++++++++ 3 files changed, 125 insertions(+), 3 deletions(-) diff --git a/src/EFCore/Storage/ValueConversion/ValueConverter`.cs b/src/EFCore/Storage/ValueConversion/ValueConverter`.cs index 55493d83522..e5984d14b6c 100644 --- a/src/EFCore/Storage/ValueConversion/ValueConverter`.cs +++ b/src/EFCore/Storage/ValueConversion/ValueConverter`.cs @@ -77,13 +77,26 @@ public ValueConverter( ? null : convertFunc(Sanitize(v)); + private static readonly bool UseOldBehavior38008 = + AppContext.TryGetSwitch("Microsoft.EntityFrameworkCore.Issue38008", out var enabled) && enabled; + private static T Sanitize(object value) { var unwrappedType = typeof(T).UnwrapNullableType(); - return (T)(!unwrappedType.IsInstanceOfType(value) - ? Convert.ChangeType(value, unwrappedType) - : value); + if (unwrappedType.IsInstanceOfType(value)) + { + return (T)value; + } + + // Convert.ChangeType cannot convert to enum types; use Enum.ToObject instead, which handles + // conversion from different enum types (with the same underlying type) or from integral types. + if (!UseOldBehavior38008 && unwrappedType.IsEnum) + { + return (T)Enum.ToObject(unwrappedType, value); + } + + return (T)Convert.ChangeType(value, unwrappedType); } /// diff --git a/test/EFCore.Relational.Specification.Tests/Query/NonSharedPrimitiveCollectionsQueryRelationalTestBase.cs b/test/EFCore.Relational.Specification.Tests/Query/NonSharedPrimitiveCollectionsQueryRelationalTestBase.cs index 7d87f035d68..0cef8665daa 100644 --- a/test/EFCore.Relational.Specification.Tests/Query/NonSharedPrimitiveCollectionsQueryRelationalTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/Query/NonSharedPrimitiveCollectionsQueryRelationalTestBase.cs @@ -250,4 +250,56 @@ protected void ClearLog() protected void AssertSql(params string[] expected) => TestSqlLoggerFactory.AssertBaseline(expected); + + // #38008 + [ConditionalTheory, MemberData(nameof(ParameterTranslationModeValues))] + public virtual async Task Parameter_collection_of_enum_Cast_from_different_enum_type(ParameterTranslationMode mode) + { + var contextFactory = await InitializeAsync( + onConfiguring: b => SetParameterizedCollectionMode(b, mode), + seed: context => + { + context.AddRange( + new Context38008.TestEntity38008 { Id = 1, Status = Context38008.EntityEnum.Clean }, + new Context38008.TestEntity38008 { Id = 2, Status = Context38008.EntityEnum.Malware }); + return context.SaveChangesAsync(); + }); + + await using var context = contextFactory.CreateContext(); + + // Cast() returns a lazy IEnumerable whose boxed values retain the ViewModelEnum runtime type. + var filter = new[] { Context38008.ViewModelEnum.Malware }.Cast(); + var result = await context.Set() + .Where(a => filter.Any(f => f == a.Status)) + .Select(a => a.Id) + .ToListAsync(); + + Assert.Equivalent(new[] { 2 }, result); + } + + protected class Context38008(DbContextOptions options) : DbContext(options) + { + protected override void OnModelCreating(ModelBuilder modelBuilder) + => modelBuilder.Entity().Property(e => e.Id).ValueGeneratedNever(); + + public class TestEntity38008 + { + public int Id { get; set; } + public EntityEnum Status { get; set; } + } + + [Flags] + public enum EntityEnum + { + Clean = 1, + Malware = 2 + } + + [Flags] + public enum ViewModelEnum + { + Clean = 1, + Malware = 2 + } + } } diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NonSharedPrimitiveCollectionsQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NonSharedPrimitiveCollectionsQuerySqlServerTest.cs index 71a755d1e22..be6280313ed 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/NonSharedPrimitiveCollectionsQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/NonSharedPrimitiveCollectionsQuerySqlServerTest.cs @@ -1201,6 +1201,63 @@ public class Dependent } } + public override async Task Parameter_collection_of_enum_Cast_from_different_enum_type(ParameterTranslationMode mode) + { + await base.Parameter_collection_of_enum_Cast_from_different_enum_type(mode); + + switch (mode) + { + case ParameterTranslationMode.Constant: + { + AssertSql( + """ +SELECT [t].[Id] +FROM [TestEntity38008] AS [t] +WHERE EXISTS ( + SELECT 1 + FROM (VALUES (CAST(2 AS int))) AS [f]([Value]) + WHERE [f].[Value] = [t].[Status]) +"""); + break; + } + + case ParameterTranslationMode.Parameter: + { + AssertSql( + """ +@filter='[2]' (Size = 4000) + +SELECT [t].[Id] +FROM [TestEntity38008] AS [t] +WHERE EXISTS ( + SELECT 1 + FROM OPENJSON(@filter) WITH ([value] int '$') AS [f] + WHERE [f].[value] = [t].[Status]) +"""); + break; + } + + case ParameterTranslationMode.MultipleParameters: + { + AssertSql( + """ +@filter1='2' + +SELECT [t].[Id] +FROM [TestEntity38008] AS [t] +WHERE EXISTS ( + SELECT 1 + FROM (VALUES (@filter1)) AS [f]([Value]) + WHERE [f].[Value] = [t].[Status]) +"""); + break; + } + + default: + throw new NotImplementedException(); + } + } + [ConditionalFact] public virtual void Check_all_tests_overridden() => TestHelpers.AssertAllMethodsOverridden(GetType()); From 8695aef9b40687447b3db00fd5d5d85c5951a349 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jiri=20Cincura=20=E2=86=B9?= Date: Mon, 13 Apr 2026 19:01:04 +0200 Subject: [PATCH 2/6] [release/10.0] Fix issues with nulls in primitive collections (#38066) Backport of #37674. --- .../Query/SqlNullabilityProcessor.cs | 19 ++++++++++++++++++- .../SqlServerSqlNullabilityProcessor.cs | 7 ++++++- .../PrimitiveCollectionsQueryCosmosTest.cs | 14 ++++++++++++++ .../PrimitiveCollectionsQueryTestBase.cs | 10 ++++++++++ ...imitiveCollectionsQueryOldSqlServerTest.cs | 8 ++++++++ ...imitiveCollectionsQuerySqlServer160Test.cs | 17 +++++++++++++++++ ...veCollectionsQuerySqlServerJsonTypeTest.cs | 17 +++++++++++++++++ .../PrimitiveCollectionsQuerySqlServerTest.cs | 17 +++++++++++++++++ .../PrimitiveCollectionsQuerySqliteTest.cs | 17 +++++++++++++++++ 9 files changed, 124 insertions(+), 2 deletions(-) diff --git a/src/EFCore.Relational/Query/SqlNullabilityProcessor.cs b/src/EFCore.Relational/Query/SqlNullabilityProcessor.cs index c2d8e99c4c6..b7b38a2d59b 100644 --- a/src/EFCore.Relational/Query/SqlNullabilityProcessor.cs +++ b/src/EFCore.Relational/Query/SqlNullabilityProcessor.cs @@ -26,6 +26,9 @@ public class SqlNullabilityProcessor : ExpressionVisitor private static readonly bool UseOldBehavior37152 = AppContext.TryGetSwitch("Microsoft.EntityFrameworkCore.Issue37152", out var enabled) && enabled; + private static readonly bool UseOldBehavior37537 = + AppContext.TryGetSwitch("Microsoft.EntityFrameworkCore.Issue37537", out var enabled) && enabled; + private readonly List _nonNullableColumns; private readonly List _nullValueColumns; private readonly ISqlExpressionFactory _sqlExpressionFactory; @@ -1906,6 +1909,7 @@ protected virtual bool TryMakeNonNullable( var parameters = ParametersDecorator.GetAndDisableCaching(); IList values; + Type elementClrType; if (UseOldBehavior37204) { if (parameters[collectionParameter.Name] is not IList list) @@ -1913,6 +1917,13 @@ protected virtual bool TryMakeNonNullable( throw new UnreachableException($"Parameter '{collectionParameter.Name}' is not an IList."); } values = list; + elementClrType = UseOldBehavior37537 + ? 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. + : list.GetType().TryGetElementType(typeof(IEnumerable<>)) ?? typeof(object); } else { @@ -1921,6 +1932,13 @@ protected virtual bool TryMakeNonNullable( throw new UnreachableException($"Parameter '{collectionParameter.Name}' is not an IEnumerable."); } values = enumerable.Cast().ToList(); + elementClrType = UseOldBehavior37537 + ? 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. + : enumerable.GetType().TryGetElementType(typeof(IEnumerable<>)) ?? typeof(object); } IList? processedValues = null; @@ -1933,7 +1951,6 @@ protected virtual bool TryMakeNonNullable( { if (processedValues is null) { - var elementClrType = values.GetType().GetSequenceType(); 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 0a03aa8743d..02c34e78553 100644 --- a/src/EFCore.SqlServer/Query/Internal/SqlServerSqlNullabilityProcessor.cs +++ b/src/EFCore.SqlServer/Query/Internal/SqlServerSqlNullabilityProcessor.cs @@ -27,6 +27,9 @@ public class SqlServerSqlNullabilityProcessor : SqlNullabilityProcessor private static readonly bool UseOldBehavior37336 = AppContext.TryGetSwitch("Microsoft.EntityFrameworkCore.Issue37336", out var enabled) && enabled; + private static readonly bool UseOldBehavior37537 = + AppContext.TryGetSwitch("Microsoft.EntityFrameworkCore.Issue37537", out var enabled) && enabled; + /// /// 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 @@ -290,7 +293,9 @@ protected override SqlExpression VisitIn(InExpression inExpression, bool allowOp new ColumnExpression( columnName, openJson.Alias, - valuesParameter.Type.GetSequenceType(), + UseOldBehavior37537 + ? 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 38d9b4b2c02..25bc5e5f408 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/PrimitiveCollectionsQueryCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/PrimitiveCollectionsQueryCosmosTest.cs @@ -647,6 +647,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 6884f0ad255..00bc11d980d 100644 --- a/test/EFCore.Specification.Tests/Query/PrimitiveCollectionsQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/PrimitiveCollectionsQueryTestBase.cs @@ -308,6 +308,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 46d2d7b1006..9ba60047cb2 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQueryOldSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQueryOldSqlServerTest.cs @@ -623,6 +623,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(); diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServer160Test.cs b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServer160Test.cs index d3be48335f1..102e3bb2e54 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServer160Test.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServer160Test.cs @@ -631,6 +631,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 04c506b5193..2f652bd5664 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerJsonTypeTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerJsonTypeTest.cs @@ -800,6 +800,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 53c8dd6c792..68246b08e36 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerTest.cs @@ -654,6 +654,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.Sqlite.FunctionalTests/Query/PrimitiveCollectionsQuerySqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/PrimitiveCollectionsQuerySqliteTest.cs index 999c446f4a7..844d0975b3e 100644 --- a/test/EFCore.Sqlite.FunctionalTests/Query/PrimitiveCollectionsQuerySqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/Query/PrimitiveCollectionsQuerySqliteTest.cs @@ -642,6 +642,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(); From e36e84cb3e064edcb501b932d8e5bbb7001c2bea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jiri=20Cincura=20=E2=86=B9?= Date: Mon, 13 Apr 2026 19:26:23 +0200 Subject: [PATCH 3/6] Fix quirks. --- .../Query/SqlNullabilityProcessor.cs | 23 ++++--------------- 1 file changed, 5 insertions(+), 18 deletions(-) diff --git a/src/EFCore.Relational/Query/SqlNullabilityProcessor.cs b/src/EFCore.Relational/Query/SqlNullabilityProcessor.cs index b7b38a2d59b..96668185635 100644 --- a/src/EFCore.Relational/Query/SqlNullabilityProcessor.cs +++ b/src/EFCore.Relational/Query/SqlNullabilityProcessor.cs @@ -26,9 +26,6 @@ public class SqlNullabilityProcessor : ExpressionVisitor private static readonly bool UseOldBehavior37152 = AppContext.TryGetSwitch("Microsoft.EntityFrameworkCore.Issue37152", out var enabled) && enabled; - private static readonly bool UseOldBehavior37537 = - AppContext.TryGetSwitch("Microsoft.EntityFrameworkCore.Issue37537", out var enabled) && enabled; - private readonly List _nonNullableColumns; private readonly List _nullValueColumns; private readonly ISqlExpressionFactory _sqlExpressionFactory; @@ -1909,7 +1906,6 @@ protected virtual bool TryMakeNonNullable( var parameters = ParametersDecorator.GetAndDisableCaching(); IList values; - Type elementClrType; if (UseOldBehavior37204) { if (parameters[collectionParameter.Name] is not IList list) @@ -1917,13 +1913,6 @@ protected virtual bool TryMakeNonNullable( throw new UnreachableException($"Parameter '{collectionParameter.Name}' is not an IList."); } values = list; - elementClrType = UseOldBehavior37537 - ? 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. - : list.GetType().TryGetElementType(typeof(IEnumerable<>)) ?? typeof(object); } else { @@ -1932,13 +1921,6 @@ protected virtual bool TryMakeNonNullable( throw new UnreachableException($"Parameter '{collectionParameter.Name}' is not an IEnumerable."); } values = enumerable.Cast().ToList(); - elementClrType = UseOldBehavior37537 - ? 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. - : enumerable.GetType().TryGetElementType(typeof(IEnumerable<>)) ?? typeof(object); } IList? processedValues = null; @@ -1951,6 +1933,11 @@ protected virtual bool TryMakeNonNullable( { if (processedValues is null) { + // 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++) { From 2047a287db123fe8621536be3c3eb65a162fb855 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jiri=20Cincura=20=E2=86=B9?= Date: Mon, 13 Apr 2026 19:27:55 +0200 Subject: [PATCH 4/6] Fix quirks. --- .../Query/Internal/SqlServerSqlNullabilityProcessor.cs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/EFCore.SqlServer/Query/Internal/SqlServerSqlNullabilityProcessor.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerSqlNullabilityProcessor.cs index 02c34e78553..1110f21842b 100644 --- a/src/EFCore.SqlServer/Query/Internal/SqlServerSqlNullabilityProcessor.cs +++ b/src/EFCore.SqlServer/Query/Internal/SqlServerSqlNullabilityProcessor.cs @@ -27,9 +27,6 @@ public class SqlServerSqlNullabilityProcessor : SqlNullabilityProcessor private static readonly bool UseOldBehavior37336 = AppContext.TryGetSwitch("Microsoft.EntityFrameworkCore.Issue37336", out var enabled) && enabled; - private static readonly bool UseOldBehavior37537 = - AppContext.TryGetSwitch("Microsoft.EntityFrameworkCore.Issue37537", out var enabled) && enabled; - /// /// 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 @@ -293,9 +290,7 @@ protected override SqlExpression VisitIn(InExpression inExpression, bool allowOp new ColumnExpression( columnName, openJson.Alias, - UseOldBehavior37537 - ? valuesParameter.Type.GetSequenceType() - : valuesParameter.Type.GetSequenceType().UnwrapNullableType(), + valuesParameter.Type.GetSequenceType().UnwrapNullableType(), elementTypeMapping, containsNulls!.Value), columnName) From 42989a3972d73f225155361bc16228df6f5dc583 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jiri=20Cincura=20=E2=86=B9?= Date: Mon, 13 Apr 2026 19:29:31 +0200 Subject: [PATCH 5/6] Fix quirks. --- src/EFCore/Storage/ValueConversion/ValueConverter`.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/EFCore/Storage/ValueConversion/ValueConverter`.cs b/src/EFCore/Storage/ValueConversion/ValueConverter`.cs index e5984d14b6c..d46d5825dc0 100644 --- a/src/EFCore/Storage/ValueConversion/ValueConverter`.cs +++ b/src/EFCore/Storage/ValueConversion/ValueConverter`.cs @@ -77,9 +77,6 @@ public ValueConverter( ? null : convertFunc(Sanitize(v)); - private static readonly bool UseOldBehavior38008 = - AppContext.TryGetSwitch("Microsoft.EntityFrameworkCore.Issue38008", out var enabled) && enabled; - private static T Sanitize(object value) { var unwrappedType = typeof(T).UnwrapNullableType(); @@ -91,7 +88,7 @@ private static T Sanitize(object value) // Convert.ChangeType cannot convert to enum types; use Enum.ToObject instead, which handles // conversion from different enum types (with the same underlying type) or from integral types. - if (!UseOldBehavior38008 && unwrappedType.IsEnum) + if (unwrappedType.IsEnum) { return (T)Enum.ToObject(unwrappedType, value); } From 805f0ba8929cfab43a75cd841cef97d9c483789e Mon Sep 17 00:00:00 2001 From: Jiri Cincura Date: Tue, 14 Apr 2026 09:00:08 +0200 Subject: [PATCH 6/6] Correct conflicts. --- ...itiveCollectionsQueryRelationalTestBase.cs | 52 +++++++++++++++++ ...imitiveCollectionsQueryOldSqlServerTest.cs | 48 ++++++++++++++++ ...imitiveCollectionsQuerySqlServer160Test.cs | 57 +++++++++++++++++++ ...veCollectionsQuerySqlServerJsonTypeTest.cs | 57 +++++++++++++++++++ .../PrimitiveCollectionsQuerySqlServerTest.cs | 57 +++++++++++++++++++ .../PrimitiveCollectionsQuerySqliteTest.cs | 57 +++++++++++++++++++ 6 files changed, 328 insertions(+) diff --git a/test/EFCore.Relational.Specification.Tests/Query/PrimitiveCollectionsQueryRelationalTestBase.cs b/test/EFCore.Relational.Specification.Tests/Query/PrimitiveCollectionsQueryRelationalTestBase.cs index a83e5e99724..fae2714ea2c 100644 --- a/test/EFCore.Relational.Specification.Tests/Query/PrimitiveCollectionsQueryRelationalTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/Query/PrimitiveCollectionsQueryRelationalTestBase.cs @@ -274,6 +274,58 @@ public override async Task Project_inline_collection_with_Concat() Assert.Equal(RelationalStrings.InsufficientInformationToIdentifyElementOfCollectionJoin, message); } + // #38008 + [ConditionalTheory, MemberData(nameof(ParameterTranslationModeValues))] + public virtual async Task Parameter_collection_of_enum_Cast_from_different_enum_type(ParameterTranslationMode mode) + { + var contextFactory = await InitializeNonSharedTest( + onConfiguring: b => SetParameterizedCollectionMode(b, mode), + seed: context => + { + context.AddRange( + new Context38008.TestEntity38008 { Id = 1, Status = Context38008.EntityEnum.Clean }, + new Context38008.TestEntity38008 { Id = 2, Status = Context38008.EntityEnum.Malware }); + return context.SaveChangesAsync(); + }); + + await using var context = contextFactory.CreateDbContext(); + + // Cast() returns a lazy IEnumerable whose boxed values retain the ViewModelEnum runtime type. + var filter = new[] { Context38008.ViewModelEnum.Malware }.Cast(); + var result = await context.Set() + .Where(a => filter.Any(f => f == a.Status)) + .Select(a => a.Id) + .ToListAsync(); + + Assert.Equivalent(new[] { 2 }, result); + } + + protected class Context38008(DbContextOptions options) : DbContext(options) + { + protected override void OnModelCreating(ModelBuilder modelBuilder) + => modelBuilder.Entity().Property(e => e.Id).ValueGeneratedNever(); + + public class TestEntity38008 + { + public int Id { get; set; } + public EntityEnum Status { get; set; } + } + + [Flags] + public enum EntityEnum + { + Clean = 1, + Malware = 2 + } + + [Flags] + public enum ViewModelEnum + { + Clean = 1, + Malware = 2 + } + } + protected class TestOwner { public int Id { get; set; } diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQueryOldSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQueryOldSqlServerTest.cs index 0c8b8425307..ceee9d5352f 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQueryOldSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQueryOldSqlServerTest.cs @@ -1231,6 +1231,54 @@ WHERE [t].[Id] IN (@ints1, @ints2, @ints3, @ints4, @ints5, @ints6, @ints7, @ints """); } + public override async Task Parameter_collection_of_enum_Cast_from_different_enum_type(ParameterTranslationMode mode) + { + switch (mode) + { + case ParameterTranslationMode.Constant: + { + await base.Parameter_collection_of_enum_Cast_from_different_enum_type(mode); + AssertSql( + """ +SELECT [t].[Id] +FROM [TestEntity38008] AS [t] +WHERE EXISTS ( + SELECT 1 + FROM (VALUES (CAST(2 AS int))) AS [f]([Value]) + WHERE [f].[Value] = [t].[Status]) +"""); + break; + } + + case ParameterTranslationMode.Parameter: + { + await AssertCompatibilityLevelTooLow( + () => base.Parameter_collection_Count_with_column_predicate_with_default_mode(mode)); + break; + } + + case ParameterTranslationMode.MultipleParameters: + { + await base.Parameter_collection_of_enum_Cast_from_different_enum_type(mode); + AssertSql( + """ +@filter1='2' + +SELECT [t].[Id] +FROM [TestEntity38008] AS [t] +WHERE EXISTS ( + SELECT 1 + FROM (VALUES (@filter1)) AS [f]([Value]) + WHERE [f].[Value] = [t].[Status]) +"""); + break; + } + + default: + throw new NotImplementedException(); + } + } + public override async Task Static_readonly_collection_List_of_ints_Contains_int() { await base.Static_readonly_collection_List_of_ints_Contains_int(); diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServer160Test.cs b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServer160Test.cs index 1b943eefbaa..7973138146f 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServer160Test.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServer160Test.cs @@ -1287,6 +1287,63 @@ WHERE [t].[Id] IN (@ints1, @ints2, @ints3, @ints4, @ints5, @ints6, @ints7, @ints """); } + public override async Task Parameter_collection_of_enum_Cast_from_different_enum_type(ParameterTranslationMode mode) + { + await base.Parameter_collection_of_enum_Cast_from_different_enum_type(mode); + + switch (mode) + { + case ParameterTranslationMode.Constant: + { + AssertSql( + """ +SELECT [t].[Id] +FROM [TestEntity38008] AS [t] +WHERE EXISTS ( + SELECT 1 + FROM (VALUES (CAST(2 AS int))) AS [f]([Value]) + WHERE [f].[Value] = [t].[Status]) +"""); + break; + } + + case ParameterTranslationMode.Parameter: + { + AssertSql( + """ +@filter='[2]' (Size = 4000) + +SELECT [t].[Id] +FROM [TestEntity38008] AS [t] +WHERE EXISTS ( + SELECT 1 + FROM OPENJSON(@filter) WITH ([value] int '$') AS [f] + WHERE [f].[value] = [t].[Status]) +"""); + break; + } + + case ParameterTranslationMode.MultipleParameters: + { + AssertSql( + """ +@filter1='2' + +SELECT [t].[Id] +FROM [TestEntity38008] AS [t] +WHERE EXISTS ( + SELECT 1 + FROM (VALUES (@filter1)) AS [f]([Value]) + WHERE [f].[Value] = [t].[Status]) +"""); + break; + } + + default: + throw new NotImplementedException(); + } + } + public override async Task Static_readonly_collection_List_of_ints_Contains_int() { await base.Static_readonly_collection_List_of_ints_Contains_int(); diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerJsonTypeTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerJsonTypeTest.cs index 171cc38ddf9..4545a3b982c 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerJsonTypeTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerJsonTypeTest.cs @@ -1459,6 +1459,63 @@ WHERE [t].[Id] IN (@ints1, @ints2, @ints3, @ints4, @ints5, @ints6, @ints7, @ints """); } + public override async Task Parameter_collection_of_enum_Cast_from_different_enum_type(ParameterTranslationMode mode) + { + await base.Parameter_collection_of_enum_Cast_from_different_enum_type(mode); + + switch (mode) + { + case ParameterTranslationMode.Constant: + { + AssertSql( + """ +SELECT [t].[Id] +FROM [TestEntity38008] AS [t] +WHERE EXISTS ( + SELECT 1 + FROM (VALUES (CAST(2 AS int))) AS [f]([Value]) + WHERE [f].[Value] = [t].[Status]) +"""); + break; + } + + case ParameterTranslationMode.Parameter: + { + AssertSql( + """ +@filter='[2]' (Size = 3) + +SELECT [t].[Id] +FROM [TestEntity38008] AS [t] +WHERE EXISTS ( + SELECT 1 + FROM OPENJSON(@filter) WITH ([value] int '$') AS [f] + WHERE [f].[value] = [t].[Status]) +"""); + break; + } + + case ParameterTranslationMode.MultipleParameters: + { + AssertSql( + """ +@filter1='2' + +SELECT [t].[Id] +FROM [TestEntity38008] AS [t] +WHERE EXISTS ( + SELECT 1 + FROM (VALUES (@filter1)) AS [f]([Value]) + WHERE [f].[Value] = [t].[Status]) +"""); + break; + } + + default: + throw new NotImplementedException(); + } + } + public override async Task Static_readonly_collection_List_of_ints_Contains_int() { await base.Static_readonly_collection_List_of_ints_Contains_int(); diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerTest.cs index f851a04f375..c9f5016ae5b 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerTest.cs @@ -1315,6 +1315,63 @@ WHERE [t].[Id] IN (@ints1, @ints2, @ints3, @ints4, @ints5, @ints6, @ints7, @ints """); } + public override async Task Parameter_collection_of_enum_Cast_from_different_enum_type(ParameterTranslationMode mode) + { + await base.Parameter_collection_of_enum_Cast_from_different_enum_type(mode); + + switch (mode) + { + case ParameterTranslationMode.Constant: + { + AssertSql( + """ +SELECT [t].[Id] +FROM [TestEntity38008] AS [t] +WHERE EXISTS ( + SELECT 1 + FROM (VALUES (CAST(2 AS int))) AS [f]([Value]) + WHERE [f].[Value] = [t].[Status]) +"""); + break; + } + + case ParameterTranslationMode.Parameter: + { + AssertSql( + """ +@filter='[2]' (Size = 4000) + +SELECT [t].[Id] +FROM [TestEntity38008] AS [t] +WHERE EXISTS ( + SELECT 1 + FROM OPENJSON(@filter) WITH ([value] int '$') AS [f] + WHERE [f].[value] = [t].[Status]) +"""); + break; + } + + case ParameterTranslationMode.MultipleParameters: + { + AssertSql( + """ +@filter1='2' + +SELECT [t].[Id] +FROM [TestEntity38008] AS [t] +WHERE EXISTS ( + SELECT 1 + FROM (VALUES (@filter1)) AS [f]([Value]) + WHERE [f].[Value] = [t].[Status]) +"""); + break; + } + + default: + throw new NotImplementedException(); + } + } + public override async Task Static_readonly_collection_List_of_ints_Contains_int() { await base.Static_readonly_collection_List_of_ints_Contains_int(); diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/PrimitiveCollectionsQuerySqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/PrimitiveCollectionsQuerySqliteTest.cs index cb03a131aa8..be3c3dab659 100644 --- a/test/EFCore.Sqlite.FunctionalTests/Query/PrimitiveCollectionsQuerySqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/Query/PrimitiveCollectionsQuerySqliteTest.cs @@ -1251,6 +1251,63 @@ public override async Task Parameter_collection_Contains_parameter_bucketization """); } + public override async Task Parameter_collection_of_enum_Cast_from_different_enum_type(ParameterTranslationMode mode) + { + await base.Parameter_collection_of_enum_Cast_from_different_enum_type(mode); + + switch (mode) + { + case ParameterTranslationMode.Constant: + { + AssertSql( + """ +SELECT "t"."Id" +FROM "TestEntity38008" AS "t" +WHERE EXISTS ( + SELECT 1 + FROM (SELECT CAST(2 AS INTEGER) AS "Value") AS "f" + WHERE "f"."Value" = "t"."Status") +"""); + break; + } + + case ParameterTranslationMode.Parameter: + { + AssertSql( + """ +@filter='[2]' (Size = 3) + +SELECT "t"."Id" +FROM "TestEntity38008" AS "t" +WHERE EXISTS ( + SELECT 1 + FROM json_each(@filter) AS "f" + WHERE "f"."value" = "t"."Status") +"""); + break; + } + + case ParameterTranslationMode.MultipleParameters: + { + AssertSql( + """ +@filter1='2' + +SELECT "t"."Id" +FROM "TestEntity38008" AS "t" +WHERE EXISTS ( + SELECT 1 + FROM (SELECT @filter1 AS "Value") AS "f" + WHERE "f"."Value" = "t"."Status") +"""); + break; + } + + default: + throw new NotImplementedException(); + } + } + public override async Task Static_readonly_collection_List_of_ints_Contains_int() { await base.Static_readonly_collection_List_of_ints_Contains_int();