Skip to content

Support for temporal tables for TPT (Table per Type) and other entity-splitting scenarios #26457

Description

@pboucher1960

Temporal tables for TPT tables

We are developing an application that currently use the concept of Table-Per-Type to minimize the duplication of fields instead of using a discriminator that is required with Table-Per-Hierarchy. More tables are planned in the future that will depend on the same parent table.

With the latest version of EF Core, we saw an opportunity to use the new temporal table support to easily keep track of operations in our tables. After adding t => t.IsTemporal() build action to our tables configuration and try to add a new migration, we received the following error:

Temporal tables are only supported for entities using Table-Per-Hierarchy inheritance mapping.

This message is returned through an InvalidOperationException in SqlServerModelValidator.ValidateTemporalTables when EF detect a derived table mapping in one of the tables for which we're trying to configure the temporal build action.

We were not able to find much information related to this restriction of temporal for TPT as per those are quite new features supported in EF Core 5 for TPT and EF Core 6 for temporal tables.

Is support planned in the future for both mechanisms to work on the same set of tables?

Thanks!

Version information

EF Core version: (6.0.0-rc.2.21480.5)
Database provider: (Microsoft.EntityFrameworkCore.SqlServer)
Target framework: (.NET 6.0.0-rc.2.21480.5)
IDE: (e.g. Visual Studio 2022 17.0.0 Preview 6)

