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

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

Closed
Arjan321 opened this issue Nov 21, 2023 · 3 comments · Fixed by #32444
Closed

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

Arjan321 opened this issue Nov 21, 2023 · 3 comments · Fixed by #32444
Assignees
Labels
area-type-mapping closed-fixed The issue has been fixed and is/will be included in the release indicated by the issue milestone. customer-reported regression Servicing-approved type-bug
Milestone

Comments

@Arjan321
Copy link

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

@Arjan321
Copy link
Author

By converting to string instead of char it seems to work again:

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

Since then you will get a nvarchar(max), the column type can be configured explicity

        modelBuilder.Entity<Person>().Property(p => p.EnumProperty)
            .HasConversion(new EnumValueConverter<EnumProperty>())
            .HasColumnType("nvarchar(1)");

@roji
Copy link
Member

roji commented Nov 21, 2023

Thanks @Arjan321, I'll investigate this.

@ajcvickers
Copy link
Member

@roji I haven't looked yet, but I suspect this might be due to conversions on top of conversions. That is, char is not supported by SQL Server, so it gets value converted to string. But then this code is composing another, custom converter, on top of the on supplied by the provider, and that whole thing is being used by the collection reader/writer.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-type-mapping closed-fixed The issue has been fixed and is/will be included in the release indicated by the issue milestone. customer-reported regression Servicing-approved type-bug
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants