Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Exception when doing a Contains query against an enum field based on byte #1189

Closed
redoz opened this issue Jan 9, 2020 · 10 comments · Fixed by #1196
Closed

Exception when doing a Contains query against an enum field based on byte #1189

redoz opened this issue Jan 9, 2020 · 10 comments · Fixed by #1196
Assignees
Labels
bug Something isn't working
Milestone

Comments

@redoz
Copy link

redoz commented Jan 9, 2020

We just upgraded to EFCore 3.1 which surfaced this issue.

Minimal reproduction:

Repro.csproj:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>netcoreapp3.1</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.EntityFrameworkCore" Version="3.1.0" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="3.1.0">
      <PrivateAssets>all</PrivateAssets>
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
    </PackageReference>
    <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="3.1.0" />
  </ItemGroup>

</Project>

Program.cs

using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;

namespace Repro
{
    class Program
    {
        static async Task Main(string[] args)
        {
            await using var dbContext = new MyContext();

            SomeEnum[] possibleValues = { SomeEnum.One, SomeEnum.Three };
            var result = await dbContext.My.Where(e => possibleValues.Contains(e.SomeByteValue)).ToListAsync();
        }
    }

    public enum SomeEnum : byte
    {
        One = 1,
        Two = 2,
        Three = 3
    }

    public class MyEntity
    {
        [Key]
        [DatabaseGenerated(DatabaseGeneratedOption.None)]
        public int TheKey { get; set; }
        public SomeEnum SomeByteValue { get; set; }
    }

    public class MyContext : DbContext
    {
        public DbSet<MyEntity> My { get; set; }

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            optionsBuilder.UseNpgsql(@"Server=.");
        }
    }
}

This line:

var result = await dbContext.My.Where(e => possibleValues.Contains(e.SomeByteValue)).ToListAsync();

throws the following exception:

System.InvalidCastException: 'Unable to cast object of type 'Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping.NpgsqlByteArrayTypeMapping' to type 'Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping.NpgsqlArrayTypeMapping'.'

Stacktrace:

This exception was originally thrown at this call stack:
	Npgsql.EntityFrameworkCore.PostgreSQL.Query.Internal.NpgsqlSqlExpressionFactory.ApplyTypeMappingOnArrayAnyAll(Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions.Internal.ArrayAnyAllExpression)
	Npgsql.EntityFrameworkCore.PostgreSQL.Query.Internal.NpgsqlSqlExpressionFactory.ApplyTypeMapping(Microsoft.EntityFrameworkCore.Query.SqlExpressions.SqlExpression, Microsoft.EntityFrameworkCore.Storage.RelationalTypeMapping)
	Microsoft.EntityFrameworkCore.Query.SqlExpressionFactory.ApplyDefaultTypeMapping(Microsoft.EntityFrameworkCore.Query.SqlExpressions.SqlExpression)
	Npgsql.EntityFrameworkCore.PostgreSQL.Query.Internal.NpgsqlSqlExpressionFactory.ArrayAnyAll(Microsoft.EntityFrameworkCore.Query.SqlExpressions.SqlExpression, Microsoft.EntityFrameworkCore.Query.SqlExpressions.SqlExpression, Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions.Internal.ArrayComparisonType, string)
	Npgsql.EntityFrameworkCore.PostgreSQL.Query.ExpressionTranslators.Internal.NpgsqlArrayMethodTranslator.Translate(Microsoft.EntityFrameworkCore.Query.SqlExpressions.SqlExpression, System.Reflection.MethodInfo, System.Collections.Generic.IReadOnlyList<Microsoft.EntityFrameworkCore.Query.SqlExpressions.SqlExpression>)
	Microsoft.EntityFrameworkCore.Query.RelationalMethodCallTranslatorProvider.Translate.AnonymousMethod__1(Microsoft.EntityFrameworkCore.Query.IMethodCallTranslator)
	System.Linq.Enumerable.SelectEnumerableIterator<TSource, TResult>.MoveNext()
	System.Linq.Enumerable.TryGetFirst<TSource>(System.Collections.Generic.IEnumerable<TSource>, System.Func<TSource, bool>, out bool)
	System.Linq.Enumerable.FirstOrDefault<TSource>(System.Collections.Generic.IEnumerable<TSource>, System.Func<TSource, bool>)
	Microsoft.EntityFrameworkCore.Query.RelationalMethodCallTranslatorProvider.Translate(Microsoft.EntityFrameworkCore.Metadata.IModel, Microsoft.EntityFrameworkCore.Query.SqlExpressions.SqlExpression, System.Reflection.MethodInfo, System.Collections.Generic.IReadOnlyList<Microsoft.EntityFrameworkCore.Query.SqlExpressions.SqlExpression>)
    ...
    [Call Stack Truncated]
