Limitations
Not supported
Each of these fails loudly, so you find out during development rather than from wrong data.
| what happens | instead | |
|---|---|---|
| Application time and bitemporal history | not implemented; the API leaves room to add it without breaking changes | PostgreSQL 19's FOR PORTION OF for application time |
Include on a historical query | NotSupportedException | a second historical query keyed on the foreign key |
AsOf / AllVersions / FromTo / ContainedIn after another operator | throws | put them first, on the DbSet |
ExecuteUpdate / ExecuteDelete on a historical query | NotSupportedException | run 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) | NotSupportedException | map 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 entity | NotSupportedException at model build | table-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 query | EF Core throws InvalidOperationException (cannot translate) | give the member a required property; see Complex and owned members |
| Inheritance hierarchies | rejected at model build | map the temporal entity on its own |
| Restoring an entity to an old version | saving a historical snapshot throws InvalidOperationException | copy the values onto the current entity |
| Removing a primary-key property of a temporal entity | InvalidOperationException at model build | keep it, or remove IsTemporal() |
| Changing a property's store type, precision/scale, max length or converter under the same column name | InvalidOperationException at model build | give it a different column name instead |
| Database providers other than Npgsql | InvalidOperationException on first use | none planned |
| Partitioned history tables from a migration | migrations create a plain history table | convert it with SQL |
Removing WithRetention() once a migration has it | InvalidOperationException at model build | keep 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 withIsTemporal()— but it cannot see which writer you actually use, so it fires atInfoseverity regardless: see the analyzer diagnostic. - Your own
SaveChangesInterceptorruns after Hindsight's. A change it makes insideSavingChangesreaches 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/Modifiedentities are restored for a retry, but aDeletedone must be queried and removed again. Details. - A retried save of a restored
Addedentity with a store-generated key sends the key it received in the rolled-back transaction. WithGENERATED BY DEFAULT AS IDENTITY(Npgsql's default) the retry succeeds and reuses that id. WithGENERATED ALWAYS AS IDENTITY(UseIdentityAlwaysColumn()) PostgreSQL rejects it with error428C9until you reset the key property to0. 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,ExecuteDeleteand raw SQL are recorded withNULLchange-context columns. - The writers differ in two observable ways: the source of timestamps (
TimeProviderornow()), 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(SQLSTATEHS001), not anInvalidOperationException: the check runs in the database, so the error can surface while rows are read. See Queries after pruning. AllVersions()andHistory<T>()return only the history that is left, which may start with an update. Read the horizon withGetHistoryHorizonAsyncto 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.