Search code examples
c#asp.net-coreentity-framework-coreef-database-first

Inheritance and navigation properties to child entities


I have a problem with Navigation Properties while I'm using Inheritance (TPH - the only available at the moment in EF Core).

My hierarchy models:

public class Proposal
{
    [Key]
    public int ProposalId { get; set; }

    [Required, Column(TypeName = "text")]
    public string Substantiation { get; set; }

    [Required]
    public int CreatorId { get; set; }

    [ForeignKey("CreatorId")]
    public Employee Creator { get; set; }

}

public class ProposalLeave : Proposal
{
    [Required]
    public DateTime LeaveStart { get; set; }

    [Required]
    public DateTime LeaveEnd { get; set; }
}

public class ProposalCustom : Proposal
{
    [Required, StringLength(255)]
    public string Name { get; set; }

}

And a part of DbContext:

public class AppDbContext : IdentityDbContext<User, Role, int>
{

    public DbSet<Employee> Employee { get; set; }
    public DbSet<Proposal> Proposal { get; set; }

    public AppDbContext(DbContextOptions<AppDbContext> options)
        : base(options)
    {
    }

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

        modelBuilder.Entity<Proposal>()
            .HasDiscriminator<string>("proposal_type")
            .HasValue<Proposal>("proposal_base")
            .HasValue<ProposalCustom>("proposal_custom")
            .HasValue<ProposalLeave>("proposal_leave");

    }
}

Ok, let's get to the point. As you can see inside parent Proposal model I have property CreatorId - reference to Employee entity. Inside Employee model I want to have two navigation properties to load created Proposals of child types as below:

public class Employee
{
    public ICollection<ProposalCustom> CreatedProposalCustoms { get; set; } 
    public ICollection<ProposalLeave> CreatedProposalLeaves { get; set; } 

}

But it causes migration errors. After I applied migration I have two references to Employee entity (CreatorId, EmployeeUserId) inside Proposal table instead of one (CreatorId). When I changed navigation properties to:

public class Employee
{      
    public ICollection<Proposal> CreatedProposals { get; set; } 
}

model was correct (there was only one reference to Employee inside Proposal table), but I still can't Include() to Employee model CreatedProposalCustoms and CreatedProposalLeaves separately.

The problem is probably inside my DbContext configuration, but I have no idea how to setup it correctly :/


Solution

  • The problem is that when you have to navigation properties, EF Core will also create two foreign keys as you already found out.

    One workaround could be to have non-mapped navigation properties, which just wrap the casting of your collection with the base class.

    public class Employee
    {
        public IDbSet<Proposal> Proposals { get; set; } 
        [NotMapped]
        public IQueryable<ProposalCustom> CreatedProposalCustoms { get; } => Proposals.OfType<ProposalCustom>();
        [NotMapped]
        public IQueryable<ProposalLeave> CreatedProposalLeaves { get; } => Proposals.OfType<ProposalLeave>();
    }
    

    Where the two non-mapped properties would just act as shorthand for Proposals.OfType<T>()

    Or if you want it more generic:

    public class Employee
    {
        public IDbSet<Proposal> Proposals { get; set; } 
        public IQueryable<T> AllProposals<T>() where T :Proposal => Proposals.OfType<T>();
    }
    

    then use it as employee.AllProposals<ProposalLeave>().Where(p => p.LeaveStart >= DateTime.Now).ToListAsync().