Announcing Entity Framework 7 Preview 5

Jeremy Likness

Entity Framework 7 (EF7) Preview 5 has shipped with support for Table-per-Concrete type (TPC) mapping. This blog post will focus on TPC. There are several other enhancements included in Preview 5, such as:

Read the full list of EF7 Preview 5 enhancements.

Table-per-concrete-type (TPC) mapping

By default, EF Core maps an inheritance hierarchy of .NET types to a single database table. This is known as the table-per-hierarchy (TPH) mapping strategy. EF Core 5.0 introduced the table-per-type (TPT) strategy, which supports mapping each .NET type to a different database table. In EF Core 7.0 preview 5.0, we are excited to introduce the table-per-concrete-type (TPC) strategy. TPC also maps .NET types to different tables, but in a way that addresses some common performance issues with the TPT strategy.

In this post, we’ll start by describing the structure of TPH, TPT, and TPC mappings, then look at how these strategies can be configured in EF Core, and finally discuss the pros and cons of each approach.

Mapping inheritance hierarchies

Consider the following object-oriented domain model:

public abstract class Animal
{
    public int Id { get; set; }
    public string Species { get; set; }
}

public class FarmAnimal : Animal
{
    public decimal Value { get; set; }
}

public class Pet : Animal
{
    public string Name { get; set; }
}

public class Cat : Pet
{
    public string EducationLevel { get; set; }
}

public class Dog : Pet
{
    public string FavoriteToy { get; set; }
}

If we are to retrieve some Animal object from the database, then we must know which type of animal it is. We don’t want to save a cat and then read it back as a dog, or vice versa. (I can tell you from experience that dogs generally don’t like to be treated as cats, and cats certainly don’t like to be treated as dogs!) So this means the type of animal–that is the actual class used when the animal was created in C#–must be saved to the database in some form.

Further, different information is associated with each Animal object depending on its type. For example, in our model, a farm animal has some monetary value but no name, while pets are priceless and named.

Inheritance mapping strategies (TPH, TPT, or TPC) define how this object-oriented type information and type-specific information are saved into a relational database, where inheritance is not a natural concept.

The TPH strategy

With the TPH strategy, a single table is created for all types in the hierarchy–hence the name “table-per-hierarchy”. This table contains a special column containing a “discriminator value”, which indicates the type of the object saved in each row. In addition, a column is created for every property of every type in the hierarchy. For example:

CREATE TABLE [Animals] (
    [Id] int NOT NULL IDENTITY,
    [Species] nvarchar(max) NOT NULL,
    [Discriminator] nvarchar(max) NOT NULL,
    [Value] decimal(18,2) NULL,
    [Name] nvarchar(max) NULL,
    [EducationLevel] nvarchar(max) NULL,
    [FavoriteToy] nvarchar(max) NULL,
    CONSTRAINT [PK_Animals] PRIMARY KEY ([Id])
);

Saving two cats, a dog, and a sheep to this table results in the following:

Id Species Discriminator Value Name EducationLevel FavoriteToy
1 Felis catus Cat NULL Alice MBA NULL
2 Felis catus Cat NULL Mac BA NULL
3 Canis familiaris Dog NULL Toast NULL Mr. Squirrel
4 Ovis aries FarmAnimal 100.00 NULL NULL NULL

Notice that:

  • The value in the Discriminator column indicates the type of C# object saved
  • There is a column for every property in the hierarchy
  • If the property does not exist for the type of the object saved, then the value in the database for that column is null

The TPH strategy requires that database columns be nullable for any property not defined in the root type of the hierarchy, even if that property is required. It is possible to create a database constraint for these columns to ensure the value is non-null whenever an instance with that property is saved, but this is not done automatically by EF Core. See Issue #20931 on the EF Core GitHub repo for more information.

The TPT strategy

With the TPT strategy, a different table is created for every type in the hierarchy–hence the name “table-per-type”. The table itself is used to determine the type of the object saved, and each table contains only columns for the properties of that type. For example:

CREATE TABLE [Animals] (
    [Id] int NOT NULL IDENTITY,
    [Species] nvarchar(max) NOT NULL,
    CONSTRAINT [PK_Animals] PRIMARY KEY ([Id])
);

