Skip to content

LINQ "Contains" fails to generate SQL with custom converter #32376

@Arjan321

Description

@Arjan321

After upgrading to EF Core 8, my custom converter for Enum no longer work. For readability in the database, we have decided to use nvarchar(1) fields to represent enum values. It's up to the developer to define some sort of char per enum field, for example

public enum EnumProperty { FieldA = 'A', FieldB = 'B' }

This was working fine with EF Core 6 and EF7, using a custom converter:

internal class EnumValueConverter<T> : ValueConverter<T, char> where T : Enum, IConvertible
{
    public EnumValueConverter() : base(p => p.ToChar(null), p => (T)Enum.Parse(typeof(T), Convert.ToInt32(p).ToString())) { }
}

Stacktrace

With the new Contains LINQ conversion and OPENJSON syntax however, EF8 fails to generate SQL and instead crashes with the following exception:

System.InvalidCastException
  HResult=0x80004002
  Message=Unable to cast object of type 'System.String' to type 'System.Char'.
  Source=Microsoft.EntityFrameworkCore
  StackTrace:
   at Microsoft.EntityFrameworkCore.Storage.Json.JsonConvertedValueReaderWriter`2.ToJsonTyped(Utf8JsonWriter writer, TModel value)
   at Microsoft.EntityFrameworkCore.Storage.Json.JsonCollectionReaderWriter`3.ToJsonTyped(Utf8JsonWriter writer, IEnumerable`1 value)
   at Microsoft.EntityFrameworkCore.Storage.Json.JsonValueReaderWriter`1.ToJson(Utf8JsonWriter writer, Object value)
   at Microsoft.EntityFrameworkCore.Storage.Json.JsonValueReaderWriter.ToJsonString(Object value)
   at Microsoft.EntityFrameworkCore.Storage.ValueConversion.ValueConverter`2.<>c__DisplayClass6_0`2.<SanitizeConverter>b__1(Object v)
   at Microsoft.EntityFrameworkCore.Storage.RelationalTypeMapping.CreateParameter(DbCommand command, String name, Object value, Nullable`1 nullable, ParameterDirection direction)
   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.CreateDbCommand(RelationalCommandParameterObject parameterObject, Guid commandId, DbCommandMethod commandMethod)
   at Microsoft.EntityFrameworkCore.Query.Internal.SingleQueryingEnumerable`1.CreateDbCommand()
   at Microsoft.EntityFrameworkCore.Query.Internal.SingleQueryingEnumerable`1.ToQueryString()
   at Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions.ToQueryString(IQueryable source)
   at Program.<<Main>$>d__0.MoveNext() in Program.cs:line 13
   at Program.<<Main>$>d__0.MoveNext() in Program.cs:line 14
   at Program.<Main>(String[] args)

This can be worked around by falling back to options.UseCompatibilityLevel(120); and not using OPENJSON.

Include your code

using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;

await using var context = new BlogContext();
await context.Database.EnsureDeletedAsync();
await context.Database.EnsureCreatedAsync();

var items = new List<EnumProperty>() { EnumProperty.FieldA, EnumProperty.FieldB };
var query = context.Set<Person>().Where(p => items.Contains(p.EnumProperty)).ToQueryString();
Console.WriteLine(query);

public class BlogContext : DbContext
{
    DbSet<Person> People => Set<Person>();

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);

        modelBuilder.Entity<Person>().Property(p => p.EnumProperty).HasConversion(new EnumValueConverter<EnumProperty>());
    }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        base.OnConfiguring(optionsBuilder);

        optionsBuilder.UseSqlServer("Data Source=.;Initial Catalog=BugRepo;Integrated Security=True;TrustServerCertificate=True");
    }
}

internal class EnumValueConverter<T> : ValueConverter<T, char> where T : Enum, IConvertible
{
    public EnumValueConverter()
        : base(p => p.ToChar(null), p => (T)Enum.Parse(typeof(T), Convert.ToInt32(p).ToString()))
    {
    }
}

public class Person
{
    public int Id { get; set; }

    public EnumProperty EnumProperty { get; set; }
}

public enum EnumProperty
{
    FieldA = 'A',
    FieldB = 'B',
    FieldC = 'C',
}

Include verbose output

Using assembly 'EfCoreContainsBug'.
Using startup assembly 'EfCoreContainsBug'.
Using root namespace 'EfCoreContainsBug'.
Remaining arguments: .
Finding DbContext classes...
Finding IDesignTimeDbContextFactory implementations...
Finding application service provider in assembly 'EfCoreContainsBug'...
Finding Microsoft.Extensions.Hosting service provider...
No static method 'CreateHostBuilder(string[])' was found on class 'Program'.
No application service provider was found.
Finding DbContext classes in the project...
Found DbContext 'BlogContext'.
BlogContext

Include provider and version information

EF Core version: 8.0.0
Database provider: Microsoft.EntityFrameworkCore.SqlServer
Target framework: .NET 8.0
Operating system: Win10
IDE: Visual Studio 2022 17.8

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions