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:

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

Disadvantages of TPH

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

Disadvantages of TPT

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:

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.