CREATE TABLE [FarmAnimals] (
    [Id] int NOT NULL,
    [Value] decimal(18,2) NOT NULL,
    CONSTRAINT [PK_FarmAnimals] PRIMARY KEY ([Id]),
    CONSTRAINT [FK_FarmAnimals_Animals_Id] FOREIGN KEY ([Id]) REFERENCES [Animals] ([Id])
);

CREATE TABLE [Pets] (
    [Id] int NOT NULL,
    [Name] nvarchar(max) NOT NULL,
    CONSTRAINT [PK_Pets] PRIMARY KEY ([Id]),
    CONSTRAINT [FK_Pets_Animals_Id] FOREIGN KEY ([Id]) REFERENCES [Animals] ([Id])
);

CREATE TABLE [Cats] (
    [Id] int NOT NULL,
    [EducationLevel] nvarchar(max) NOT NULL,
    CONSTRAINT [PK_Cats] PRIMARY KEY ([Id]),
    CONSTRAINT [FK_Cats_Pets_Id] FOREIGN KEY ([Id]) REFERENCES [Pets] ([Id])
);

CREATE TABLE [Dogs] (
    [Id] int NOT NULL,
    [FavoriteToy] nvarchar(max) NOT NULL,
    CONSTRAINT [PK_Dogs] PRIMARY KEY ([Id]),
    CONSTRAINT [FK_Dogs_Pets_Id] FOREIGN KEY ([Id]) REFERENCES [Pets] ([Id])
);

Saving the same data into this database results in the following:

Animals table:

Id Species
1 Felis catus
2 Felis catus
3 Canis familiaris
4 Ovis aries

FarmAnimals table:

Id Value
4 100.00

Pets table:

Id Name
1 Alice
2 Mac
3 Toast

Cats table:

Id EducationLevel
1 MBA
2 BA

Dogs table:

Id FavoriteToy
3 Mr. Squirrel

Notice that the data is saved in a normalized form, but that this means the information for a single object is spread across multiple tables.

TPC mapping

The TPC strategy is similar to the TPT strategy except that a different table is created for every concrete type in the hierarchy, but tables are not created for abstract types–hence the name “table-per-concrete-type”. As with TPT, the table itself indicates the type of the object saved. However, unlike TPT mapping, each table contains columns for every property in the concrete type and its base types. For example:

CREATE TABLE [FarmAnimals] (
    [Id] int NOT NULL DEFAULT (NEXT VALUE FOR [AnimalIds]),
    [Species] nvarchar(max) NOT NULL,
    [Value] decimal(18,2) NOT NULL,
    CONSTRAINT [PK_FarmAnimals] PRIMARY KEY ([Id])
);

CREATE TABLE [Pets] (
    [Id] int NOT NULL DEFAULT (NEXT VALUE FOR [AnimalIds]),
    [Species] nvarchar(max) NOT NULL,
    [Name] nvarchar(max) NOT NULL,
    CONSTRAINT [PK_Pets] PRIMARY KEY ([Id])
);

CREATE TABLE [Cats] (
    [Id] int NOT NULL DEFAULT (NEXT VALUE FOR [AnimalIds]),
    [Species] nvarchar(max) NOT NULL,
    [Name] nvarchar(max) NOT NULL,
    [EducationLevel] nvarchar(max) NOT NULL,
    CONSTRAINT [PK_Cats] PRIMARY KEY ([Id])
);

CREATE TABLE [Dogs] (
    [Id] int NOT NULL DEFAULT (NEXT VALUE FOR [AnimalIds]),
    [Species] nvarchar(max) NOT NULL,
    [Name] nvarchar(max) NOT NULL,
    [FavoriteToy] nvarchar(max) NOT NULL,
    CONSTRAINT [PK_Dogs] PRIMARY KEY ([Id])
);

Notice that:

  • There is no table for Animal, since it is an abstract type in the object model. Remember that C# does not allow instances of abstract types, and there is therefore no situation where one will be saved to the database.
  • The mapping of properties in base types is repeated for each concrete type–for example, every table has a Species column, and both Cats and Dogs have a Name column.

Saving the same data into this database results in the following:

FarmAnimals table:

Id Species Value
4 Ovis aries 100.00

Pets table:

Id Species Name

Cats table:

Id Species Name EducationLevel
1 Felis catus Alice MBA
2 Felis catus Mac BA

Dogs table:

Id Species Name FavoriteToy
3 Canis familiaris Toast Mr. Squirrel

Notice that, unlike with TPT mapping, all the information for a single object is contained in a single table.

Configuring inheritance mappings in EF Core

When mapping an inheritance hierarchy, all types in the hierarchy must be explicitly included in the model. This can be done by creating a DbSet property for the type on your DbContext:

public DbSet<Animal> Animals { get; set; }
public DbSet<Pet> Pets { get; set; }
public DbSet<Cat> Cats { get; set; }
public DbSet<Dog> Dogs { get; set; }
public DbSet<FarmAnimal> FarmAnimals { get; set; }

Or by using the Entity method in OnModelCreating:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Animal>();
    modelBuilder.Entity<Pet>();
    modelBuilder.Entity<Cat>();
    modelBuilder.Entity<Dog>();
    modelBuilder.Entity<FarmAnimal>();
}

This is different from the legacy EF6 behavior, where derived types of mapped base types would sometimes be automatically discovered.

Nothing else needs to be done to map the hierarchy as TPH, since it is the default strategy. However, you can make this explicit by calling UseTphMappingStrategy on the base type of the hierarchy. For example:

modelBuilder.Entity<Animal>().UseTphMappingStrategy();

To use TPT instead, change this to UseTptMappingStrategy. For example:

modelBuilder.Entity<Animal>().UseTptMappingStrategy();

Likewise, UseTpcMappingStrategy is used to configure TPC:

modelBuilder.Entity<Animal>().UseTpcMappingStrategy();

In each case, the table name to use for each type can be configured using the ToTable builder method, or the [Table] attribute. However, this is only valid on types that are mapped to a table for the strategy being used. For example, the following code specifies the table names for TPC mapping:

modelBuilder.Entity<Pet>().ToTable("Pets");
modelBuilder.Entity<Cat>().ToTable("Cats");
modelBuilder.Entity<Dog>().ToTable("Dogs");
modelBuilder.Entity<FarmAnimal>().ToTable("FarmAnimals");

No table name can be specified for Animal because it is not mapped to its own table when using the TPC strategy. Conversely, when using the TPH strategy, only the base type (Animal) can be given a table name.

If multiple types in a hierarchy are given different table names, but no mapping strategy is explicitly specified, then the TPT strategy is used. This was the normal way to configure TPT prior to EF7.

Primary keys

The inheritance mapping strategy chosen has consequences for how primary key values are generated and managed. Keys in TPH are easy, since each entity instance is represented by a single row in a single table. Any kind of key value generation can be used, and no additional constraints are needed.

For the TPT strategy, there is always a row in the table mapped to the base type of the hierarchy. Any kind of key generation can be used on this row. The keys for other tables are linked to this table using foreign key constraints. For example:

CREATE TABLE [FarmAnimals] (
    [Id] int NOT NULL,
    [Value] decimal(18,2) NOT NULL,
    CONSTRAINT [PK_FarmAnimals] PRIMARY KEY ([Id]),
    CONSTRAINT [FK_FarmAnimals_Animals_Id] FOREIGN KEY ([Id]) REFERENCES [Animals] ([Id])
);

This ensures that same primary key value is used for a given entity in every table of the hierarchy.

This gets a bit more complicated when the TPC strategy is used. First, it’s important to understand that EF Core requires that all entities in a hierarchy must have a unique key value, even if the entities have different types. So, using our example model, a Dog cannot have the same Id key value as a Cat. Second, unlike TPT, there is no common table that can act as the single place where key values live and can be generated. This means a simple Identity column cannot be used.

For databases that support sequences, this key values can be generated by using a single sequence and referencing it in the default constraint for every table. This is the strategy used in the TPC tables shown above, where each table has the following:

[Id] int NOT NULL DEFAULT (NEXT VALUE FOR [AnimalIds])

AnimalIds is a sequence created by EF Core migrations. The following model building code sets this up for SQL Server:

modelBuilder.HasSequence<int>("AnimalIds");

modelBuilder.Entity<Animal>()
    .UseTpcMappingStrategy()
    .Property(e => e.Id).HasDefaultValueSql("NEXT VALUE FOR [AnimalIds]");

The syntax for the default constraint may be different for over database systems.