@roji
Copy link
Member

roji commented Jan 9, 2020

There's some difficulty around byte arrays, since they can map to both PostgreSQL bytea (special type for a BLOB) and to smallint[] (array of smallints). As a workaround I'd recommend simply changing the enum's type to short - PostgreSQL doesn't actually have a byte type, so your byte enum is getting mapped to smallint in any case (or just the default C# enum type, int). Another option could be to map to PostgreSQL enums instead of smallint/int - if this works for you, it would probably be the cleanest solution.

Note: this can be reproduced without enums:

var bytes = new byte[] { 1, 2, 3 };
var result = await dbContext.My.Where(e => bytes.Contains(e.RealByte)).ToListAsync();

@redoz
Copy link
Author

redoz commented Jan 9, 2020

@roji If we were willing to try to create a PR for this, any suggestions where we should start looking?

@roji
Copy link
Member

roji commented Jan 9, 2020

This would basically be an implementation of dotnet/efcore#4601 (already done for SQL Server and Sqlite) for the Npgsql provider. We'd translate Contains over byte arrays in a specific way (by using PostgreSQL position) before the general array logic kicks in. Following what was done for the other providers, we'd add an NpgsqlByteArrayMethodTranslator where that translation would take place.

Good luck and let me know if you need any more guidance! If you end up not having time, please post a note here so I know to take care of it myself.

@HenriKoelewijn
Copy link

HenriKoelewijn commented Jan 9, 2020

A quick workaround is to provide an IList of T i.s.o. a T[] in the where clause:

IList<Status> list = new []{ Status.Enabled, Status.Disabled };
dbContext.My.Where(e => list.Contains(e.SomeByteValue))

@roji
Copy link
Member

roji commented Jan 9, 2020

Good idea!

@redoz
Copy link
Author

redoz commented Jan 9, 2020

@roji As we have an easy workaround for now I think we won't pick this one up, I suspect you'll be able to get this done way faster than we will anyway :)

I appreciate the pointers though, if our workload was a bit lighter we could spend some time investigating getting a real fix done.

@roji
Copy link
Member

roji commented Jan 9, 2020

Sure thing, no problem!

@roji roji unassigned redoz Jan 9, 2020
@roji roji modified the milestones: 3.1.1, 5.0.0 Jan 10, 2020
@roji roji self-assigned this Jan 10, 2020
@roji
Copy link
Member

roji commented Jan 10, 2020

Changing milestone to 5.0.0 as the fix isn't trivial and there's a good workaround.

@freerider7777
Copy link

freerider7777 commented Apr 1, 2021

I have the same issue... Is it fixed only in .Net 5? Workaround with "short" does not work.
List works, thanks, but many changes in code are needed...
Update: with short the exception is in other place :)

@roji
Copy link
Member

roji commented Apr 2, 2021

@freerider7777 this issue is fixed only in EF Core 5.0 (as indicated by the milestone).

If you're seeing other exceptions when using EF Core 5.0 (e.g. with short), can you please open a new issue for that with a code sample?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants