Search code examples
c#entity-frameworkinheritancedowncast

Entity Framework 6.1 Discriminator TPH


I am looking to implement Table-per-Hierarchy using EF6 similar to the instructions found here: example.

I have an abstract base class of User with the following derived types:

  • Student
  • Contact
  • Instructor

When I examine the database table Users the discriminator column value is (Undefined) when I pass a student object into my Save method below. Instead I would expect the value to be Student. Otherwise my data is saved correctly in both the Users and Students tables.

While troubleshooting the problem I added a UserType enumerator Get property to the classes to ensure that I am casting from User to Student.

In my UserRepository class my Save method is below.

    public void Save(User user)
    {
      if (Exists(user.Id))
        UpdateUser(user);
      else
      {
        switch (user.Role)
        {
          case UserType.Role.Base:
           _db.Users.Add(user);
            break;
          case UserType.Role.Student:
           _db.Users.Add(user as Student);
           break;
         case UserType.Role.Instructor:
           _db.Users.Add(user as Instructor);
           break;
         case UserType.Role.Contact:
           _db.Users.Add(user as Contact);
           break;
       }
     }
     _db.SaveChanges();
    }

Failed Alternative

I've tried code like the following to explicitly create a new Student.

    private void MapToStudent(User user)
    {
     _db.Users.Add(new Student()
     {
       FirstName = user.FirstName,
       LastName = user.LastName,
       //...
      });
    }

Question

I am not downcasting correctly? Or rather what is the proper/preferred way to save subclasses using EF?

User Base Class

      public abstract class User
      {
         public int Id { get; set; }
         //...
      }

      internal class UserNotFound: User 
      { 
        public override UserType.Role Role 
        { 
          get 
            { 
             return UserType.Role.Base; 
            } 
        }
      }

      public class Student : User 
      {
         //...
         public override UserType.Role Role 
         {
           get { return UserType.Role.Student; }          
         }
      }

      public class Contact : User 
      {
         //...
         public override UserType.Role Role 
         {
           get { return UserType.Role.Contact; }          
         }
      }

      public class Instructor : User 
      {
         //...
         public override UserType.Role Role 
         {
           get { return UserType.Role.Instructor; }          
         }
      }

DatabaseContext Mapping

      public class DatabaseContext : Context
      {
          protected override void OnModelCreating(DbModelBuilder modelBuilder)
          {
              modelBuilder.Entity<Student>().ToTable("Students");
              modelBuilder.Entity<Contact>().ToTable("Contacts");
              modelBuilder.Entity<Instructor>().ToTable("Instructors");
          }
      }

Solution

  • It appears your mappings are incorrect for TPH. The linked example in your questions shows:

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
      modelBuilder.Entity<BillingDetail>()
        .Map<BankAccount>(m => m.Requires("BillingDetailType").HasValue("BA"))
        .Map<CreditCard>(m => m.Requires("BillingDetailType").HasValue("CC"));
    }
    

    which modeled after your question might look like:

      modelBuilder.Entity<User>()
        .Map<Student>(m => m.Requires("Discriminator").HasValue("STU"))
        .Map<Instructor>(m => m.Requires("Discriminator").HasValue("INS"));