Pros and cons of mapping strategies

All of the above may be interesting, but how do you decide which strategy to use?

TPH

In almost all cases, TPH mapping works just fine, which is why it is the default. People are often concerned that the table can become very wide, with many columns only sparsely populated. While this can be true, it is rarely a problem with modern database systems. Query performance with TPH is always very good, mainly because no matter what query you write, only one table is ever needed to return results.

The most important performance differences of the different strategies stem from the SQL needed for different types of common query. To illustrate this, we will run the same three LINQ queries with each of the strategies. These queries are:

  1. A query that returns entities of all types in the hierarchy:
context.Animals.Where(a => a.Species.StartsWith("F")).ToList();
  1. A query that returns entities from a subset of types in the hierarchy:
context.Pets.Where(a => a.Species.StartsWith("F")).ToList();
  1. A query that returns only entities from a single leaf type in the hierarchy:
context.Cats.Where(a => a.Species.StartsWith("F")).ToList();

When using TPH, the SQL generated is simple and efficient in all cases. To help improve the performance of these queries, consider defining an index for faster filtering on the discriminator. In some scenarios this can a source of query slowdown compared to a table scan. However, note that SQL Server avoids using an index if it isn’t highly selective. For example, for 5 discriminator values over 1 million rows, SQL server prefers a table scan. Note that adding an index will also slows down updates, which may or may not be important.

Finally, if your database system supports it, then consider using sparse columns when the majority of rows will be null for that column.

  1. All types:
SELECT [a].[Id], [a].[Discriminator], [a].[Species], [a].[Value], [a].[Name], [a].[EducationLevel], [a].[FavoriteToy]
FROM [Animals] AS [a]
WHERE [a].[Species] LIKE N'F%'
  1. Subset of types:
SELECT [a].[Id], [a].[Discriminator], [a].[Species], [a].[Name], [a].[EducationLevel], [a].[FavoriteToy]
FROM [Animals] AS [a]
WHERE [a].[Discriminator] IN (N'Pet', N'Cat', N'Dog') AND ([a].[Species] LIKE N'F%')
  1. Leaf type:
SELECT [a].[Id], [a].[Discriminator], [a].[Species], [a].[Name], [a].[EducationLevel]
FROM [Animals] AS [a]
WHERE [a].[Discriminator] = N'Cat' AND ([a].[Species] LIKE N'F%')

TPT

The TPT strategy is rarely a good choice. It is mainly used when it is considered important that the data is stored in a normalized form, which is in turn often the case for legacy existing databases or databases managed independently from the application development team.

The main issue with the TPT strategy is that almost all queries involve joining multiple tables because the data for any given entity instance is split across multiple tables.

Using the same queries again, we can see that querying for entities of all types requires all five tables to be joined:

SELECT [a].[Id], [a].[Species], [f].[Value], [p].[Name], [c].[EducationLevel], [d].[FavoriteToy], CASE
    WHEN [d].[Id] IS NOT NULL THEN N'Dog'
    WHEN [c].[Id] IS NOT NULL THEN N'Cat'
    WHEN [p].[Id] IS NOT NULL THEN N'Pet'
    WHEN [f].[Id] IS NOT NULL THEN N'FarmAnimal'
END AS [Discriminator]
FROM [Animals] AS [a]
    LEFT JOIN [FarmAnimals] AS [f] ON [a].[Id] = [f].[Id]
    LEFT JOIN [Pets] AS [p] ON [a].[Id] = [p].[Id]
    LEFT JOIN [Cats] AS [c] ON [a].[Id] = [c].[Id]
    LEFT JOIN [Dogs] AS [d] ON [a].[Id] = [d].[Id]
WHERE [a].[Species] LIKE N'F%'

EF Core uses “discriminator synthesis” to determine which table the data comes from, and hence the correct type to use. This works because the LEFT JOIN returns nulls for the dependent ID column (the “sub-tables”) which aren’t the correct type. So for a dog, [d].[Id] will be non-null, and all the other (concrete) IDs will be null.

Querying for entities of a subset of types still requires that the base table be joined, resulting in four tables being used:

