Skip to content

Temporal entities ​

An entity becomes temporal with one call in OnModelCreating:

cs
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Policy>().IsTemporal();
}

The next dotnet ef migrations add creates its history table. See What a migration creates.

Defaults ​

settingdefault
history table<main table>_history, in the main table's schema
period columnsvalid_from, valid_to
versioned propertiesevery mapped property of the entity

Customizing ​

Pass a builder to IsTemporal to change any default:

cs
modelBuilder.Entity<Policy>(policy =>
{
    policy.ToTable("policies");

    policy.IsTemporal(temporal => temporal
        .UseHistoryTable("policy_versions", schema: "audit") // default: policies_history, same schema
        .HasPeriodStart("sys_from")                          // default: valid_from
        .HasPeriodEnd("sys_to")                              // default: valid_to
        .Exclude(p => p.UpdatedAt));                         // never copied to history
});
methodeffect
UseHistoryTable(name, schema)Names the history table, and optionally its schema. Changing it later renames the table; see Renaming tables.
HasPeriodStart(column), HasPeriodEnd(column)Names the period columns.
Exclude(property)Leaves a property out of the history; see below.
WithDbSessionUser()Adds the db_session_user audit column; see Change context.

Excluding properties ​

Exclude removes a property from versioning completely:

  • the history table has no column for it;
  • a SaveChanges that changed only excluded properties writes no history row.

Use it for bookkeeping columns that change often and mean little on their own, such as UpdatedAt or a cached calculation. The sample project excludes Policy.UpdatedAt:

cs
using Hindsight;
using Microsoft.EntityFrameworkCore;

namespace InsuranceSample;

public sealed class InsuranceDbContext(DbContextOptions<InsuranceDbContext> options) : DbContext(options)
{
    public DbSet<Policy> Policies => Set<Policy>();

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Policy>(e =>
        {
            e.ToTable("policies");
            e.Property(p => p.Status).HasConversion<string>();
            e.IsTemporal(t => t.Exclude(p => p.UpdatedAt));
        });
    }
}

You can exclude some properties of a composite primary key, but not all of them: Hindsight needs at least one key column to tell which history rows belong to which entity.

What can be temporal ​

A temporal entity must be a standalone entity type mapped to its own table, with a primary key. Owned references, complex properties and inheritance hierarchies are rejected when the model is built. See Model validation for the full list and what to do instead.

Released under the MIT License.