Search code examples
c#entity-frameworkef-fluent-apitph

Unable to create a TPH using Fluent API on EF 6


I modeling a Google Forms-like project. The bellow entities are pretty simple and straightforward (I guess), as follows.

Question types:

// Base class for any kind of question
public abstract class Question : Bean
{
    public string Statement { get; set; }
}

// Visual questions are questions where images are answers.
public class VisualQuestion : Question
{
    public virtual VisualAnswer Answer { get; set; }
}

// Discursive questions are questions where big texts are answers.
public class DiscursiveQuestion : Question
{
    public virtual DiscursiveAnswer Answer { get; set; }
}

// Objective questions are questions that can have multiple answers,
// where each of them should be no bigger than 1 character.
public class ObjectiveQuestion : Question
{
    public virtual List<ObjectiveQuestionOption> Options { get; set; }
}

// Options for objective questions.
public class ObjectiveQuestionOption : Question
{
    public int ObjectiveQuestionId { get; set; }

    public virtual ObjectiveQuestion Question { get; set; }

    public virtual ObjectiveAnswer Answer { get; set; }
}

Answer types:

public abstract class Answer : Bean
{
    public int QuestionId { get; set; }
}

public class DiscursiveAnswer : Answer
{
    public string Answer { get; set; }

    public virtual DiscursiveQuestion Question { get; set; }
}

public class ObjectiveAnswer : Answer
{
    public char Answer { get; set; }

    public virtual ObjectiveQuestion Question { get; set; }
}

public class VisualAnswer : Answer
{
    public byte[] Blob { get; set; } // Image answer

    public virtual VisualQuestion Question { get; set; }
}

Where Bean is:

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

For the questions I could have, instead, a single Question object and a QuestionOption for the objective questions. If this is it, we would need all the 3 Answer objects inside Question, which doesnt sound right to me (one would need to recognize the question type and then access its answer member accordingly, like the is and as casts). For a workaround, I decided to split questions into 3 objects, defined above, and have individual Answer members, using the TPH approach.

Everything seems to work with only 1 condition: all the fluent API settings must be done within the void OnModelCreating(DbModelBuilder modelBuilder) of the DbContext class (I have overridden it). This is a problem because I'm sepparating all my configs for each entity object and added them like this:

protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
    // Approach 1: This works
    //modelBuilder.Entity<Question>().Map<DiscursiveQuestion>(p => p.Requires("TP_QUESTION").HasValue("D")).ToTable("TB_QUESTION");
    //modelBuilder.Entity<Question>().Map<ObjectiveQuestion>(p => p.Requires("TP_QUESTION").HasValue("O")).ToTable("TB_QUESTION");
    //modelBuilder.Entity<Question>().Map<VisualQuestion>(p => p.Requires("TP_QUESTION").HasValue("V")).ToTable("TB_QUESTION");

    // Approach 2: This does not work: it complains that 2 of the 3 entities cant share the TB_QUESTION table because "they are not in the same type hierarchy 
    // or do not have a valid one to one foreign key relationship with matching primary keys between them" (???).
    modelBuilder.Configurations.Add(new VisualQuestionConfiguration());
    modelBuilder.Configurations.Add(new ObjectiveQuestionConfiguration());
    modelBuilder.Configurations.Add(new DiscursiveQuestionConfiguration());
}

These are the configuration objects:

public abstract class QuestionConfiguration<T> : EntityTypeConfiguration<T> where T : Question
{
    public QuestionConfiguration()
    {
        Property(p => p.Statement).HasColumnName("STATEMENT");
    }
}

public class DiscursiveQuestionConfiguration : QuestionConfiguration<DiscursiveQuestion>
{
    public DiscursiveQuestionConfiguration()
    {
        Map(p => p.Requires("TP_QUESTION").HasValue("D")).ToTable("TB_QUESTION");
    }
}

public class VisualQuestionConfiguration : QuestionConfiguration<VisualQuestion>
{
    public VisualQuestionConfiguration()
    {
        Map(p => p.Requires("TP_QUESTION").HasValue("V")).ToTable("TB_QUESTION");
    }
}

public class ObjectiveQuestionConfiguration : QuestionConfiguration<ObjectiveQuestion>
{
    public ObjectiveQuestionConfiguration()
    {
        Map(p => p.Requires("TP_QUESTION").HasValue("O")).ToTable("TB_QUESTION");
    }
}

Why does approach 1 work while 2 doesnt?

EDIT:

I removed the configuration inheritance and it "almost" worked (see below). Like this:

public class QuestionConfiguration : EntityTypeConfiguration<Question>
{
    public QuestionConfiguration()
    {
        Property(p => p.Statement).HasColumnName("STATEMENT");

        // Configures the TPH
        Map<VisualQuestion>(p => p.Requires("TYPE").HasValue("Visual").HasMaxLength(10));
        Map<ObjectiveQuestion>(p => p.Requires("TYPE").HasValue("Objective").HasMaxLength(10));
        Map<DiscursiveQuestion>(p => p.Requires("TYPE").HasValue("Discursive").HasMaxLength(10));

        ToTable("TB_QUESTION");
    }
}

public class DiscursiveQuestionConfiguration : Configuration<DiscursiveQuestion>
{
    public DiscursiveQuestionConfiguration()
    {
    }
}

public class VisualQuestionConfiguration : Configuration<VisualQuestion>
{
    public VisualQuestionConfiguration()
    {
    }
}

public class ObjectiveQuestionConfiguration : Configuration<ObjectiveQuestion>
{
    public ObjectiveQuestionConfiguration()
    {
    }
}

public class ObjectiveQuestionOptionConfiguration : Configuration<ObjectiveQuestionOption>
{
    public ObjectiveQuestionOptionConfiguration()
    {
        HasRequired(p => p.Question).WithMany(p => p.Options).HasForeignKey(p => p.ObjectiveQuestionId);

        Property(p => p.ObjectiveQuestionId).HasColumnName("ID_OBJECTIVE_QUESTION");
        Property(p => p.Statement).HasColumnName("STATEMENT"); // <--- This doesnt get mapped! :(

        ToTable("TB_OBJECTIVE_QUESTION_OPTION");
    }
}

And registered them like this:

protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
    // Approach 1: This works
    //modelBuilder.Entity<Question>().Map<DiscursiveQuestion>(p => p.Requires("TP_QUESTION").HasValue("D")).ToTable("TB_QUESTION");
    //modelBuilder.Entity<Question>().Map<ObjectiveQuestion>(p => p.Requires("TP_QUESTION").HasValue("O")).ToTable("TB_QUESTION");
    //modelBuilder.Entity<Question>().Map<VisualQuestion>(p => p.Requires("TP_QUESTION").HasValue("V")).ToTable("TB_QUESTION");

    // Approach 2: This does work too, however ObjectiveQuestionOption* does not inherit the statement column
    modelBuilder.Configurations.Add(new QuestionConfiguration());
    modelBuilder.Configurations.Add(new QuestionOptionConfiguration());
}

Solution

  • The difference is that the first approach tells EF to treat the base abstract class Question as entity (the modelBuilder.Entity<Question>() call) while the second doesn't.

    You need to create and register a separate configuration for Question. Since you would configure all the common properties there, the QuestionConfiguration<T> class is redundant.

    Here is the correct implementation of the second approach.

    Configurations:

    public class QuestionConfiguration : EntityTypeConfiguration<Question>
    {
        public QuestionConfiguration()
        {
            Property(p => p.Statement).HasColumnName("STATEMENT");
            ToTable("TB_QUESTION");
        }
    }
    
    public class DiscursiveQuestionConfiguration : EntityTypeConfiguration<DiscursiveQuestion>
    {
        public DiscursiveQuestionConfiguration()
        {
            Map(p => p.Requires("TP_QUESTION").HasValue("D"));
        }
    }
    
    public class VisualQuestionConfiguration : EntityTypeConfiguration<VisualQuestion>
    {
        public VisualQuestionConfiguration()
        {
            Map(p => p.Requires("TP_QUESTION").HasValue("V"));
        }
    }
    
    public class ObjectiveQuestionConfiguration : EntityTypeConfiguration<ObjectiveQuestion>
    {
        public ObjectiveQuestionConfiguration()
        {
            Map(p => p.Requires("TP_QUESTION").HasValue("O"));
        }
    }
    

    Registering:

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        modelBuilder.Configurations.Add(new QuestionConfiguration());
        modelBuilder.Configurations.Add(new VisualQuestionConfiguration());
        modelBuilder.Configurations.Add(new ObjectiveQuestionConfiguration());
        modelBuilder.Configurations.Add(new DiscursiveQuestionConfiguration());
    }