SELECT [a].[Id], [a].[Species], [p].[Name], [c].[EducationLevel], [d].[FavoriteToy], CASE
    WHEN [d].[Id] IS NOT NULL THEN N'Dog'
    WHEN [c].[Id] IS NOT NULL THEN N'Cat'
    WHEN [p].[Id] IS NOT NULL THEN N'Pet'
END AS [Discriminator]
FROM [Animals] AS [a]
    LEFT JOIN [Pets] AS [p] ON [a].[Id] = [p].[Id]
    LEFT JOIN [Cats] AS [c] ON [a].[Id] = [c].[Id]
    LEFT JOIN [Dogs] AS [d] ON [a].[Id] = [d].[Id]
WHERE ([d].[Id] IS NOT NULL OR [c].[Id] IS NOT NULL OR [p].[Id] IS NOT NULL) AND ([a].[Species] LIKE N'F%')

And even querying for entities of just a single leaf type requires the tables for all the types that the leaf type derives from:

SELECT [a].[Id], [a].[Species], [p].[Name], [c].[EducationLevel], CASE
    WHEN [c].[Id] IS NOT NULL THEN N'Cat'
END AS [Discriminator]
FROM [Animals] AS [a]
    LEFT JOIN [Pets] AS [p] ON [a].[Id] = [p].[Id]
    LEFT JOIN [Cats] AS [c] ON [a].[Id] = [c].[Id]
WHERE [c].[Id] IS NOT NULL AND ([a].[Species] LIKE N'F%')

TPC

The TPC strategy is an improvement over TPT because it ensures that the information for a given entity instance is always stored in a single table. This means the TPC strategy can be useful when the mapped hierarchy is large and has many concrete (usually leaf) types, each with a large number of properties, and where only a small subset of types are used in most queries.

Using the same LINQ queries again, the SQL needed when querying for entities of all types is better than it was for TPT, since it requires one fewer table in the query. This is because there is no table for the abstract base type. In addition, UNION ALL is used instead of the LEFT JOIN needed for TPT. UNION ALL does not need to perform any matching between rows or de-duplication of rows, which makes it more efficient that the joins used in TPT queries.

All that being said, when compared to the SQL for TPH, the SQL for TPC in this case is still not great:

SELECT [t].[Id], [t].[Species], [t].[Value], [t].[Name], [t].[EducationLevel], [t].[FavoriteToy], [t].[Discriminator]
FROM (
    SELECT [f].[Id], [f].[Species], [f].[Value], NULL AS [Name], NULL AS [EducationLevel], NULL AS [FavoriteToy], N'FarmAnimal' AS [Discriminator]
    FROM [FarmAnimals] AS [f]
    UNION ALL
    SELECT [p].[Id], [p].[Species], NULL AS [Value], [p].[Name], NULL AS [EducationLevel], NULL AS [FavoriteToy], N'Pet' AS [Discriminator]
    FROM [Pets] AS [p]
    UNION ALL
    SELECT [c].[Id], [c].[Species], NULL AS [Value], [c].[Name], [c].[EducationLevel], NULL AS [FavoriteToy], N'Cat' AS [Discriminator]
    FROM [Cats] AS [c]
    UNION ALL
    SELECT [d].[Id], [d].[Species], NULL AS [Value], [d].[Name], NULL AS [EducationLevel], [d].[FavoriteToy], N'Dog' AS [Discriminator]
    FROM [Dogs] AS [d]
) AS [t]
WHERE [t].[Species] LIKE N'F%'

This is again the case when querying for entities of a subset of types:

SELECT [t].[Id], [t].[Species], [t].[Name], [t].[EducationLevel], [t].[FavoriteToy], [t].[Discriminator]
FROM (
    SELECT [p].[Id], [p].[Species], [p].[Name], NULL AS [EducationLevel], NULL AS [FavoriteToy], N'Pet' AS [Discriminator]
    FROM [Pets] AS [p]
    UNION ALL
    SELECT [c].[Id], [c].[Species], [c].[Name], [c].[EducationLevel], NULL AS [FavoriteToy], N'Cat' AS [Discriminator]
    FROM [Cats] AS [c]
    UNION ALL
    SELECT [d].[Id], [d].[Species], [d].[Name], NULL AS [EducationLevel], [d].[FavoriteToy], N'Dog' AS [Discriminator]
    FROM [Dogs] AS [d]
) AS [t]
WHERE [t].[Species] IS NOT NULL AND ([t].[Species] LIKE N'F%')

