Search code examples
c#entity-frameworkentity-framework-core

Entity Framework Core foreign key naming issue with inheritance


I'm experiencing an unexpected behavior with Entity Framework Core when creating migrations for my Person class hierarchy. Specifically, when I generate a migration for the Person class, I get a foreign key named Student_InstitutionId instead of InstitutionId in the Person table.

public abstract class Person : Aggregate<long>
{
    // ... properties ...
}

public class Student : Person
{
    // ... properties ...
    public long InstitutionId { get; private set; }
    public virtual Institution Institution { get; set; }
}

public class Institution : Aggregate<long>
{
    // ... properties ...
    public virtual ICollection<Student> Students { get; set; }
}

My Fluent API configurations are as follows:

public class PersonEntityConfig : BaseEntityConfig<Person, long>
{
    public override void Configure(EntityTypeBuilder<Person> builder)
    {
        // ... configuration ...
    }
}

public class StudentEntityConfig : BaseEntityConfig<Student, long>
{
    public override void Configure(EntityTypeBuilder<Student> builder)
    {
        // ... configuration ...
        builder.HasOne(a => a.Institution)
               .WithMany(a => a.Students)
               .HasForeignKey(a => a.InstitutionId)
               .OnDelete(DeleteBehavior.NoAction);
    }
}
public class InstitutionEntityConfig : BaseEntityConfig<Institution, long>
{
    #region Fields
    #endregion

    #region Properties
    #endregion

    #region Methods
    public override void Configure(EntityTypeBuilder<Institution> builder)
    {
        base.Configure(builder);
        builder.Property(x => x.Name).HasMaxLength(50).IsRequired();
        builder.Property(x => x.CenterNumber).HasMaxLength(50);
        builder.Property(x => x.EMISNumber).HasMaxLength(50);
        builder.Property(x => x.EmailAddress).HasMaxLength(50);

        builder.HasOne(a => a.Address)
            .WithOne(i => i.Institution)
            .HasForeignKey<Institution>(x => x.AddressId)
            .OnDelete(DeleteBehavior.Cascade);
    }
    #endregion

    #region Constructors
    #endregion
}

The issue seems to be related to how Entity Framework Core handles the discriminator property and the foreign keys in the Person table. How can I ensure that the foreign key is correctly named as InstitutionId instead of Student_InstitutionId?

I expect the foreign key in the Person table to be named InstitutionId, not Student_InstitutionId. When generating a migration, Entity Framework Core creates a foreign key named Student_InstitutionId in the Person table.

Please provide guidance on how to resolve this naming issue and ensure that the foreign key is correctly named as InstitutionId.


Solution

  • After testing this, no, this won't work out of the box with a TPH inheritance structure, code-first or schema-first if some of the inheriting classes have a relationship while others do not. Using the following example:

    [Table("Animals")]
    public abstract class Animal
    {
        [Key]
        public int AnimalId { get; set; }
        public string Name { get; set; }
    }
    
    public class Dog : Animal
    {
        public virtual Owner Owner { get; set; }
    }
    
    public class Cat : Animal
    {
        public virtual Owner Owner { get; set; }
    }
    
    public class Wolf : Animal
    {
    }
    

    with the configuration:

            modelBuilder.Entity<Animal>()
                .HasDiscriminator<string>("AnimalType")
                .HasValue<Dog>("Dog")
                .HasValue<Cat>("Cat")
                .HasValue<Wolf>("Wolf");
            modelBuilder.Entity<Dog>()
                .HasOne(x => x.Owner)
                .WithMany()
                .IsRequired(false)
                .HasForeignKey("OwnerId")
                .HasConstraintName("FK_Dogs_Owners");
            modelBuilder.Entity<Cat>()
                .HasOne(x => x.Owner)
                .WithMany()
                .HasForeignKey("OwnerId")
                .HasConstraintName("FK_Cats_Owners");
    

    Even attempting to "trick" it with separate FK constraints didn't work if we want an owner on Cats & Dogs but not Wolves. It expects a Dog_OwnerId and Cat_OwnerId on the Animal table. If all animals have an owner this is fine, we can put the Owner in Animal and everything works but then Wolves have owners. Either way whether we use a single OwnerId or DogOwnerId/CatOwnerId these FKs would need to be null-able which may not be ideal from a referential integrity PoV if Cats and Dogs are expected to require an owner. With TPH we would need a DogOwnerId and CatOwnerId in the Animal table, otherwise we can "hack" it slightly:

    [Table("Animals")]
    public abstract class Animal
    {
        [Key]
        public int AnimalId { get; set; }
        public string Name { get; set; }
    
        public virtual Owner? Owner { get; set; }
    
    }
    
    public class Dog : Animal
    {
    }
    
    public class Cat : Animal
    {
    }
    
    public class Wolf : Animal
    {
        public new virtual Owner? Owner { get { return null; }; set { if (value != null) throw new InvalidOperationException("Wolves have no owner."); } }
    }
    

    This moves the Owner into Animal to keep the FK association happy, but we overload the Owner in Wolf so that if anything tries to set an owner we throw an exception. This doesn't prevent the database from assigning an owner to a wolf, and EF will happily even query wolves by owner.

    The better solution if some inherited classes have a relationship while others do not, and to enforce referential integrity properly, would be to switch to a TPT inheritance. Here the Animal table we would remove the OwnerId and the Discriminator. We add a Dog table with AnimalId as PK + FK to Animal, and OwnerId FK to owner. We add Cat table with AnimalId as PK + FK to Animal, and OwnerId FK to owner. We add Wolf table with Animal as PK + FK to Animal, no OwnerId. The entities can be updated slightly for their table names:

    [Table("Animals")]
    public abstract class Animal
    {
        [Key]
        public int AnimalId { get; set; }
        public string Name { get; set; }
    }
    
    [Table("Dogs")]
    public class Dog : Animal
    {
        public virtual Owner Owner { get; set; }
    }
    
    [Table("Cats")]
    public class Cat : Animal
    {
        public virtual Owner Owner { get; set; }
    }
    
    [Table("Wolves")]
    public class Wolf : Animal
    {
    }
    

    ... and the mapping:

            modelBuilder.Entity<Animal>()
                .ToTable("Animals")
            modelBuilder.Entity<Dog>()
                .HasOne(x => x.Owner)
                .WithMany()
                .HasForeignKey("OwnerId");
            modelBuilder.Entity<Cat>()
                .HasOne(x => x.Owner)
                .WithMany()
                .HasForeignKey("OwnerId");
    

    The catch here is that we can use the [Table("Dogs")] attribute for the sub-class tables, but we still need the explicit .ToTable("Animals") for the base class. This seems to be a requirement for TPT to be recognized. Just using the [Table] attribute seems to have EF expecting to use TPC inheritance.