Skip to content

Commit 74afc72

Browse files
rojiCopilotcincuranet
authored
[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<T> 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 <jiri@cincura.net>
1 parent c5255f3 commit 74afc72

3 files changed

Lines changed: 125 additions & 3 deletions

File tree

src/EFCore/Storage/ValueConversion/ValueConverter`.cs

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -77,13 +77,26 @@ public ValueConverter(
7777
? null
7878
: convertFunc(Sanitize<TIn>(v));
7979

80+
private static readonly bool UseOldBehavior38008 =
81+
AppContext.TryGetSwitch("Microsoft.EntityFrameworkCore.Issue38008", out var enabled) && enabled;
82+
8083
private static T Sanitize<T>(object value)
8184
{
8285
var unwrappedType = typeof(T).UnwrapNullableType();
8386

84-
return (T)(!unwrappedType.IsInstanceOfType(value)
85-
? Convert.ChangeType(value, unwrappedType)
86-
: value);
87+
if (unwrappedType.IsInstanceOfType(value))
88+
{
89+
return (T)value;
90+
}
91+
92+
// Convert.ChangeType cannot convert to enum types; use Enum.ToObject instead, which handles
93+
// conversion from different enum types (with the same underlying type) or from integral types.
94+
if (!UseOldBehavior38008 && unwrappedType.IsEnum)
95+
{
96+
return (T)Enum.ToObject(unwrappedType, value);
97+
}
98+
99+
return (T)Convert.ChangeType(value, unwrappedType);
87100
}
88101

89102
/// <summary>

test/EFCore.Relational.Specification.Tests/Query/NonSharedPrimitiveCollectionsQueryRelationalTestBase.cs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,4 +250,56 @@ protected void ClearLog()
250250

251251
protected void AssertSql(params string[] expected)
252252
=> TestSqlLoggerFactory.AssertBaseline(expected);
253+
254+
// #38008
255+
[ConditionalTheory, MemberData(nameof(ParameterTranslationModeValues))]
256+
public virtual async Task Parameter_collection_of_enum_Cast_from_different_enum_type(ParameterTranslationMode mode)
257+
{
258+
var contextFactory = await InitializeAsync<Context38008>(
259+
onConfiguring: b => SetParameterizedCollectionMode(b, mode),
260+
seed: context =>
261+
{
262+
context.AddRange(
263+
new Context38008.TestEntity38008 { Id = 1, Status = Context38008.EntityEnum.Clean },
264+
new Context38008.TestEntity38008 { Id = 2, Status = Context38008.EntityEnum.Malware });
265+
return context.SaveChangesAsync();
266+
});
267+
268+
await using var context = contextFactory.CreateContext();
269+
270+
// Cast<EntityEnum>() returns a lazy IEnumerable whose boxed values retain the ViewModelEnum runtime type.
271+
var filter = new[] { Context38008.ViewModelEnum.Malware }.Cast<Context38008.EntityEnum>();
272+
var result = await context.Set<Context38008.TestEntity38008>()
273+
.Where(a => filter.Any(f => f == a.Status))
274+
.Select(a => a.Id)
275+
.ToListAsync();
276+
277+
Assert.Equivalent(new[] { 2 }, result);
278+
}
279+
280+
protected class Context38008(DbContextOptions options) : DbContext(options)
281+
{
282+
protected override void OnModelCreating(ModelBuilder modelBuilder)
283+
=> modelBuilder.Entity<TestEntity38008>().Property(e => e.Id).ValueGeneratedNever();
284+
285+
public class TestEntity38008
286+
{
287+
public int Id { get; set; }
288+
public EntityEnum Status { get; set; }
289+
}
290+
291+
[Flags]
292+
public enum EntityEnum
293+
{
294+
Clean = 1,
295+
Malware = 2
296+
}
297+
298+
[Flags]
299+
public enum ViewModelEnum
300+
{
301+
Clean = 1,
302+
Malware = 2
303+
}
304+
}
253305
}

test/EFCore.SqlServer.FunctionalTests/Query/NonSharedPrimitiveCollectionsQuerySqlServerTest.cs

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1201,6 +1201,63 @@ public class Dependent
12011201
}
12021202
}
12031203

1204+
public override async Task Parameter_collection_of_enum_Cast_from_different_enum_type(ParameterTranslationMode mode)
1205+
{
1206+
await base.Parameter_collection_of_enum_Cast_from_different_enum_type(mode);
1207+
1208+
switch (mode)
1209+
{
1210+
case ParameterTranslationMode.Constant:
1211+
{
1212+
AssertSql(
1213+
"""
1214+
SELECT [t].[Id]
1215+
FROM [TestEntity38008] AS [t]
1216+
WHERE EXISTS (
1217+
SELECT 1
1218+
FROM (VALUES (CAST(2 AS int))) AS [f]([Value])
1219+
WHERE [f].[Value] = [t].[Status])
1220+
""");
1221+
break;
1222+
}
1223+
1224+
case ParameterTranslationMode.Parameter:
1225+
{
1226+
AssertSql(
1227+
"""
1228+
@filter='[2]' (Size = 4000)
1229+
1230+
SELECT [t].[Id]
1231+
FROM [TestEntity38008] AS [t]
1232+
WHERE EXISTS (
1233+
SELECT 1
1234+
FROM OPENJSON(@filter) WITH ([value] int '$') AS [f]
1235+
WHERE [f].[value] = [t].[Status])
1236+
""");
1237+
break;
1238+
}
1239+
1240+
case ParameterTranslationMode.MultipleParameters:
1241+
{
1242+
AssertSql(
1243+
"""
1244+
@filter1='2'
1245+
1246+
SELECT [t].[Id]
1247+
FROM [TestEntity38008] AS [t]
1248+
WHERE EXISTS (
1249+
SELECT 1
1250+
FROM (VALUES (@filter1)) AS [f]([Value])
1251+
WHERE [f].[Value] = [t].[Status])
1252+
""");
1253+
break;
1254+
}
1255+
1256+
default:
1257+
throw new NotImplementedException();
1258+
}
1259+
}
1260+
12041261
[ConditionalFact]
12051262
public virtual void Check_all_tests_overridden()
12061263
=> TestHelpers.AssertAllMethodsOverridden(GetType());

0 commit comments

Comments
 (0)