Introduction
Inheritance is a common concept in object-oriented programming. A base class can define common properties and behavior, while derived classes can add their own properties and functionality.
When using Entity Framework Core (EF Core), inheritance needs to be mapped from the object-oriented class hierarchy to relational database tables. EF Core supports several inheritance mapping strategies, including Table Per Hierarchy (TPH) and Table Per Type (TPT).
In this article, we will understand how TPH and TPT work in EF Core by creating a simple employee hierarchy. We will configure both strategies, insert sample data, query the entities, and understand how each approach is represented in the database.
What Is Inheritance Mapping in EF Core?
Consider a base Employee class with two derived classes:
public abstract class Employee
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
}
public class Developer : Employee
{
public string ProgrammingLanguage { get; set; } = string.Empty;
}
public class Manager : Employee
{
public int TeamSize { get; set; }
}
Here, Employee contains properties common to all employees. Developer and Manager inherit those properties and add their own specific properties.
The database does not directly understand C# inheritance. EF Core therefore needs an inheritance mapping strategy to determine how these classes should be stored in relational tables.
The two strategies discussed in this article are:
Table Per Hierarchy (TPH)
Table Per Type (TPT)
Create the Sample EF Core Project
Step 1: Create the Project
dotnet new console -n EfCoreInheritanceDemo
cd EfCoreInheritanceDemo
Step 2: Install Entity Framework Core Packages
dotnet add package Microsoft.EntityFrameworkCore
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Microsoft.EntityFrameworkCore.Design
The example uses SQL Server as the database provider.
Create the Entity Classes
Step 1: Create the Base Entity
public abstract class Employee
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
}
The Employee class contains the properties shared by all employee types.
Step 2: Create the Derived Entities
public class Developer : Employee
{
public string ProgrammingLanguage { get; set; } = string.Empty;
}
public class Manager : Employee
{
public int TeamSize { get; set; }
}
The Developer class contains the programming language specific to developers, while Manager contains the team size.
Create the DbContext
Create an EmployeeDbContext class:
using Microsoft.EntityFrameworkCore;
public class EmployeeDbContext : DbContext
{
public DbSet<Employee> Employees => Set<Employee>();
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseSqlServer(
"Server=(localdb)\\MSSQLLocalDB;Database=EfCoreInheritanceDemo;Trusted_Connection=True;TrustServerCertificate=True");
}
}
The Employees DbSet represents the employee inheritance hierarchy.
The database structure generated by EF Core depends on the inheritance strategy configured in the model.
Table Per Hierarchy (TPH)
Table Per Hierarchy (TPH) stores the entire inheritance hierarchy in a single database table.
A discriminator column identifies which derived class each row represents.
For example, the database can conceptually look like this:
Employees
------------------------------------------------------------------
Id | Name | Discriminator | ProgrammingLanguage | TeamSize
------------------------------------------------------------------
1 | John | Developer | C# | NULL
2 | Sarah | Manager | NULL | 8
Configure TPH
TPH is the default inheritance strategy in EF Core. It can also be configured explicitly:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Employee>()
.HasDiscriminator<string>("Discriminator")
.HasValue<Developer>("Developer")
.HasValue<Manager>("Manager");
}
The Discriminator column identifies the CLR type represented by each database row.
Insert TPH Data
using (var context = new EmployeeDbContext())
{
context.Database.EnsureCreated();
var developer = new Developer
{
Name = "John",
ProgrammingLanguage = "C#"
};
var manager = new Manager
{
Name = "Sarah",
TeamSize = 8
};
context.Add(developer);
context.Add(manager);
context.SaveChanges();
}
Both entities are stored in the same Employees table.
Query TPH Data
using (var context = new EmployeeDbContext())
{
var employees = context.Employees.ToList();
foreach (var employee in employees)
{
Console.WriteLine(
$"{employee.Id} - {employee.Name} - {employee.GetType().Name}");
}
}
EF Core uses the discriminator value to determine whether each row should be materialized as a Developer or Manager.
TPH Output
1 - John - Developer
2 - Sarah - Manager
Advantages of TPH
Stores the entire hierarchy in one table.
Requires fewer joins when querying the hierarchy.
Keeps the database structure relatively simple.
Can provide good query performance for many inheritance scenarios.
Disadvantages of TPH
Can result in many nullable columns.
The table can become very wide as the hierarchy grows.
Different derived types may have significantly different properties in the same table.
Table Per Type (TPT)
Table Per Type (TPT) maps the base entity and each derived entity to separate database tables.
For the example in this article, the database structure would look like this:
Employees
----------------
Id | Name
Developers
-------------------------
Id | ProgrammingLanguage
Managers
----------------
Id | TeamSize
The derived tables use the same key as the corresponding row in the base table.
Configure TPT
TPT can be configured using ToTable():
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Employee>()
.ToTable("Employees");
modelBuilder.Entity<Developer>()
.ToTable("Developers");
modelBuilder.Entity<Manager>()
.ToTable("Managers");
}
EF Core now stores common employee properties in the Employees table and derived properties in the appropriate derived table.
Insert TPT Data
using (var context = new EmployeeDbContext())
{
context.Database.EnsureCreated();
var developer = new Developer
{
Name = "John",
ProgrammingLanguage = "C#"
};
var manager = new Manager
{
Name = "Sarah",
TeamSize = 8
};
context.Add(developer);
context.Add(manager);
context.SaveChanges();
}
EF Core distributes the data across the appropriate tables.
For example, John's common information is stored in Employees, while his programming language is stored in Developers.
Query TPT Data
using (var context = new EmployeeDbContext())
{
var employees = context.Employees.ToList();
foreach (var employee in employees)
{
Console.WriteLine(
$"{employee.Id} - {employee.Name} - {employee.GetType().Name}");
}
}
EF Core handles the relationships between the base and derived tables when materializing the entities.
TPT Output
1 - John - Developer
2 - Sarah - Manager
The database contains the data across multiple tables:
Employees
----------------
Id | Name
----------------
1 | John
2 | Sarah
Developers
-------------------------
Id | ProgrammingLanguage
-------------------------
1 | C#
Managers
----------------
Id | TeamSize
----------------
2 | 8
Advantages of TPT
Separates base and derived properties into different tables.
Reduces nullable columns caused by unrelated derived properties.
Provides clear database-level separation between entity types.
Can be useful for inheritance hierarchies with substantially different properties.
Disadvantages of TPT
Queries involving derived entities can require joins.
The database structure contains more tables and relationships.
Complex inheritance hierarchies can result in more complicated queries.
Join-heavy queries can have additional performance overhead.
TPH vs TPT
Feature | TPH | TPT |
|---|---|---|
Tables | Single table | Base and derived tables |
Discriminator | Yes | No |
Nullable columns | More likely | Fewer |
JOIN operations | Generally fewer | More likely |
Database structure | Simpler | More complex |
Schema separation | Lower | Higher |
Query performance | Generally good | Can be slower for join-heavy queries |
Best suited for | Similar entity structures | Highly differentiated entity structures |
When Should You Use TPH?
TPH is a good option when derived entities share most of their structure and a simple database schema is preferred.
For example, if several employee types have mostly common properties with only a few type-specific fields, storing them in one table can be practical.
TPH is also worth considering when inheritance queries are frequent and avoiding additional joins is important.
When Should You Use TPT?
TPT can be useful when derived entities have significantly different properties and keeping those properties in separate database tables provides a cleaner design.
For example, if Developer, Manager, and other employee types contain many type-specific properties, TPT can provide clearer separation.
The trade-off is that queries involving derived entities may require joins between the base and derived tables.
Factors to Consider Before Choosing a Strategy
The inheritance strategy should be selected based on the application's actual requirements and workload.
Consider the following factors:
Number of derived entity types.
Number of properties shared between entities.
Number of properties specific to each derived type.
Frequency of queries against derived entities.
Database schema complexity.
Expected growth of the inheritance hierarchy.
Performance requirements.
It is also useful to inspect the SQL generated by EF Core for important queries and test the selected mapping strategy with realistic data volumes.
Conclusion
Entity Framework Core provides inheritance mapping strategies that allow object-oriented class hierarchies to be represented in relational databases.
With Table Per Hierarchy (TPH), the complete hierarchy is stored in a single table with a discriminator column. This keeps the database structure simple and can reduce the number of joins, but it may result in many nullable columns as the hierarchy grows.
With Table Per Type (TPT), the base entity and derived entities are stored in separate tables. This provides clearer separation of data and avoids many nullable columns, but queries involving derived entities can require additional joins.
TPH is generally a straightforward choice for inheritance hierarchies with mostly shared properties, while TPT can be useful when derived entities have significantly different data requirements.
The best strategy depends on the application's domain model, database structure, query patterns, data volume, and performance requirements.

Join the conversation! Your thoughts help the community grow.