EFCore and class inheritance

on

I was playing with how Entity Framework Core deals with class inheritance in the code first approach – what tables it generates and how it connects them. This article is discussing the Table-per-hierarchy and Table-per-type models in which EF Core handles inheritance. The official Microsoft documentation on the topic is awesome and I strongly suggest going through it either before or after reading this article.

I am writing this article more as notes to myself than I expect it to be useful, but if anyone out there finds it useful, I am really glad I was able to help. The code to follow along, if you do not want to type your own, is available on my GitHub.

Setup

I am starting from a new console application in .NET 6.0. I will not be building any real features here so this seems enough. If someone is a total noob with .NET, you can just open Visual Studio and use the Create New Project option. Choose the Console Application. The resulting output, in .NET 6.0 just gives you an empty project with the Program.cs file in it.

I am going to be using a MS SQL Server database, so I need to add the following NuGet packages.

  • Microsoft.EntityFrameworkCore
  • Microsoft.EntityFrameworkCore.SqlServer
  • Microsoft.EntityFrameworkCore.Tools

(if you’re using a different DB, this part may differ a bit).

Next, I need to add a context to the project, something like this:

public class VehiclesContext : DbContext
{
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
       optionsBuilder.UseSqlServer(@"-- my connection string here --");
    }
}

If you run Update-Database from the Package Manager Console you will see an empty DB created. (if you’re using dotnet then it is dotnet ef database update).

Initial inheritance classes

Let’s create three classes just so we can demonstrate inheritance:

public class Vehicle
{
    public int Id { get; set; }
    public string Make { get; set; }
}

public class Car : Vehicle
{
    public int TrunkSizeInCm3 { get; set; }
}

public class Motorcycle : Vehicle
{
   public RacingClass RacingClass { get; set; }
}

public enum RacingClass
{
    cc125 = 1,
    cc250 = 2,
    cc500 = 3
}

(please disregard the stupidity of the example, it was literally the first thing that came to mind!)

Table per Hierarchy – the default model

Initial migration

So, if we now add these three classes to the context like this:

public DbSet<Vehicle> Vehicles { get; set; }
public DbSet<Car> Cars { get; set; }
public DbSet<Motorcycle> Motorcycles { get; set; }

and run the Add-Migration InitialTables command, we will get the following migration generated:

public partial class InitialTables : Migration
{
    protected override void Up(MigrationBuilder migrationBuilder)
    {
        migrationBuilder.CreateTable(
            name: "Vehicles",
            columns: table => new
            {
                Id = table.Column<int>(type: "int", nullable: false)
                    .Annotation("SqlServer:Identity", "1, 1"),
                Make = table.Column<string>(type: "nvarchar(max)", nullable: false),
                Discriminator = table.Column<string>(type: "nvarchar(max)", nullable: false),
                TrunkSizeInCm3 = table.Column<int>(type: "int", nullable: true),
                RacingClass = table.Column<int>(type: "int", nullable: true)
            },
            constraints: table =>
            {
                table.PrimaryKey("PK_Vehicles", x => x.Id);
            });
    }

    protected override void Down(MigrationBuilder migrationBuilder)
    {
        migrationBuilder.DropTable(
            name: "Vehicles");
    }
}

So, by default, Entity Framework Core generates only one table for the entity from which others are inheriting, having all columns that all of the entities inheriting have. Do note that the Discriminator column has been added automatically! This is done by EF to be able to distinguish between different types of records and is completely hidden from the developer in the code.

This model of handling inheritance is called the Table per Hierarchy model.

Saving and fetching data

In our main program we will add some basic code to create some data, save it and fetch it:

using (var ctx = new VehiclesContext())
{
    List<Vehicle> vehicles = new List<Vehicle>();
    vehicles.Add(new Car { Make = "Chevrolet Orlando", TrunkSizeInCm3 = 10000 });
    vehicles.Add(new Motorcycle { Make = "Suzuki", RacingClass = RacingClass.cc125 });
    vehicles.Add(new Motorcycle { Make = "Yamaha", RacingClass = RacingClass.cc250 });

    await ctx.Vehicles.AddRangeAsync(vehicles);
    await ctx.SaveChangesAsync();

    var myCars = await ctx.Cars.ToListAsync();
    var myMotorcycles = await ctx.Motorcycles.ToListAsync();
    var myVehicles = await ctx.Vehicles.ToListAsync();

}

If we set a breakpoint at the end of the program and run this code, we will note the following change in our table in the database:

EF Core and class inheritance - vehicles table preview

Note that all three items have been added.

Note that the Discriminator table has been filled automatically. It is a string value that matches the class name.

Note that TrunkSizeInCm3 and RacingClass values are nullable and have the null values for rows for which they do not exist – even though in the code class definitions they are not nullable. This is because EF Core knows that for given discriminator type it cannot have values for this columns.

If we check the values at the breakpoint, we will see that the three fetching commands have generated data of types: List<Car>, List<Motorcycle>, List<Vehicle> respectively. In the myVehicles list we have three items of types Car, Motorcycle, Motorcycle respectively.

So, everything here works as expected, EF Core handles saving, fetching, filtering and all of that for us.

Renaming a class and its effects on the Discriminator

We mentioned earlier that the Discriminator column gets created automatically and populated by a string from the class name. If we decide to rename the class Motorcycle to Motorbike and then run the Add-Migration command, we get the following:

public partial class RenameMotorcycleToMotorbike : Migration
{
    protected override void Up(MigrationBuilder migrationBuilder)
    {

    }

    protected override void Down(MigrationBuilder migrationBuilder)
    {

    }
}