But TPC is much better than TPT when querying for entities of a single leaf type, since all the information for those entities comes from a single table:

SELECT [c].[Id], [c].[Species], [c].[Name], [c].[EducationLevel]
FROM [Cats] AS [c]
WHERE [c].[Species] LIKE N'F%'

These types of queries for single leaf types is where TPC really excels.

Guidance

In summary, the guidance for which mapping strategy to use is quite simple:

  • If your code will mostly query for entities of a single leaf type, then use TPC. This is because:
    • The storage requirements are smaller, since there are no null columns and no discriminator.
    • No index is ever needed on the discriminator column, which would slow down updates and possibly also queries. An index may not be needed when using TPH either, but that depends on various factors.
  • If your code will mostly query for entities of many types, such as writing queries against the base type, then lean towards TPH.
    • If your database system supports it (e.g. SQL Server), then consider using sparse columns for columns that will be rarely populated.
  • Use TPT only if constrained to do so by external factors.

Prerequisites

  • EF7 currently targets .NET 6.
  • EF7 will not run on .NET Framework.

EF7 is the successor to EF Core 6.0, not to be confused with EF6. If you are considering upgrading from EF6, please read our guide to port from EF6 to EF Core.

How to get EF7 previews

EF7 is distributed exclusively as a set of NuGet packages. For example, to add the SQL Server provider to your project, you can use the following command using the dotnet tool:

dotnet add package Microsoft.EntityFrameworkCore.SqlServer --version 7.0.0-preview.5.22302.2

This following table links to the preview 5 versions of the EF Core packages and describes what they are used for.

Package Purpose
Microsoft.EntityFrameworkCore The main EF Core package that is independent of specific database providers
Microsoft.EntityFrameworkCore.SqlServer Database provider for Microsoft SQL Server and SQL Azure
Microsoft.EntityFrameworkCore.SqlServer.NetTopologySuite SQL Server support for spatial types
Microsoft.EntityFrameworkCore.Sqlite Database provider for SQLite that includes the native binary for the database engine
Microsoft.EntityFrameworkCore.Sqlite.Core Database provider for SQLite without a packaged native binary
Microsoft.EntityFrameworkCore.Sqlite.NetTopologySuite SQLite support for spatial types
Microsoft.EntityFrameworkCore.Cosmos Database provider for Azure Cosmos DB
Microsoft.EntityFrameworkCore.InMemory The in-memory database provider
Microsoft.EntityFrameworkCore.Tools EF Core PowerShell commands for the Visual Studio Package Manager Console; use this to integrate tools like scaffolding and migrations with Visual Studio
Microsoft.EntityFrameworkCore.Design Shared design-time components for EF Core tools
Microsoft.EntityFrameworkCore.Proxies Lazy-loading and change-tracking proxies
Microsoft.EntityFrameworkCore.Abstractions Decoupled EF Core abstractions; use this for features like extended data annotations defined by EF Core
Microsoft.EntityFrameworkCore.Relational Shared EF Core components for relational database providers
Microsoft.EntityFrameworkCore.Analyzers C# analyzers for EF Core

We also published the 7.0 preview 5 release of the Microsoft.Data.Sqlite.Core provider for ADO.NET.

Installing the EF7 Command Line Interface (CLI)

Before you can execute EF7 Core migration or scaffolding commands, you’ll have to install the CLI package as either a global or local tool.

To install the preview tool globally, install with:

dotnet tool install --global dotnet-ef --version 7.0.0-preview.5.22302.2 

If you already have the tool installed, you can upgrade it with the following command:

dotnet tool update --global dotnet-ef --version 7.0.0-preview.5.22302.2 

It’s possible to use this new version of the EF7 CLI with projects that use older versions of the EF Core runtime.

Daily builds

EF7 previews are aligned with .NET 7 previews. These previews tend to lag behind the latest work on EF7. Consider using the daily builds instead to get the most up-to-date EF7 features and bug fixes.

As with the previews, the daily builds require .NET 6.

The .NET Data Community Standup

The .NET data team is now live streaming every other Wednesday at 10am Pacific Time, 1pm Eastern Time, or 17:00 UTC. Join the stream to ask questions about the data-related topic of your choice, including the latest preview release.

Documentation and Feedback

