BUG: Entity Framework Core ValueConverters are ignored when querying polymorphic entities

I have an Entity Framework DbContext where some of the entities are just a single class, others are members of a class hierarchy.
I also have a type named Status, for which I defined a ValueConverter convention in DbContext. Both the single class entity, and one of classes in the hierarchy have properties of type Status.
When I query the DbSet for the single class, and specify a Where clause that references the property that should be value converted, everything’s fine.
But, if I query the DbSet for the class hierarchy, specifying OfType<>() and Where(), and referencing the property that should be value converted, the generated MQL ignores the value conversion, and the query fails. Proof of concept below; I can provide an entire, running .NET project if you need it.
Using .NET 8, MongoDB.EntityFrameworkCore 8.2.0, MongoDB Community 7.0.14

public class CarBrand
{
    public Guid Id { get; set; }
    public string Name { get; set; } = string.Empty;
    public Status Status { get; set; } = Status.UNKNOWN;
}

public class Person
{
    public Guid Id { get; set; }
    public string Name { get; set; } = string.Empty;
}

public class Teacher : Person
{
    public int NumberOfStudents { get; set; }
}

public abstract class Doctor : Person
{
    public string LicenseNumber { get; set; } = string.Empty;
    public Status Status { get; set; } = Status.UNKNOWN;
}

public class SpecialistDoctor : Doctor
{
    public string Speciality { get; set; } = string.Empty;
}

public class TraineeDoctor : Doctor
{
    public string MentorName { get; set; } = string.Empty;
}

public class Status
{
    public static readonly Status UNKNOWN = new(nameof(UNKNOWN));
    public static readonly Status INACTIVE = new(nameof(INACTIVE));
    public static readonly Status ACTIVE = new(nameof(ACTIVE));

    public static IEnumerable<Status> GetAll()
    {
        yield return UNKNOWN;
        yield return INACTIVE;
        yield return ACTIVE;
    }

    public static Status FromName(string name)
    {
        foreach (Status status in GetAll())
        {
            if (status.Name == name)
            {
                return status;
            }
        }

        throw new InvalidOperationException($"No such status '{name}'");
    }

    private Status(string name)
    {
        Name = name;
    }

    public string Name { get; }
}

public class StatusConverter : ValueConverter<Status, string>
{
    public StatusConverter() : base(s => s.Name, s => Status.FromName(s))
    {
    }
}

public class MyDbContext(DbContextOptions options) : DbContext(options)
{
    public DbSet<CarBrand> CarBrands { get; set; }
    public DbSet<Person> Persons { get; init; }

    public static MyDbContext Create(IMongoDatabase database)
    {
        var options = new DbContextOptionsBuilder<MyDbContext>()
            .UseMongoDB(database.Client, database.DatabaseNamespace.DatabaseName)
            .EnableSensitiveDataLogging()
            .LogTo(Console.WriteLine)
            .Options;

        return new MyDbContext(options);
    }

    protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
    {
        base.ConfigureConventions(configurationBuilder);

        configurationBuilder.Properties<Status>().HaveConversion<StatusConverter>();
    }

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

        modelBuilder.Entity<CarBrand>()
            .ToCollection(nameof(CarBrands))
            .HasKey(c => c.Id);

        modelBuilder.Entity<Person>()
            .ToCollection(nameof(Persons))
            .HasKey(p => p.Id);

        modelBuilder.Entity<Teacher>();
        modelBuilder.Entity<Doctor>();
        modelBuilder.Entity<TraineeDoctor>();
        modelBuilder.Entity<SpecialistDoctor>();
    }
}

// Find active car brands - works correctly
var activeBrands = dbContext.CarBrands.Where(d => d.Status == Status.ACTIVE).ToList();
// Generated MQL query (correct):
// myDatabase.CarBrands.aggregate([{ "$match" : { "Status" : "ACTIVE" } }])

// Find active doctors - FAILS
var activeDoctors = dbContext.Persons.OfType<Doctor>().Where(d => d.Status == Status.ACTIVE).ToList();
// Generated MQL query (WRONG) - notice the Status match:
// myDatabase.Persons.aggregate([{ "$match" : { "_t" : { "$in" : ["Doctor", "SpecialistDoctor", "TraineeDoctor"] } } }, { "$match" : { "Status" : { } } }])

Hi @Jorge_Aguiar,

Welcome to the MongoDB Community Forums! Thanks for raising this. I filed this ticket for us to take a look at this and we will be investigating it soon.

Thanks,

Rishit.

Fixed in version 8.2.1. Thank you @Rishit_Bhatia !

1 Like

This topic was automatically closed 5 days after the last reply. New replies are no longer allowed.