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:

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

Disadvantages of TPH

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

Disadvantages of TPT

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:

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.