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, however, 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
For this example, create a console application and install Entity Framework Core with the SQL Server provider.
dotnet new console -n EfCoreInheritanceDemo
cd EfCoreInheritanceDemo
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.
Step 1: Create the Base Entity
Create an abstract Employee class containing the properties shared by all employee types.
public abstract class Employee
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
}
The abstract keyword indicates that an Employee object should not be created directly. Instead, objects such as Developer and Manager will be created.
Step 2: Create the Derived Entities
Create the derived classes.
public class Developer : Employee
{
public string ProgrammingLanguage { get; set; } = string.Empty;
}
public class Manager : Employee
{
public int TeamSize { get; set; }
}
The Developer class has a ProgrammingLanguage property, while the Manager class has a TeamSize property.
Step 3: 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 hierarchy in EF Core.
The actual database structure depends on the inheritance strategy configured in the model.
Table Per Hierarchy (TPH)
Table Per Hierarchy is the default inheritance mapping strategy used by EF Core.
With TPH, the complete inheritance hierarchy is stored in a single database table.
A discriminator column is used to identify which derived class each row represents.
For our example, the database table could look conceptually like this:
Employees
---------------------------------------------------------------
Id | Name | Discriminator | ProgrammingLanguage | TeamSize
---------------------------------------------------------------
1 | John | Developer | C# | NULL
2 | Sarah | Manager | NULL | 8
3 | Michael | Developer | Java | NULL
Properties that do not apply to a particular derived type are stored as NULL.
Configure TPH
TPH is the default strategy, so no special configuration is required. However, explicitly configuring it can make the mapping easier to understand.
Update the OnModelCreating method:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Employee>()
.HasDiscriminator<string>("Discriminator")
.HasValue<Developer>("Developer")
.HasValue<Manager>("Manager");
}
The Discriminator column tells EF Core which CLR type should be created when a row is loaded.
For example:
Discriminator = Developer
causes EF Core to materialize that row as a Developer object.
Similarly:
Discriminator = Manager
causes EF Core to materialize it as a Manager object.
Add Sample Data
We can now insert a developer and a manager.
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 objects are stored in the same Employees table.
Query TPH Data
EF Core can query the complete hierarchy through the base DbSet.
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 column to determine the appropriate derived type.
TPH Output
A possible console output is:
1 - John - Developer
2 - Sarah - Manager
The corresponding database data can conceptually look like this:
Id | Name | Discriminator | ProgrammingLanguage | TeamSize
---|-------|---------------|---------------------|---------
1 | John | Developer | C# | NULL
2 | Sarah | Manager | NULL | 8
This demonstrates the main characteristic of TPH: all entities in the inheritance hierarchy are stored in one table.
Advantages of TPH
All inherited entities are stored in a single table.
Queries can be simpler because joins between inheritance tables are not required.
It generally provides good read performance for inheritance queries.
The database schema is relatively simple.
Disadvantages of TPH
The table can contain many nullable columns.
The table can become wide when the hierarchy contains many derived classes.
Maintaining a large hierarchy can become difficult.
Different derived classes may have very different properties but still share the same table.
Table Per Type (TPT)
Table Per Type takes a different approach.
With TPT, the base class has its own table, and each derived class has a separate table containing properties specific to that type.
For our example, the database structure would look like this:
Employees
----------------
Id
Name
Developers
----------------
Id
ProgrammingLanguage
Managers
----------------
Id
TeamSize
The Id in the derived tables is also the primary key and is related to the corresponding row in the Employees table.
Configure TPT
EF Core allows TPT to be configured using the ToTable() method.
Update the OnModelCreating method:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Employee>()
.ToTable("Employees");
modelBuilder.Entity<Developer>()
.ToTable("Developers");
modelBuilder.Entity<Manager>()
.ToTable("Managers");
}
Now EF Core maps each type to its own table.
The resulting database structure is:
Employees
----------------
Id | Name
----------------
1 | John
2 | Sarah
Developers
----------------
Id | ProgrammingLanguage
-------------------------
1 | C#
Managers
----------------
Id | TeamSize
-------------
2 | 8
The relationship between these tables is established using the primary key.
Add Sample Data
The application code for inserting the entities remains straightforward:
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();
}
Although the application adds two objects, EF Core stores their data across the appropriate tables.
For example, the developer's common information is stored in Employees, while the ProgrammingLanguage value is stored in Developers.
Query TPT Data
We can query the base entity in the same way:
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 required table relationships when materializing the derived entities.
TPT Output
A possible console output is:
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
Unlike TPH, there is no need for a discriminator column because the table in which the derived information exists identifies the entity type.
Advantages of TPT
Base and derived properties are separated into different tables.
The database structure can be easier to understand for highly differentiated entities.
It avoids a large number of nullable columns caused by unrelated derived properties.
Each entity type has a clearly defined table structure.
Disadvantages of TPT
Queries involving derived entities can require joins between the base and derived tables.
The database schema contains more tables and relationships.
Complex inheritance hierarchies can result in more complicated SQL queries.
Read performance can be lower than TPH for queries that require multiple joins, depending on the database and workload.
TPH vs TPT: Key Differences
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 |
Read performance | Generally good | Can be slower for join-heavy queries |
Schema separation | Lower | Higher |
Suitable for | Similar entity structures | Highly differentiated entity structures |
When Should You Use TPH?
TPH is a good choice when the derived entities share most of their structure and you want a simple database model.
For example, if several employee types contain mostly common employee information and only have a few type-specific properties, keeping them in one table can be practical.
TPH can also be a reasonable choice when read performance and straightforward queries are important.
However, a very large inheritance hierarchy can result in a wide table with many nullable columns.
When Should You Use TPT?
TPT can be useful when derived entities have substantially different properties and you want those properties separated at the database level.
For example, if Developer, Manager, Contractor, and SalesEmployee each have many type-specific fields, separate tables may provide a cleaner relational model.
The trade-off is that retrieving complete derived entities can require joins between the base and derived tables.
Choosing Between TPH and TPT
The choice should be based on the application's actual data model and workload rather than using one strategy universally.
Consider the following questions:
How many derived entity types will the hierarchy contain?
How many properties are shared between the entities?
How many properties are specific to each derived type?
How frequently will the application query derived entities?
Is database schema simplicity important?
Are join-heavy queries acceptable for the application's workload?
For a small hierarchy with mostly shared properties, TPH is often a straightforward option.
For a hierarchy where derived types have significantly different data and database-level separation is valuable, TPT may be a better fit.
Common Considerations
Inheritance mapping can affect both the application model and database performance. Before selecting a strategy, consider the size of the tables, query patterns, indexing requirements, and expected growth of the hierarchy.
It is also useful to inspect the SQL generated by EF Core for important queries. This helps identify whether a particular mapping strategy is producing expensive joins or unnecessarily wide queries.
The right strategy should ultimately be validated against the application's real workload rather than selected only based on theoretical advantages.
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 using a discriminator column. This keeps the database structure simple and can provide efficient queries, but it can result in nullable columns as the hierarchy grows.
With Table Per Type (TPT), the base entity and each derived entity are represented by separate tables. This provides clearer separation of data and avoids many nullable columns, but queries involving derived entities may require additional joins.
TPH is generally a good starting point for simple inheritance hierarchies with mostly shared properties, while TPT can be useful when derived entities have substantially different data requirements.
The best choice depends on the application's domain model, database design, query patterns, and performance requirements.

Join the conversation! Your thoughts help the community grow.