diff --git a/src/EFCore/Storage/ValueConversion/ValueConverter`.cs b/src/EFCore/Storage/ValueConversion/ValueConverter`.cs index 55493d83522..d46d5825dc0 100644 --- a/src/EFCore/Storage/ValueConversion/ValueConverter`.cs +++ b/src/EFCore/Storage/ValueConversion/ValueConverter`.cs @@ -81,9 +81,19 @@ 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 (unwrappedType.IsEnum) + { + return (T)Enum.ToObject(unwrappedType, value); + } + + return (T)Convert.ChangeType(value, unwrappedType); } /// 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();