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 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 |
| Owned references and complex properties on a temporal entity | NotSupportedException at model build | a standalone entity without them |
| 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 |
| Retention and partitioning of history | history grows without bound | planned for a later major version; the schema allows it without migrating existing data |
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.
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.
EnableRetryOnFailure()needs your own transaction around a save that Hindsight would otherwise wrap in one. See Transactions.- Pooled and factory-created contexts need a singleton change-context provider. See Change context.