Activity

  1. ajcvickers commented on Oct 27, 2021

    @ajcvickers
    Contributor

    @pboucher1960 We discussed this, and we're going to consider it for EF Core 7.0. For it to work correctly, all the tables must be updated inside the same transaction. We do this by default when using SaveChanges in EF Core, but it's possible that a set of TPT tables are updated in a way where the time periods would be inconsistent. We suspect (hope) that will only be a corner case and therefore should not block supporting temporal tables.

  2. added this to the Backlog milestone on Oct 27, 2021
  3. pboucher1960 commented on Oct 27, 2021

    @pboucher1960
    Author

    @ajcvickers Thank you for your answer. Do we have any time frame for EF Core 7.0 to be usable in test cases? In the meantime, we will evaluate a more conventional table structure to meet our requirements.

  4. roji commented on Oct 27, 2021

    @roji
    Member

    @pboucher1960 EF Core 7.0 is planned to release in November of 2022 (a year after 6.0).

  5. LeonarddeR commented on Dec 2, 2021

    @LeonarddeR

    I have the same problem as @pboucher1960 and would like to add an additional suggestion. When activating a temporal table for an entity, it makes sense to make all tables in the hiërarchie temporal. However, personally I'd prefer an approach where only the base entity has a temporal table whereas the derived entities don't have one. Therefore, the temporal information is only available when querying the base entity.

    For it to work correctly, all the tables must be updated inside the same transaction. We do this by default when using SaveChanges in EF Core, but it's possible that a set of TPT tables are updated in a way where the time periods would be inconsistent. We suspect (hope) that will only be a corner case and therefore should not block supporting temporal tables.

    I don't think that's a real corner case actually. What happens if you only update data in a derived entity? That means the table containing the base entity's data isn't touched at all. Not even mentioning that there are cases (as in our case) where an external process is responsible for filling the actual tables.

  6. changed the title [-]Is support for temporal tables for TPT (Table per Types) planned in the future and if so, when?[/-] [+]Support for temporal tables for TPT (Table per Type)[/+] on Jan 11, 2022
  7. changed the title [-]Support for temporal tables for TPT (Table per Type)[/-] [+]Support for temporal tables for TPT (Table per Type) and other table-splitting scenarios[/+] on Jun 15, 2022
  8. changed the title [-]Support for temporal tables for TPT (Table per Type) and other table-splitting scenarios[/-] [+]Support for temporal tables for TPT (Table per Type) and other entity-splitting scenarios[/+] on Jun 15, 2022
  9. 4 remaining items

  10. KillianDav commented on Jun 22, 2023

    @KillianDav

    It's a pretty major drawback to be unable to use system versioning with TPT hierarchies. Table per hierarchy is not appropriate for complex inheritance that is typical in real world apps, and it isn't good to be forced to use TPH just to enable the use of system versioning (which has been around for some time and is useful). This sort of thing I would hope is not based on votes that may or may not accurately reflect the end user collective desire for a feature. This is fairly fundamental functionality and should work for completeness, it sounds rather like it is possibly being avoided due to complexity given that it was due in 7?

  11. jarednapier commented on Dec 14, 2023

    @jarednapier

    Is there going to be a bug fix with TPC mapping and temporal tables?

  12. skoehlerrto commented on May 15, 2024

    @skoehlerrto

    Just asking: Can we see this working on TPC/TPT in the next time (EF Core 9 f.e.)?
    I'm pretty sure I already know the answer, but i dont want to hear read it.

  13. ajcvickers commented on May 15, 2024

    @ajcvickers
    Contributor

    @skoehlerrto It's not going to happen for EF9. This issue has 29 votes (👍) at the time of writing. That means that there are 108 issues on the backlog with more votes. The best thing you can do is vote for this issue, but implementation is unlikely to happen anytime soon.

  14. salgaard commented on Sep 11, 2024

    @salgaard

    what the hell haha. this is kind of weird. it seems quite fundamental to have this. more 👍 please haha :)
    Anyone have a nice alternative?

  15. SpaceOgre commented on Sep 26, 2024

    @SpaceOgre

    Just ran into this today, does anyone have a good workaround for TPC?
    It feels incorrect to change my model back to TPH since I only want one of the types at a time

  16. jarednapier commented on Sep 26, 2024

    @jarednapier

    what the hell haha. this is kind of weird. it seems quite fundamental to have this. more 👍 please haha :) Anyone have a nice alternative?

    Just ran into this today, does anyone have a good workaround for TPC? It feels incorrect to change my model back to TPH since I only want one of the types at a time

    I've had success not referencing the abstract type at all when configuring EF Core. The downside to this is you have to copy/paste identical configuration code into every type. My caveat is that my abstract classes are pretty simple, but I do have derived types that EF Core sets up with temporal tables in the DB.

  17. Venom1991 commented on Dec 28, 2024

    @Venom1991

    what the hell haha. this is kind of weird. it seems quite fundamental to have this. more 👍 please haha :) Anyone have a nice alternative?

    Just ran into this today, does anyone have a good workaround for TPC? It feels incorrect to change my model back to TPH since I only want one of the types at a time

    I've had success not referencing the abstract type at all when configuring EF Core. The downside to this is you have to copy/paste identical configuration code into every type. My caveat is that my abstract classes are pretty simple, but I do have derived types that EF Core sets up with temporal tables in the DB.

    Yeah, I had to do this as well. Luckily, there wasn't a lot of copy-pasting required in my case.

  18. added theissue type on Jun 15, 2025
  19. Fronix commented on Oct 1, 2025

    @Fronix

    Can't we just activate temporal tables manually without EF knowing about it? EF doesn't need to know about the temporal tables since SQL-server handles all the heavy lifting anyway. That would make EF happy when using TPC and TPT?

  20. buvinghausen commented on Oct 10, 2025

    @buvinghausen
    Contributor

    Yeah, I had to do this as well. Luckily, there wasn't a lot of copy-pasting required in my case.

    You should never copy/paste.....

    /// <summary>
    /// Provides a base configuration for entities inheriting from <see cref="EntityRowBase"/>.
    /// </summary>
    public static class EntityConfigurationExtensions
    {
    	/// <summary>
    	/// Configures the base properties for the entity type.
    	/// </summary>
    	/// <param name="builder">The entity type builder.</param>
    	/// <param name="isTemporal">Flag to indicate if table should be temporal or not.</param>
    	/// <param name="excludeFromMigrations">Flag to include or exclude from migrations.</param>
    	/// <returns>An <see cref="EntityTypeBuilder{T}" /> for further configuration.</returns>
    	public static EntityTypeBuilder<T> ConfigureBase<T>(this EntityTypeBuilder<T> builder, bool isTemporal = true, bool excludeFromMigrations = false) where T : EntityRowBase
    	{
    		// First map to temporal table and configure the primary key
    		builder
    			.ToTable(typeof(T).Name, e =>
    			{
    				e.IsTemporal(isTemporal);
    				e.ExcludeFromMigrations(excludeFromMigrations);
    			})
    			.HasKey(e => e.Id);
    
    		// Make sure the id is not generated by the database
    		builder
    			.Property(e => e.Id)
    			.ValueGeneratedNever();
    
    		// Configure the row version for concurrency
    		builder
    			.Property(e => e.RowVersion)
    			.IsRowVersion();
    
    		// Return the builder for further configuration
    		return builder;
    	}
    
    	/// <summary>
    	/// Configures the base properties for the bridge entity.
    	/// </summary>
    	/// <param name="builder">The entity type builder.</param>
    	/// <param name="key">The expression to define the primary key</param>
    	/// <param name="isTemporal">Flag to indicate if table should be temporal or not.</param>
    	/// <param name="excludeFromMigrations">Flag to include or exclude from migrations.</param>
    	/// <returns>An <see cref="EntityTypeBuilder{T}" /> for further configuration.</returns>
    	public static void ConfigureBridge<T>(this EntityTypeBuilder<T> builder, Expression<Func<T, object?>> key, bool isTemporal = true, bool excludeFromMigrations = false) where T : class => builder
    		.ToTable(typeof(T).Name, e =>
    		{
    			e.IsTemporal(isTemporal);
    			e.ExcludeFromMigrations(excludeFromMigrations);
    		})
    		.HasKey(key);
    }
  21. mdzieg commented on Nov 18, 2025

    @mdzieg

    The issue was opened 4 years ago. Why do I have to be affected again by issues nobody cares to fix?
    4 long years to get this solved, and we got nothing. Well, almost nothing. We got copilot everywhere...

  22. buvinghausen commented on Aug 5, 2026

    @buvinghausen
    Contributor

    Coming back to this one with a use case the workaround I posted above cannot solve, and which I don't think this issue's framing currently contemplates: non-temporal fragments of a temporal entity — asymmetric temporality via entity splitting.

    The scenario. We're making the ASP.NET Core Identity users table temporal. The driver is erasure/audit compliance built on crypto-shredding (NIST SP 800-88 cryptographic erase applied at the field level): PII columns are ciphertext under per-subject keys, temporal history is the audit record, and destroying a subject's key renders every copy — current row, history rows, backups — unreadable in one operation. Temporal history isn't incidental here; it is the compliance artifact.

    The problem. Identity keeps AccessFailedCount and LockoutEnd on the user entity. With the table temporal, every wrong-password attempt mints a history row — the audit log drowns in rate-limiter churn. The natural EF answer is entity splitting:

    builder.SplitToTable("UserLockout", lockout =>
    {
        lockout.Property(u => u.ConcurrencyStamp);
        lockout.Property(u => u.SecurityStamp);
        lockout.Property(u => u.PasswordHash);
        lockout.Property(u => u.LockoutEnd);
        lockout.Property(u => u.AccessFailedCount);
    });

    …with the explicit intent that users is temporal and UserLockout is not. The asymmetry is the entire point of the split.

    What happens today. Temporality is an entity-type annotation, so it chains onto every fragment — #30366 (closed as a duplicate of this issue) shows all fragments stamped SqlServer:IsTemporal sharing a single TemporalHistoryTableName, then an NRE in migration generation. Checking current main, SqlServerModelValidator.ValidateTemporalTable still has no entity-splitting guard, and no SplitTableBuilder.IsTemporal(...) surface exists — so this is neither supported nor cleanly blocked. It's the same per-store-object metadata gap I ran into from the other direction in efcore/EFCore.NamingConventions#396 (key/FK constraint names have no per-store-object override either).

    The ask. When this gets picked up, please make per-fragment temporal control part of the design — specifically the ability for a fragment table of a temporal entity to be non-temporal. Supporting splitting by making all fragments temporal would leave this use case exactly where it is today.

    Happy to put together a minimal repro against the .NET 11 previews if that helps.

    cc @roji — this is the SQL Server half of a story we're building cross-provider (trigger-based history on the PG side); would value your read on whether per-store-object temporal facets are feasible as part of this issue's scope.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions