efcore: Query: Error for queries with enum parameters whose value is of underlying type but the value expected by type mapping is the actual enum type

Steps to reproduce

I have the following query:

result.Items = await query.Select(a => new MedicalReportSectionListView()
{
	Id = a.Id,
	OperatorId = a.OperatorId,
	Name = a.Name,
	Order = a.Order,
	DefaultContent = a.DefaultContent,
	Hidden = (a.Flags & MedicalReportSectionFlags.Private) != 0,
	Active = (a.Flags & MedicalReportSectionFlags.Active) != 0
}).ToArrayAsync();

the issue is in:

Hidden = (a.Flags & MedicalReportSectionFlags.Private) != 0,

Entity:

[Flags]
public enum MedicalReportSectionFlags
{
	None = 0x0,
	Active = 0x1,
	Private = 0x2,
	Attachment= 0x4
}
public class MedicalReportSection
{
	[Key]
	public int Id { get; set; }
	public Guid OperatorId { get; set; }
	public string Name { get; set; }
	public int Order { get; set; }
	public string DefaultContent { get; set; }
	public virtual Operator Operator { get; set; }
	public MedicalReportSectionFlags Flags { get; set; }
}

Model:

public class MedicalReportSectionListView
{
	public int Id { get; set; }
	public Guid OperatorId { get; set; }
	public string Name { get; set; }
	public int Order { get; set; }
	public string DefaultContent { get; set; }
	public bool Hidden { get; set; }
	public bool Active { get; set; }
}

Stack trace:

System.InvalidCastException
  HResult=0x80004002
  Message=Invalid cast from 'System.Int32' to 'DocSpecial.Data.Entities.MedicalReportSectionFlags'.
  Source=System.Private.CoreLib
  StackTrace:
   at System.Convert.DefaultToType(IConvertible value, Type targetType, IFormatProvider provider)
   at System.Int32.System.IConvertible.ToType(Type type, IFormatProvider provider)
   at System.Convert.ChangeType(Object value, Type conversionType, IFormatProvider provider)
   at System.Convert.ChangeType(Object value, Type conversionType)
   at Microsoft.EntityFrameworkCore.Storage.ValueConversion.ValueConverter`2.Sanitize[T](Object value)
   at Microsoft.EntityFrameworkCore.Storage.ValueConversion.ValueConverter`2.<>c__DisplayClass3_0`2.<SanitizeConverter>b__0(Object v)
   at Microsoft.EntityFrameworkCore.Storage.RelationalTypeMapping.CreateParameter(DbCommand command, String name, Object value, Nullable`1 nullable)
   at Microsoft.EntityFrameworkCore.Storage.Internal.TypeMappedRelationalParameter.AddDbParameter(DbCommand command, Object value)
   at Microsoft.EntityFrameworkCore.Storage.Internal.RelationalParameterBase.AddDbParameter(DbCommand command, IReadOnlyDictionary`2 parameterValues)
   at Microsoft.EntityFrameworkCore.Storage.RelationalCommand.CreateCommand(RelationalCommandParameterObject parameterObject, Guid commandId, DbCommandMethod commandMethod)
   at Microsoft.EntityFrameworkCore.Storage.RelationalCommand.<ExecuteReaderAsync>d__17.MoveNext()
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter`1.GetResult()
   at Microsoft.EntityFrameworkCore.Query.Internal.QueryingEnumerable`1.AsyncEnumerator.<InitializeReaderAsync>d__18.MoveNext()
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter`1.GetResult()
   at Microsoft.EntityFrameworkCore.SqlServer.Storage.Internal.SqlServerExecutionStrategy.<ExecuteAsync>d__7`2.MoveNext()
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Microsoft.EntityFrameworkCore.Query.Internal.QueryingEnumerable`1.AsyncEnumerator.<MoveNextAsync>d__17.MoveNext()
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions.<ToListAsync>d__64`1.MoveNext()
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions.<ToListAsync>d__64`1.MoveNext()
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter`1.GetResult()
   at Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions.<ToArrayAsync>d__65`1.MoveNext()
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter`1.GetResult()
   at DocSpecial.Core.MedicalReportSectionListHandler.<ExecuteWorkAsync>d__0.MoveNext() in D:\Development\IctMakers\Git\DocSpecial\DocSpecial.Core\Commands\MedicalReportSectionListHandler.cs:line 42

  This exception was originally thrown at this call stack:
	System.Convert.DefaultToType(System.IConvertible, System.Type, System.IFormatProvider)
	int.System.IConvertible.ToType(System.Type, System.IFormatProvider)
	System.Convert.ChangeType(object, System.Type, System.IFormatProvider)
	System.Convert.ChangeType(object, System.Type)
	Microsoft.EntityFrameworkCore.Storage.RelationalTypeMapping.CreateParameter(System.Data.Common.DbCommand, string, object, bool?)
	Microsoft.EntityFrameworkCore.Storage.Internal.TypeMappedRelationalParameter.AddDbParameter(System.Data.Common.DbCommand, object)
	Microsoft.EntityFrameworkCore.Storage.Internal.RelationalParameterBase.AddDbParameter(System.Data.Common.DbCommand, System.Collections.Generic.IReadOnlyDictionary<string, object>)
	Microsoft.EntityFrameworkCore.Storage.RelationalCommand.CreateCommand(Microsoft.EntityFrameworkCore.Storage.RelationalCommandParameterObject, System.Guid, Microsoft.EntityFrameworkCore.Diagnostics.DbCommandMethod)
    ...
    [Call Stack Truncated]

Thanks

Further technical details

EF Core version: 5.0.0-alpha1.19552.2 - 3.1.0-preview3.19554.8 Database provider: SqlServer Target framework: .NET Core 3.1 Operating system: Windows 10 - 1903 IDE: Visual Studio 2019 Preview 16.4.0 Preview 6.0

About this issue

  • Original URL
  • State: closed
  • Created 5 years ago
  • Reactions: 6
  • Comments: 24 (14 by maintainers)

Commits related to this issue

Most upvoted comments

bringing back to triage so we can discuss if it meets the bar for patch

This one is quite interesting.

Simplified repro:

        [ConditionalFact]
        public virtual void Repro19128()
        {
            using (var ctx = new Context19128())
            {
                ctx.Database.EnsureDeleted();
                ctx.Database.EnsureCreated();

                var query = ctx.MedicalReportSections
                    .OrderBy(x => x.Id)
                    .Skip(1)
                    .Select(a => a.Flags & MedicalReportSectionFlags.Active).ToList();
            }
        }

        public class Context19128 : DbContext
        {
            public DbSet<MedicalReportSection> MedicalReportSections { get; set; }


            protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
            {
                optionsBuilder.UseSqlServer(@"Server=.;Database=Repro19128;Trusted_Connection=True;MultipleActiveResultSets=True");
            }
        }

        [Flags]
        public enum MedicalReportSectionFlags
        {
            None = 0,
            Active = 1,
        }
        public class MedicalReportSection
        {
            public int Id { get; set; }
            public MedicalReportSectionFlags Flags { get; set; }
        }

What’s happening here is that during parameter extraction, we parameterize argument to the skip method (int = 1), which we always do. However, because the value of MedicalReportSectionFlags.Active is also int = 1, it also gets parameterized, and the same parameter is assigned. So after parameter extraction we have the following:

DbSet<MedicalReportSection>
    .OrderBy(x => x.Id)
    .Skip(__p_0)
    .Select(a => (MedicalReportSectionFlags)((int)a.Flags & __p_0))

When this gets translated to sql we get:

SELECT [m].[Flags] & @__p_0
FROM [MedicalReportSections] AS [m]
ORDER BY [m].[Id]
OFFSET @__p_0 ROWS

However, the type mapping of parameter in the projection is sql int, backed by clr MedicalReportSectionFlags (inferred from the column). When we visit the projection, this is the parameter that gets added to Parameters collection on RelationalCommand. However the parameterValues contains a value typed as clr int, so when we try to add that value to the parameter collection, exception is thrown