efcore icon indicating copy to clipboard operation
efcore copied to clipboard

Cosmos SQL query enum array (Contains) to string conversion not working properly

Open Kevenvz opened this issue 1 year ago • 4 comments

I have an issue with the .Contains query translation in Cosmos. The problem is as follows: My entity has an enum discriminator type, I want to serialize this to a string for better readability. Now when I want to apply .Contains in my query the enum gets serialized as an array of integers instead of an array of strings.

The logs show the following query

info: Microsoft.EntityFrameworkCore.Database.Command[30100] Executing SQL query for container 'Products' in partition 'None' [Parameters=[@__types_0='[1,2]']] SELECT VALUE c FROM root c WHERE (c["Type"] IN ("Menu", "Regular", "Supplement") AND ARRAY_CONTAINS(@__types_0, c["Type"]))

Snippet of the related code

public enum ProductType
{
    Supplement,
    Menu,
    Regular
}

public abstract class Product
{
    public ProductType Type { get; init; }
}

public class MenuProduct : Product
{
    public MenuProduct()
    {
        Type = ProductType.Menu;
    }
}

public class Supplement : Product
{
    public MenuProduct()
    {
        Type = ProductType.Supplement;
    }
}

public class RegularProduct : Product
{
    public RegularProduct()
    {
        Type = ProductType.Regular;
    }
}

public class ProductConfiguration : IEntityTypeConfiguration<Product>
{
    public void Configure(EntityTypeBuilder<Product> builder)
    {
        builder.HasDiscriminator(x => x.Type)
            .HasValue<RegularProduct>(ProductType.Regular)
            .HasValue<Supplement>(ProductType.Supplement)
            .HasValue<MenuProduct>(ProductType.Menu);

        builder.Property(x => x.Type)
            .HasConversion<string>();
        // Rest is left out
    }
}

ProductType[] types = [ProductType.Menu, ProductType.Regular];
Product[] results = await dbContext.Products
    .Where(p => types.Contains(p.Type))
    .ToArrayAsync();

EF Core version: 9.0.0 Database provider: Microsoft.EntityFrameworkCore.Cosmos Target framework: .NET 9 Operating system: Windows and Linux (Docker) IDE: JetBrains Rider 2024.2.6

Kevenvz avatar Nov 13 '24 12:11 Kevenvz

Confirmed this is a bug on EF9--full code below. This is a regression from EF8 where we were still inlining constants for Contains:

Query generated by EF8:

info: 11/13/2024 14:22:50.123 CosmosEventId.ExecutingSqlQuery[30100] (Microsoft.EntityFrameworkCore.Database.Command) 
      Executing SQL query for container 'SomeDbContext' in partition '(null)' [Parameters=[]]
      SELECT c
      FROM root c
      WHERE (c["Type"] IN ("Menu", "Regular", "Supplement") AND c["Type"] IN ("Menu", "Regular"))

Query generated by EF9:

info: 11/13/2024 14:24:44.949 CosmosEventId.ExecutingSqlQuery[30100] (Microsoft.EntityFrameworkCore.Database.Command) 
      Executing SQL query for container 'SomeDbContext' in partition 'None' [Parameters=[@__types_0='[1,2]']]
      SELECT VALUE c
      FROM root c
      WHERE (c["Type"] IN ("Menu", "Regular", "Supplement") AND ARRAY_CONTAINS(@__types_0, c["Type"]))
Code
using (var context = new SomeDbContext())
{
    await context.Database.EnsureDeletedAsync();
    await context.Database.EnsureCreatedAsync();

    context.AddRange(new MenuProduct { Id = "A" }, new Supplement { Id = "B" }, new RegularProduct { Id = "C" });

    await context.SaveChangesAsync();
}

using (var context = new SomeDbContext())
{
    ProductType[] types = [ProductType.Menu, ProductType.Regular];
    Product[] results = await context.Products
        .Where(p => types.Contains(p.Type))
        .ToArrayAsync();
}

public class SomeDbContext : DbContext
{
    public DbSet<Product> Products => Set<Product>();

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        => optionsBuilder
            .EnableSensitiveDataLogging()
            .LogTo(Console.WriteLine)
            .UseCosmos(
                "https://localhost:8081",
                "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==",
                "PrimitiveCollections");

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Product>(b =>
        {
            b.HasDiscriminator(x => x.Type)
                .HasValue<RegularProduct>(ProductType.Regular)
                .HasValue<Supplement>(ProductType.Supplement)
                .HasValue<MenuProduct>(ProductType.Menu);

            b.Property(x => x.Type)
                .HasConversion<string>();
        });
    }
}

public enum ProductType
{
    Supplement,
    Menu,
    Regular
}

public abstract class Product
{
    public string Id { get; set; }
    public ProductType Type { get; init; }
}

public class MenuProduct : Product
{
}

public class Supplement : Product
{
}

public class RegularProduct : Product
{
}

ajcvickers avatar Nov 13 '24 14:11 ajcvickers

If anyone is looking for a workaround:

ProductType[] types = [ProductType.Menu, ProductType.Regular];
string[] stringTypes = types.Select(t => t.ToString()).ToArray();
Product[] results = await dbContext.Products
    .Where(p => stringTypes.Contains(EF.Property<string>(p, "Type")))
    .ToArrayAsync();

Kevenvz avatar Nov 13 '24 16:11 Kevenvz

Note for anyone looking for a workaround, using an object array for the parameters also works:

    object[] types = ["Menu", "Regular"];
    Puffin[] results = await context.Products
        .Where(p => types.Contains(p.Type))
        .ToArrayAsync();

ajcvickers avatar Nov 27 '24 16:11 ajcvickers

Note for team: a proper fix for this requires implementation of #34026. Initial investigation of this issue is not showing any promising paths for a patch-safe fix, but there is a pretty good workaround.

ajcvickers avatar Nov 27 '24 16:11 ajcvickers