In an effort to avoid the use of Table Per Hierarchy (TPH) I have been looking at examples of how best to implement Table-Per-Concrete Class (TPC) inheritance in my database model. I came across the official documentation and this article.
Below are some mock-up classes with some simple inheritance.
public class BaseEntity
{
public BaseEntity()
{
ModifiedDateTime = DateTime.Now;
}
[Key]
[DatabaseGeneratedAttribute(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
public DateTime ModifiedDateTime { get; set; }
}
public class Person : BaseEntity
{
public string FirstName { get; set; }
public string LastName { get; set; }
}
public class Business : BaseEntity
{
public string Name { get; set; }
public string Location { get; set; }
}
The DbModelBuilder configurations used per the examples in both articles.
modelBuilder.Entity<BaseEntity>()
.Property(c => c.Id)
.HasDatabaseGeneratedOption(DatabaseGeneratedOption.None);
modelBuilder.Entity<Person>().Map(m =>
{
m.MapInheritedProperties();
m.ToTable("Person");
});
modelBuilder.Entity<Business>().Map(m =>
{
m.MapInheritedProperties();
m.ToTable("Business");
});
The application runs successfully but when I go back to the database I find three (3) tables instead of the two (2) I expected to find. After a bit of testing it would appear the "BaseEntity" table is created but is never used. Everything seems to work just fine with the exception of this empty orphaned table.
I mess around with the DbModelBuilder configurations, eventually removing the "BaseEntity" configurations which provides the expected result; Two (2) tables, each of them having the correct properties and functioning correctly.
I do one last test, rip out all the DbModelBuilder configurations, only include the two (2) DbSet properties for "Person" and "Business" and test again.
public DbSet<Person> People { get; set; }
public DbSet<Business> Businesses { get; set; }
To my surprise the project builds, goes out to the database, creates only the two tables with all the class properties including the inherited ones from the "BaseEntity" class. I can do CRUD operations without issue.
After running many tests I can't find any issues with the final test and I have not been able to reproduce the duplicate key error both articles warned about.
The changes to the database were committed successfully, but an error occurred while updating the object context. The ObjectContext might be in an inconsistent state. Inner exception message: AcceptChanges cannot continue because the object's key values conflict with another object in the ObjectStateManager. Make sure that the key values are unique before calling AcceptChanges.
TPH inheritance uses one database table to maintain data for all of the entity types in an inheritance hierarchy. In this walkthrough we will map the Person table to three entity types: Person (the base type), Student (derives from Person), and Instructor (derives from Person).
Table-per-hierarchy and discriminator configuration. By default, EF maps the inheritance using the table-per-hierarchy (TPH) pattern. TPH uses a single table to store the data for all types in the hierarchy, and a discriminator column is used to identify which type each row represents.
Inheritance with EF Code First: Table per Hierarchy (TPH)
I use mapping classes, but never-mind. I solve it like this:
public class PersonMap : EntityTypeConfiguration<Person>
{
public PersonMap()
{
Map(m => { m.ToTable("Person"); m.MapInheritedProperties(); });
HasKey(p => p.Id);
Property(p => p.Id).HasDatabaseGeneratedOption(DatabaseGeneratedOption.None);
}
}
Remember - base class must be abstract.
Just to make this all simpler, I've moved the code necessary to force TablePerConcrete to open source. Its purpose is to allow features normally only available in the Fluent Interface (where you have to scatter a lot of code into your Db class' OnModelCreating method) to migrate over to Attribute-based features.
It allows you to do things like this:
[TablePerConcrete]
public class MySubclassTable : MyParentClassEntity
Forcing TPC regardless of what EF might decide to infer from your parent class/subclass relationship.
One interesting challenge here is that sometimes EF will screw up an inherited Id property, setting it to be filled with an explicit value rather than being database-generated. You can ensure it doesn't do that by having the parent class implement interface IId
(which just says: This has an Id property), then marking the subclasses with [ForcePKId]
.
public class MyParentClassEntity : IId
{
public int Id { get; set; }
. . .
[TablePerConcrete]
[ForcePKId]
public class MySubclassTable : MyParentClassEntity
{
// No need for PK/Id property here, it was inherited and will work as
// you intended.
Kicking off the code that handles all this for you is pretty simple - just add a couple lines to your Db class:
public class Db : DbContext
{
. . .
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
var modelsProject = Assembly.GetExecutingAssembly();
B9DbExtender.New().Extend(modelBuilder, modelsProject);
You can access it one of 2 ways:
Via a single gist with all the relevant classes copy-pasted into a single file, here: https://gist.github.com/b9chris/8efd30687d554d1ceeb3fee359c179f9
Via a library, our Brass9.Data, which we're releasing open source. It has a lot of other EF6 tools in it, like Data Migrations. It's also more organized, with classes broken out into separate files as you'd normally expect: https://github.com/b9chris/Brass9.Data
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With