1

I use EF core to create my tables (code first) and I'm encountering strange behaviour with my abstract base classes for some entities. I have the class ConfigurableDiscountas an abstract base class and multiple classes inheriting from it, for example: AfterSalesCoverage. The classes look like this:

public abstract class ConfigurableDiscount : TenantEntity, TraceChangesEntity
{
    public DealType DealType { get; set; }
    [Required]
    [Column(TypeName = "decimal(18,4)")]
    public decimal Discount { get; set; }
}


public class AfterSalesCoverage : ConfigurableDiscount
{
    public Guid Id { get; set; }
    [Required]
    public string Name { get; set; }
}

The TenantEntity and TraceChangesEntity are another abstract class and an empty interface, respectively.

public abstract class TenantEntity : BaseDeleteEntity
{
    public Guid BusinessUnitId { get; set; }
    public virtual BusinessUnit BusinessUnit { get; set; }
}
public abstract class BaseDeleteEntity : BaseEntity
{
    public DateTime? DeletedOn { get; set; }
}
public abstract class BaseEntity
{
    [Required]
    public DateTime CreatedOn { get; set; }
    public Guid CreatedById { get; set; }
    public virtual User CreatedBy { get; set; }
    public DateTime? ChangedOn { get; set; }
    public Guid? ChangedById { get; set; }
    public virtual User ChangedBy { get; set; }
}

So as you can see, all of these are abstract except the AfterSalesCoverage. If I try to build or migrate, I get the following error:

The entity type 'ConfigurableDiscount' requires a primary key to be defined.

If I do so (either defining a dummy key inside ConfigurableDiscount or shifting the Id from AfterSalesCoverage to ConfigurableDiscount) and try to migrate, I get the following error:

The filter expression 'entity => (entity.DeletedOn == null)' cannot be specified for entity type 'AfterSalesCoverage'. A filt er may only be applied to the root entity type in a hierarchy.

Which makes me wonder why the AfterSalesCoverage isn't the root entity? The filter expression is defined/configured in the DBContext.

EDIT: The filter in the DBContext is called inside of OnModelCreating(ModelBuilder builder) and looks like this:

private void SetSoftDeleteFilterQuery(ModelBuilder builder)
    {
        System.Collections.Generic.IEnumerable<Type> q = from t in Assembly.GetExecutingAssembly().GetTypes()
                                                         where t.IsClass && t.IsSubclassOf(typeof(BaseDeleteEntity))
                                                         select t;
        foreach (Type type in q)
        {
            MethodInfo method = typeof(CPEDbContext).GetMethod("ApplySoftDeleteFilterQuery", BindingFlags.NonPublic | BindingFlags.Instance);
            MethodInfo generic = method.MakeGenericMethod(type);
            if (type.Name != "RequestWrapper" && type.Name != "TenantEntity" && type.Name != "BaseBenchmarkDiscount")
            {
                {
                    generic.Invoke(this, new[] { builder });
                }
            }
        }
    }

private void ApplySoftDeleteFilterQuery<Tdb>(ModelBuilder builder) where Tdb : BaseDeleteEntity
    {
        builder.Entity<Tdb>().HasQueryFilter(entity => entity.DeletedOn == null);
    }

It seems like this filter is the center of interest in this case...

Question: Why does the ConfigurableDiscount need a primary key? It's not supposed to be an actual entity. If a inherit directly from TenantEntity, I don't need an ID in the TenantEntity either... The ConfigurableDiscount entity is referenced nowhere but in the inheriting classes. So there is no DBSet or anything like that in my DBContext.

EngJon
  • 987
  • 8
  • 20
  • It it's not referenced, then check the fluent configuration(s) for statement like `modelBuilder.Entity`. In case you are using `IEntityTypeConfiguration` classes, makes sure they are generic - see [Entity Framework Core 2.0: How to configure abstract base class once](https://stackoverflow.com/questions/49990365/entity-framework-core-2-0-how-to-configure-abstract-base-class-once/49997115#49997115) – Ivan Stoev Jul 09 '19 at 16:38
  • How did you define the global query filter? – Gert Arnold Jul 09 '19 at 19:21
  • @GertArnold I added the soft filter from the db context. It oddly seems like editing this one is doing the trick since I am able to execute the migration with the ConfigurableDiscount added to the if clause.... – EngJon Jul 10 '19 at 07:16
  • @IvanStoev Thanks for the hints, but there is no reference at all for the ConfigurableDiscount. As I pointed out in the comment above, editing the soft delete filter to exclude this class as well (like TenantEntity, BaseBenchmarkDiscount and RequestWrapper) made the migration work. Does anyone know WHY exactly it's working that way? Would be glad to not just take it like this but know exactly the reasons behind this behaviour. – EngJon Jul 10 '19 at 07:18
  • 2
    The reason is that in fact you *were* calling `modelBuilder.Entity()` passing `ConfigurableDiscount` as `T` - not directly, but via reflection. Doing so is one of the ways to tell EF that the class is *entity*. You can avoid such issues by correctly filtering the types you pass to the common configuration generic methods like your `ApplySoftDeleteFilterQuery`, for instance adding `&& !t.IsAbstract` to the criteria, or better, don't use reflection but EF Core model API, e.g. `from t in builder.Model.GetEntityTypes() where t.ClrType … select t.ClrType`. – Ivan Stoev Jul 10 '19 at 08:13
  • If you want to know why the call to `Entity()` method marks the `T` as entity, see [Including & Excluding Types - Conventions](https://learn.microsoft.com/en-us/ef/core/modeling/included-types#conventions) - "*In addition, types that are mentioned in the `OnModelCreating` method are also included."*. If course they could have written it better - *"are mentioned*" means used in `Entity` (and some other fluent) calls. – Ivan Stoev Jul 10 '19 at 08:18
  • @IvanStoev thanks, that sounds reasonable. If you'd bother to conclude this in an answer, I'd mark it as correct. – EngJon Jul 10 '19 at 09:35

0 Answers0