default0
default0

Reputation: 105

Why does EntityFramework 6 not support explicitly filtering by Discriminator?

The following is a small EF6 Program to demonstrate the issue.

public abstract class Base
{
    public int Id { get; set; }

    public abstract int TypeId { get; }
}
public class SubA : Base
{
    public override int TypeId => 1;
}
public class SubAA : SubA
{
    public override int TypeId => 2;
}
public class SubB : Base
{
    public override int TypeId => 3;
}
public class SubC : Base
{
    public override int TypeId => 4;
}

public class DevartContext : DbContext
{
    public virtual DbSet<Base> Bases { get; set; }

    public DevartContext()
    {

    }
    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);

        modelBuilder.Entity<Base>()
            .Map<SubA>(x => x.Requires(nameof(SubA.TypeId)).HasValue(1))
            .Map<SubAA>(x => x.Requires(nameof(SubAA.TypeId)).HasValue(2))
            .Map<SubB>(x => x.Requires(nameof(SubB.TypeId)).HasValue(3))
            .Map<SubC>(x => x.Requires(nameof(SubC.TypeId)).HasValue(4));
    }
}

public class Program
{
    public static void Main(string[] args)
    {
        using (DevartContext ctx = new DevartContext())
        {
            // prevent model-changes from wrecking the test
            ctx.Database.Delete();
            ctx.Database.Create();

            var result = ctx.Bases.Where(x => x.TypeId == 1);
            // throws on materialization, why?
            foreach (var entry in result)
            {
                Console.WriteLine(entry);
            }
        }
        Console.ReadLine();
    }
}

The jist of it is this: We have a TPH-Model with an explicitly configured Discriminator (TypeId in this case). We then try to query a specific subtype using that TypeId because using the is operator in our hypothetical example would also return SubAAs, not just SubAs.

I could obviously modify the above to something like Where(x => x is SubA && !(x is SubAA)) but this is obviously going to break as soon as I add SubAB, and automating this by building an exact-filter-linq-to-entities-helper-method is obviously really slow because that method has to do a decent amount of reflection. Not to mention that the generated SQL of the above is horrendous because EF/My SQL Provider does not optimize it properly.

Now trying to do the above results in a NotSupportedException being thrown when the query gets materialized, which basically states that because TypeId is not a member of the Entity, I cannot use it for filtering.

I went to look around for ways to circumvent this, but the best thing I could find was a snippet for automatically generating the Where(x => x is SubA && !(x is SubAA)) version to solve the problem, which is likely to be what I will have to do to get around this.

So my question is: Why does EntityFramework not support this?

Upvotes: 3

Views: 1107

Answers (1)

Bassam Alugili
Bassam Alugili

Reputation: 17033

This soultion working excatly as you wish donot change anything ^^ "never change a running system" :)

You can use enum instead of integers, this give your code more type safety!

static void Main(string[] args)
{
  using (DevartContext ctx = new DevartContext())
  {
    // prevent model-changes from wrecking the test
    ctx.Database.Delete();
    ctx.Database.Create();
    ctx.Bases.Add(new SubA());
    ctx.Bases.Add(new SubAA());
    ctx.Bases.Add(new SubB());

    ctx.SaveChanges();

    var result = ctx.Bases.Where(x => x.TypeId == 1);
    // throws on materialization, why?
    foreach (var entry in result)
    {
      Console.WriteLine(entry);
    }
  }
   Console.ReadLine();
  }


public abstract class Base
{
  public int Id { get; set; }

  public virtual int TypeId { get; protected set; } 
}
public class SubA : Base
{
  public override int TypeId { get;protected set; } = 1;
}
public class SubAA : SubA
{
  public override int TypeId { get; protected set; } = 2;
}
public class SubB : Base
{
  public override int TypeId { get; protected set; } = 3;
}
public class SubC : Base
{
  public override int TypeId { get; protected set; } = 4;
}

public class DevartContext : DbContext
{
  public DbSet<Base> Bases { get; set; }

  public DevartContext()
  {
  }
}

Result in DB:

Id  TypeId  Discriminator
1   1       SubA
2   2       SubAA
3   3       SubB

Upvotes: 1

Related Questions