Skip to content

Rules for historical queries ​

These apply to AsOf, AllVersions, FromTo, ContainedIn and History<T>. Each rule exists because breaking it would return a result that looks right and is not; in every case Hindsight throws instead.

History is read-only ​

Results are always no-tracking: detached snapshots that leave the change tracker untouched, and adding .AsTracking() throws.

Saving a snapshot back also throws. Re-attaching one with Update, Attach, Add, Remove or by setting its state, and then calling SaveChanges, would write the past over the present and create a spurious version; it throws InvalidOperationException instead. This covers Version<T>.Entity too.

To bring back an old value, copy it onto the current, tracked entity and save that:

cs
var snapshot = await db.Policies.AsOf(before).SingleAsync(p => p.Id == policyId);
var current = await db.Policies.SingleAsync(p => p.Id == policyId);

current.Premium = snapshot.Premium; // copy onto the tracked, current entity
await db.SaveChangesAsync();        // a new version that carries the old premium

No Include ​

Include and ThenInclude after a historical query throw NotSupportedException. Reading related entities as of a point in time is a join on two periods, and a plain join would pair versions that never existed together.

Load related rows with a second historical query, keyed on the foreign key you already have.

AsOf, AllVersions, FromTo and ContainedIn come first ​

Put them directly on the DbSet, before Where, OrderBy or Select. db.Policies.Where(…).AsOf(t) throws, and so does db.Policies.Where(…).FromTo(from, to). (History<T>() starts from the DbContext, so there is nothing to put before it.)

No ExecuteUpdate or ExecuteDelete ​

ExecuteUpdate and ExecuteDelete on a historical query throw NotSupportedException. Left alone, EF Core would turn the query into an UPDATE of the history table itself and silently rewrite the audit trail. Select the current rows with a normal query and run ExecuteUpdate or ExecuteDelete on that.

Global query filters apply to history ​

AsOf, AllVersions, FromTo, ContainedIn and History<T> apply the entity's global query filters to each stored version. A tenant or organization filter therefore uses the historical row's values and EF Core reads context members at query execution, so changing the tenant on a reused context does not reuse the previous tenant's value. IgnoreQueryFilters() and named-filter overloads work on historical operators as they do on ordinary queries. A filter over a primitive collection, such as an array of partner ids tested with Contains, translates on history as it does on the main table.

If a filter uses an entity property that is not stored in the history table, Hindsight throws NotSupportedException when a historical query for that entity is compiled rather than silently skipping that filter. Model construction and current-data queries remain available.

Temporal entities only ​

A historical query on an entity that is not temporal throws InvalidOperationException naming the entity. So does one on an entity that used to be temporal: its history table is kept for the data, but it is no longer queryable with LINQ.

Standalone entities only ​

Entities in an inheritance hierarchy, or with owned collections or JSON-mapped members, cannot be temporal (see Model validation); a historical query on one throws NotSupportedException. Read the history table with FromSql if you have to. Table-split complex properties and owned references are versioned and rebuilt.

Released under the MIT License.