The starting point for all EF Core documentation is docs.microsoft.com/ef/.

Please file issues found and any other feedback on the dotnet/efcore GitHub repo.

The following links are provided for easy reference and access.

Thank you from the team

A big thank you from the EF team to everyone who has used and contributed to EF over the years!

Welcome to EF7.

17 comments

Discussion is closed. Login to edit/delete existing comments.

  • Joel Hodes 0

    Very clearly explained. Thank you.
    The danger of TPT in my mind is that – technically – multiple derived classes can link to the same FK in the base/abstract table. TPH could be an issue if two classes share the same property name but different property type. TPC seems a decent choice but if there’s a lot of polymorphism then I’d probably question whether relational db is the right choice to begin with.

    • Shay RojanskyMicrosoft employee 0

      Re TPT, that’s technically possible, but EF Core wouldn’t produce that state of affairs; it would have to be done externally via SQL etc.
      Re TPH, if two classes in the hierarchy have properties with the same name, EF Core automatically renames them.
      IMHO both TPC and TPH are pretty reasonable if you need polymorphism – it’s not necessarily a reason to stay away from relational databases. But of course there are other choices.

  • Avinash Kunnure 0

    Good detailed description. Thank it’s will help me.

  • Vitalii Bondarchuk 0

    Using default server-side values in the example:
    HasDefaultValueSql(“NEXT VALUE FOR [AnimalIds]”)
    is not the best option. In this case EF will generate much more expensive SQL statements on insert to fetch ID value after insertion.
    Probably better option is to use explicitly defined sequence and client-side LoHi generators assigned to ID properties

    • Shay RojanskyMicrosoft employee 0

      In this case EF will generate much more expensive SQL statements on insert to fetch ID value after insertion.

      I’ve recently looked into this, and the overhead of fetching the database-generated ID is in itself minimal; see this issue where this investigated. Previous versions of EF Core 6.0 did generate sub-optimal insertion SQL in some cases, but in EF Core 7.0 that’s no longer the case.

      Now, you may be referring to what happens if the table with the database-generated ID is a principal in a relationship, and some other table has a foreign key pointing to it. In that case, inserting both principal and dependent requires an extra roundtrip (first insert the principal and get back its ID, only then insert the dependent). In this case, using client ID generation is indeed better – either HiLo, or Guids are the way to go.

      Note that we do have HiLo support in the SQL Server (and PG) providers, for people who want int IDs but without the extra roundtrip (unfortunately that feature isn’t really documented…)

      • Vitalii Bondarchuk 0

        I did a research how it works, but some time ago with EF 5

        How it works:

        1. LoHi semi-client-side generator based on a declared sequence
          INSERT INTO [CategorySeqLoHi] ([Id], [Name]) VALUES (@p0, @p1);

        2. Server-side autoincrenet field
          INSERT INTO [CategoryIdentity] ([Name]) VALUES (@p0);
          SELECT [Id] FROM [CategoryIdentity] WHERE @@ROWCOUNT = 1 AND [Id] = scope_identity();

        3. Client-side sequental guid generator
          INSERT INTO [CategoryGuidVG] ([Id], [Name]) VALUES (@p0, @p1);

        4. Server-side guid with default value as ‘NEWSEQUENTIALID()’
          DECLARE @inserted0 TABLE ([Id] uniqueidentifier, [_Position] [int]);
          MERGE [CategoryGuidSS] USING ( VALUES (@p0, 0)) AS i ([Name], _Position) ON 1=0
          WHEN NOT MATCHED THEN INSERT ([Name]) VALUES (i.[Name]) OUTPUT INSERTED.[Id], i._Position INTO @inserted0;
          SELECT [t].[Id] FROM [CategoryGuidSS] t INNER JOIN @inserted0 i ON ([t].[Id] = [i].[Id]) ORDER BY [i].[_Position];

        Definitely, #4 was most slow. And any field that has defined server-side default value and not assigned on client will inspire similar performance penalties.
        It’s interesting to see how EF 7 handles it.

        “Note that we do have HiLo support in the SQL Server (and PG) providers, for people who want int IDs but without the extra roundtrip (unfortunately that feature isn’t really documented…)”
        Honestly, I didn’t get what you mean. LoHi generator has to periodically make a trip to server to fetch next range of values. Like
        SELECT NEXT VALUE FOR [CategorySequence]
        That’s why exists AddAsync
        And that’s why (I guess) surprisingly using true client-side guid keys showed a bit better performance in my benchmarks than LoHi.
        Btw, what I saw in prod with EF 3.1 and elastic APM it seems even with client-side guids it makes 2 trips – 1st for inserting primary record and 2nd is batch with all records for children tables.

  • Fabien Geraud 0

    Is it possible to use both TPC and TPH on the same inherithance tree?

    • Fabien Geraud 0

      What i mean is if i add to class inheriting FarmAnimal like cow and sheep. Can i use TPC for animal and TPH for FarmAnimal. This way i would have the tables (FarmAnimal, Pets, Cats, Dogs).

      • Marcel 0

        Fantastic question. We are wondering this also.

        This can come up when you have different “clusters” of entities that may each themselves be related, but share some common attribute.

        For example on a media platform with Posts, you might have some common attributes/actions on Post (such as an ID, likes etc.), but have very different Post types that themselves might have derivations. It would make best sense to use TPC for Post, but TPH for any subsequent sub-class derivations to get the best of all worlds.

        Curious if the mapper could handle such scenarios.

  • Eric Blankenburg 0

    Is EF7 going to support the Cosmos DB hierarchical partition keys?

  • Marcel 0

    Fantastic example given in the write-up!
    Both fun and very crisp to follow along (lol @ “EducationLevel” and “FavouriteToy”)

    A key question that hasn’t been addressed here: how are external tables that need to refer to types in a hierarchy handled?
    For example, in TPH/TPT, external tables that reference a hierarchy-mapped type have a foreign key constraint to the root table (Animal)
    ie: In a presumed ‘Ownenships’ table that references people and animals:

    CREATE TABLE [dbo].[Ownerships](
        [PersonID] [bigint] NOT NULL,
        [AnimalID] [nvarchar](36) NOT NULL,
    ) ON [PRIMARY]
    ALTER TABLE [dbo].[Ownerships] ADD  CONSTRAINT [PK_dbo.Ownerships] PRIMARY KEY CLUSTERED 
    (
        [PersonID] ASC,
        [AnimalID] ASC
    )
    ALTER TABLE [dbo].[Ownerships]  WITH CHECK ADD  CONSTRAINT [FK_dbo.Ownerships_dbo.Animals_Id] FOREIGN KEY([Id])
    REFERENCES [dbo].[Animals] ([Id])

    In TPC, these references cannot all be to the same root type, since the table corresponding to it doesn’t exist. How is this scenario handled?
    Does this complexity get “pushed” out to the referencing classes, and are there any limitations that occur as a result?
    (ie: perhaps they have to have a different reference column for each concrete type to refer to each concrete table in the referenced hierarchy in order to encode both the discriminator and the ID – or use some other multi-table foreign key referencing mechanism I am unfamiliar with in RDBMS’s)

    • Andriy SvyrydMicrosoft employee 0

      Such constraints can’t be represented in the database, so we issue a warning if you try to model them. But we’ll let you use it if you are ok with the constraint not being enforced.

      • Marcel 0

        I don’t understand, can there be a navigation property set up to an entity that is part of a TPC hierarchy or not?

        The point above is that the Ownership entity has a relationship link to an Animal entity. This works for both TPH and TPT – does TPC come with an additional constraint where you cannot refer to any of the TPC entities from any other entities?

        This is a very significant limitation and architectural consideration to include in the write-up if so.

        To clarify, are any of the following scenarios supported in TPC? (they are both supported in TPH and TPT)

        class SomeEntity {
            // NOT SUPPORTED in TPC?
            public Animal Animal { get; private set; }
        }
        
        class SomeOtherEntity {
            // NOT SUPPORTED in TPC?
            public IList<Animal> Animals { get; private set; }
        }

        What would viable alternatives be for modelling relations into TPC hierarchies if this is indeed the case?

        • Patrick Roza 0

          I was just thinking the exact same thing. One uses RDBMS primarily because of relationships right?

  • Marina Sundström 0

    I could imagine having the abstract concept of a Customer with it being either a concrete Person or an Organization – sharing the Customer Id/No aspects – but not necessarily anything else.

Feedback usabilla icon