Nothing changes!

However, (if we comment out the statements for adding the data) and run the main program again, we will get an exception:

EF Core and class inheritance - discriminator exception

Do note that EF Core, when generating a migration after the class was renamed, did not automatically update existing records. And now, when fetching data, we have an issue that it cannot read records with the old Discriminator.

So, apparently, for this case you need to write your own migration code to update the Discriminator column properly! Which is annoying – but it’s a solution, so… hopefully it gets improved in the future versions of EF Core.

Type per Table model

Another way we could approach the situation is to have one table for the base class, and then a separate table for each class deriving from it. This is called the Type per Table model and it is not default, but needs to be configured. This was introduced in Entity Framework Core 5.0 and is supported by versions higher, but not versions lower.

Configuring the DbContext

In order to demonstrate the Type per Table, we will create a VehiclesTPTContext:

public class VehiclesTPTContext : DbContext
{
    public DbSet<Vehicle> Vehicles { get; set; }
    public DbSet<Car> Cars { get; set; }
    public DbSet<Motorbike> Motorcycles { get; set; }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseSqlServer(@"-- my connection string --");
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Car>().ToTable("Cars");
        modelBuilder.Entity<Motorbike>().ToTable("Motorbikes");
        modelBuilder.Entity<Vehicle>().ToTable("Vehicles");
    }
}

Note the changes in the OnModelCreating method.

Initial migration

Now if we add a migration (do note that now we need to add a -Context parameter and specify the context, as we have two contexts we’re playing with), we will get the following code:

{
    protected override void Up(MigrationBuilder migrationBuilder)
    {
        migrationBuilder.CreateTable(
            name: "Vehicles",
            columns: table => new
            {
                Id = table.Column<int>(type: "int", nullable: false)
                    .Annotation("SqlServer:Identity", "1, 1"),
                Make = table.Column<string>(type: "nvarchar(max)", nullable: false)
            },
            constraints: table =>
            {
                table.PrimaryKey("PK_Vehicles", x => x.Id);
            });

        migrationBuilder.CreateTable(
            name: "Cars",
            columns: table => new
            {
                Id = table.Column<int>(type: "int", nullable: false),
                TrunkSizeInCm3 = table.Column<int>(type: "int", nullable: false)
            },
            constraints: table =>
            {
                table.PrimaryKey("PK_Cars", x => x.Id);
                table.ForeignKey(
                    name: "FK_Cars_Vehicles_Id",
                    column: x => x.Id,
                    principalTable: "Vehicles",
                    principalColumn: "Id");
            });

        migrationBuilder.CreateTable(
            name: "Motorbikes",
            columns: table => new
            {
                Id = table.Column<int>(type: "int", nullable: false),
                RacingClass = table.Column<int>(type: "int", nullable: false)
            },
            constraints: table =>
            {
                table.PrimaryKey("PK_Motorbikes", x => x.Id);
                table.ForeignKey(
                    name: "FK_Motorbikes_Vehicles_Id",
                    column: x => x.Id,
                    principalTable: "Vehicles",
                    principalColumn: "Id");
            });
    }

    protected override void Down(MigrationBuilder migrationBuilder)
    {
        migrationBuilder.DropTable(
            name: "Cars");

        migrationBuilder.DropTable(
            name: "Motorbikes");

        migrationBuilder.DropTable(
            name: "Vehicles");
    }
}

Updating the database creates three tables which are connected in a way that the primary key of a table “deriving” from another table is the same as the primary key in the base class table and is actually also a foreign key to the base class table.

EF Core and class inheritance - TPT example

Note that the Vehicles table contains all the properties that are in the Vehicles class, while all class-specific properties for Car and Motorbike are in their respective tables.

Saving and fetching data

If we take the same code we used to save the data in the TPH example and use it with the TPT context, put a breakpoint in the end and run it, we would get following results:

EF Core and class inheritance - TPT example with data

Note that all data has been properly saved. Do note that the IDs in the Motorbikes table do not start from 1 and are not autoincrement – this is because they are in fact connected to the primary key from the Vehicles table via a foreign key constraint.

If we check the in memory data on the breakpoint we will see the following:

  • myCars, myMotorcycles and myVehicles are of types List<Car>, List<Motorbike> and List<Vehicle> respectively
  • myVehicles has three items of types Car, Motorbike, Motorbike

So, we get exactly the same results with Table per Type as we did with the Table per Hierarchy model.

Performance – TPH vs TPT?

I have not done my own tests.

In the Microsoft official documentation they claim that: “In many cases, TPT shows inferior performance when compared to TPH.” They are also providing resources to which you need to pay attention to and their benchmark tests in the Modeling for Performance documentation page.

TPH vs TPT – which is better?

Like always, when you ask this type of question to anyone in software development, you will never get an answer. Or, rather, you will get the famous “Well, it depends!” answer.

It does depend on what data you’re storing, how many inherited classes you have, what types of queries do you use… The list goes on and on.

But, I am using both on a couple of projects I am working on, so I hope that in a few months I will develop an opinion based on which one caused me less problems. Until then – I’ll stay with “it depends!” 🙂

2 thoughts on “EFCore and class inheritance

  1. I like the TPT approach and I have used in most projects. It feels more object-related and it is easier to use if you have to dig in the database yourself (without EF).
    But I am curious about the performance too…

    1. Possibly / probably a topic for a different article, once I have any real data for benchmarking.

      But, from a glance, TPT looks cleaner and more “object-oriented” to me as well!

Leave a Reply

Your email address will not be published.

You are currently offline