From 9f97574e0da98c91ea71b0c47658db2fee32908c Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Fri, 27 Mar 2026 08:51:48 +0100 Subject: [PATCH 1/2] 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> --- .../ValueConversion/ValueConverter`.cs | 19 +++++-- ...itiveCollectionsQueryRelationalTestBase.cs | 52 +++++++++++++++++++ 2 files changed, 68 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 + } + } } From d2b5d25ad5e71652f0b8d01de644352193908f01 Mon Sep 17 00:00:00 2001 From: Jiri Cincura Date: Thu, 9 Apr 2026 13:33:49 +0200 Subject: [PATCH 2/2] Fix tests. --- ...dPrimitiveCollectionsQuerySqlServerTest.cs | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) 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());