Skip to content

Limitations ​

Not supported ​

Each of these fails loudly, so you find out during development rather than from wrong data.

what happensinstead
Application time and bitemporal historynot implemented; the API leaves room to add it without breaking changesPostgreSQL 19's FOR PORTION OF for application time
Include on a historical queryNotSupportedExceptiona second historical query keyed on the foreign key
AsOf / AllVersions / FromTo / ContainedIn after another operatorthrowsput them first, on the DbSet
ExecuteUpdate / ExecuteDelete on a historical queryNotSupportedExceptionrun them on a normal query of the current rows
Diff on an entity with a versioned shadow property (such as a foreign key with no CLR property)NotSupportedExceptionmap the property on the entity class; see Diff
Owned collections, owned references with their own table, JSON-mapped (ToJson()) members and complex collections on a temporal entityNotSupportedException at model buildtable-split complex properties and owned references, which are versioned
Where / OrderBy on an optional complex or owned member that has no required property, in a historical queryEF Core throws InvalidOperationException (cannot translate)give the member a required property; see Complex and owned members
Inheritance hierarchiesrejected at model buildmap the temporal entity on its own
Restoring an entity to an old versionsaving a historical snapshot throws InvalidOperationExceptioncopy the values onto the current entity
Removing a primary-key property of a temporal entityInvalidOperationException at model buildkeep it, or remove IsTemporal()
Changing a property's store type, precision/scale, max length or converter under the same column nameInvalidOperationException at model buildgive it a different column name instead
Database providers other than NpgsqlInvalidOperationException on first usenone planned
Partitioned history tables from a migrationmigrations create a plain history tableconvert it with SQL
Removing WithRetention() once a migration has itInvalidOperationException at model buildkeep it; see Retention

Known trade-offs ​

Interceptor writer ​

  • It only sees SaveChanges. ExecuteUpdate, ExecuteDelete, raw SQL and other processes write no history. The trigger writer records them. See Interceptor writer. An analyzer (HDST001) catches the common case at compile time — a bulk operation against an entity your project configures with IsTemporal() — but it cannot see which writer you actually use, so it fires at Info severity regardless: see the analyzer diagnostic.
  • Your own SaveChangesInterceptor runs after Hindsight's. A change it makes inside SavingChanges reaches the table without a version. Details.
  • Deleting costs one extra round trip: the row is re-read by key (SELECT … FOR UPDATE) so the tombstone holds real values. If the row is already gone, the save throws.
  • A failed history write leaves deleted entities detached. The transaction rolls back correctly, and Added / Modified entities are restored for a retry, but a Deleted one must be queried and removed again. Details.
  • A retried save of a restored Added entity with a store-generated key sends the key it received in the rolled-back transaction. With GENERATED BY DEFAULT AS IDENTITY (Npgsql's default) the retry succeeds and reuses that id. With GENERATED ALWAYS AS IDENTITY (UseIdentityAlwaysColumn()) PostgreSQL rejects it with error 428C9 until you reset the key property to 0.
  • EnableRetryOnFailure() needs your own transaction around a save that Hindsight would otherwise wrap in one. The trigger writer does not. See Transactions.

Both writers ​

  • Bulk writes have no change context. Under the trigger writer, ExecuteUpdate, ExecuteDelete and raw SQL are recorded with NULL change-context columns.
  • The writers differ in two observable ways: the source of timestamps (TimeProvider or now()), and whether an update that sets a column to its current value writes a version. See Choosing a history writer.
  • The change context is asserted by the application, not guaranteed by the database. See the trust model.
  • Pooled and factory-created contexts need a singleton change-context provider. See Change context.

Pruned history ​

  • A query before the retention horizon fails with a PostgresException (SQLSTATE HS001), not an InvalidOperationException: the check runs in the database, so the error can surface while rows are read. See Queries after pruning.
  • AllVersions() and History<T>() return only the history that is left, which may start with an update. Read the horizon with GetHistoryHorizonAsync to tell.
  • Retention creates a PL/pgSQL function in the migration, under either writer. If the use of PL/pgSQL was revoked from your migration role, the migration fails and rolls back. See Turning retention on.

Released under the MIT License.