Repository navigation
Releases: horesse/hindsight-efcore
Release list
v1.4.3
What's changed
🐛 Fixes
- fix(model): global query filters over array columns translate on historical queries (#107)
🧰 Maintenance
- chore(release): bump validation baseline for v1.4.2 (#105)
Upgrading
No code change and no database step. A global query filter over a primitive collection (for example p.PartnerIds.Contains(id)) now translates on AsOf, AllVersions, FromTo, ContainedIn and History<T>, as it does on the main table; in 1.4.2 it did not.
Full changelog: v1.4.2...v1.4.3
Install: dotnet add package Hindsight.EntityFrameworkCore.PostgreSQL --version 1.4.3
v1.4.2
What's changed
🐛 Fixes
- fix(query): apply global filters to history reads (#103)
- fix(model): history columns share the source property's value comparer (#102)
🧰 Maintenance
- chore(release): bump validation baseline for v1.4.1 (#97)
- docs(release): attach the global-filter note to 1.4.2 (#104)
Upgrading
No code change and no database step, but one behavior change to check:
- Historical queries now apply the entity's global query filters.
AsOf,AllVersions,FromTo,ContainedInandHistory<T>used to ignore them, so they may now return fewer rows. If a filter uses a member that cannot be represented in history, only a historical query for that entity throwsNotSupportedException; model construction and current-data queries keep working. Review the filters on your temporal entities before relying on historical reads.
See Upgrading to 1.4.2.
Full changelog: v1.4.1...v1.4.2
Install: dotnet add package Hindsight.EntityFrameworkCore.PostgreSQL --version 1.4.2
v1.4.1
What's changed
🐛 Fixes
- fix: run the Trigger writer under EnableRetryOnFailure, migrate temporal tables with foreign keys, accept enum and ltree columns, load HDST001 on every .NET 10 SDK (#96)
🧰 Maintenance
- chore(release): ship PublicAPI surface and bump validation baseline for v1.4.0 (#95)
Upgrading
No code change and no database step. 1.4.1 removes the need for four workarounds; each one keeps working if you leave it in place:
- The
HDST001analyzer loads on every .NET 10 SDK. Up to 1.4.0 it needed a newer compiler than the 10.0.1xx SDKs ship, and the build failed withCS9057. If you removed the analyzer from the build, you can put it back. HistoryWriter.Triggerworks withEnableRetryOnFailure(), which Aspire turns on by default. A plainSaveChangesno longer throws, and a retried attempt records the change context too. The Interceptor writer still needs your own transaction under retry.- A temporal table with a foreign key (to another table or to itself) migrates in one step. The history seed and the trigger used to run before the main table was created (
relation … does not exist). - An enum without
HasConversion, or an NpgsqlLTree, no longer fails the next migration with "changed its store type". A converter added only to get past this is no longer needed.
See Upgrading to 1.4.1.
Full changelog: v1.4.0...v1.4.1
Install: dotnet add package Hindsight.EntityFrameworkCore.PostgreSQL --version 1.4.1
v1.4.0
What's changed
✨ Features
- feat(writer): prune history older than a cutoff with PruneHistoryAsync (#94)
- feat(model): support complex properties and owned references on temporal entities (D9) (#93)
- feat(context): rename ChangeContext.UserId/UserName to ChangedBy/ChangedByName (source-breaking) (#92)
- feat(sample): add a history viewer web sample (#90)
- feat(query): add FromTo and ContainedIn time-range operators (#89)
- feat(query): diff two versions of an entity (#88)
📚 Documentation
- docs: stop tying history retention to a major version (#91)
🧰 Maintenance
- chore(release): ship HDST001 and bump validation baseline for v1.3.0 (#87)
Upgrading
ChangeContext.UserId/UserNameare renamed toChangedBy/ChangedByName, the namesVersion<TEntity>and the history columns already use. AnIChangeContextProviderthat sets the old names stops compiling (CS0117): rename them there. No database step: the columns, the Trigger writer's session settings and the stored values are unchanged. If you matchChangeContext.ToString()output in logs, update the pattern. See Upgrading to 1.4.- Retention is opt-in: nothing changes until you call
WithRetention()on an entity. The nextdotnet ef migrations addthen creates thehindsight_retention_horizontable andhindsight_history_retainedfunction; nothing is deleted until you callPruneHistoryAsync. See Retention and partitioning.
Full changelog: v1.3.0...v1.4.0
Install: dotnet add package Hindsight.EntityFrameworkCore.PostgreSQL --version 1.4.0
v1.3.0
What's changed
✨ Features
- feat(migrations): auto-seed history when an existing table becomes temporal (D6) (#79)
- feat(analyzer): warn on ExecuteUpdate/ExecuteDelete bypassing history under Interceptor (D4) (#80)
🐛 Fixes
- fix(deps): restore the Npgsql 10.0.1 floor a routine bump raised to 10.0.3 (#81)
📚 Documentation
- docs: state the Include and TPH limits without pinning them to v1 (#85)
🧰 Maintenance
- ci: make the EF Core canaries actually run and restore Release Drafter's PR labels (#84)
- chore(ci): fix stale bug-report link and drop the unused v1.1 label (#78)
- chore(release): ship PublicAPI surface and bump validation baseline for v1.2.0 (#77)
Upgrading
- If you hand-wrote history seeding SQL when making an existing table temporal (as the 1.2 docs told you to), remove it from your next such migration:
dotnet ef migrations addnow generates thatINSERT … SELECTitself, and keeping both seeds every existing row twice. Migrations you already applied are unaffected. See Upgrading to 1.3. - HDST001 (Info) now flags
ExecuteUpdate/ExecuteDeleteagainst a temporal entity: under the Interceptor writer those calls never wrote history, and the analyzer makes that visible. It cannot see which writer you use, so it fires under Trigger too — there it is a false positive; suppress it at the call site (how). It ships inside the package; nothing to install. - The declared minimum for
Npgsql.EntityFrameworkCore.PostgreSQLgoes back to 10.0.1 (1.2.0 required 10.0.3 by accident). No action needed.
Full changelog: v1.2.0...v1.3.0
Install: dotnet add package Hindsight.EntityFrameworkCore.PostgreSQL --version 1.3.0
v1.2.0
What's changed
✨ Features
- feat(context): add opt-in db_session_user audit column (D16) (#72)
- feat(sample): add ProductCatalog and TaskTracker showcase samples (#70)
🐛 Fixes
- fix(migrations): reject unsafe history column type changes and fix schema-move DDL (#69)
- fix(migrations): stop MigrateAsync from double-creating history tables on a fresh database (#71)
📚 Documentation
- docs: restructure README for a developer landing from search (#62)
- docs(release): add upgrade guide section for #69 and #72 (#75, #76)
🧰 Maintenance
- chore(deps): bump NuGet packages to latest patch/minor (#68)
- test(writer): add concurrency stress test for 50 parallel updates (#63)
- chore(deps): lower EF Core/Npgsql floor, add 10.x compat canary (#61)
- chore(deps): bump GitHub Actions to latest majors (#66)
Full changelog: v1.1.1...v1.2.0
Install: dotnet add package Hindsight.EntityFrameworkCore.PostgreSQL --version 1.2.0
v1.1.1
What's changed
📚 Documentation
- docs: replace DocFX with a versioned VitePress site and drop CHANGELOG.md (#53)
The documentation moved to https://horesse.github.io/hindsight-efcore/ and is now versioned per minor
release; this release publishes the 1.1 documentation. No behavior or public API changes: two
exception messages now link to the documentation site instead of a file in the repository, and the
package's release notes point to GitHub Releases.
Full changelog: v1.1.0...v1.1.1
Install: dotnet add package Hindsight.EntityFrameworkCore.PostgreSQL --version 1.1.1
v1.1.0
What's changed
✨ Features
- feat(migrations): add GiST index on the history period range (#28)
🐛 Fixes
- fix(migrations): rename the period-range index when a history table is renamed (#50)
- fix(writer): stop stale set_config from leaking reason across SaveChanges in a caller transaction (#47)
- fix(migrations): regenerate trigger DDL when a temporal or history table is renamed (#42)
- fix(query): reject ExecuteUpdate/ExecuteDelete on AsOf()/AllVersions()/History<T>() queries (#44)
- fix(model): reject temporal entity configurations whose generated identifiers exceed PostgreSQL's 63-byte limit (#43)
- fix(context): report unsafe IChangeContextProvider resolution under context pooling (#41)
- fix(writer): cooperate with an ambient TransactionScope instead of opening a conflicting transaction (#40)
- fix(context): validate the Npgsql provider is configured and fail early otherwise (#39)
- fix(context): scope WithReason's override to the specific DbContext instance (#37)
- fix(query): tag history-derived entities reachable through any argument of Concat/Union, not just the first (#35)
- fix(writer): capture real row values for a delete tombstone when the entity was never loaded (#33)
- fix(writer): restore entity state after a failed history write (#32)
- fix(model): reject IsTemporal() when the entire primary key is excluded from history (#31)
- fix(model): reject entity columns that collide with fixed history columns (#30)
- fix(writer): report EnableRetryOnFailure + owned-transaction conflict clearly (#29)
- fix(writer): reject owned/complex properties on temporal entities at model build (#27)
- fix(writer): clamp interceptor close timestamps so concurrent updates stay contiguous (#26)
⚡ Performance
- perf(writer): stop redoing DetectChanges() when HistorySnapshotGuardInterceptor already ran it (#38)
📚 Documentation
- docs(writer): document that a custom interceptor after Hindsight's own is invisible to HistoryWriter.Interceptor (#49)
- docs: document the period-range index upgrade gap (#46)
- docs: document the change-context trust model and propose a session_user audit column (#45)
- docs(writer): plan the v2.0 default-writer question and sharpen the production-writer warning (#34)
🧰 Maintenance
- chore(release): prepare 1.1.0 (#52)
- refactor: tighten private-const naming rule and trim internal PR-history comments (#48)
- test(writer): pin down retried SaveChanges for a store-generated key after a failed history write (#51)
- chore(release): move 1.0.0 symbols into PublicAPI.Shipped.txt and enable the package-validation baseline (#36)
Full changelog: v1.0.0...v1.1.0
Install: dotnet add package Hindsight.EntityFrameworkCore.PostgreSQL --version 1.1.0
v1.0.0
Added
- Repository scaffold: solution, packaging, CI, release workflow.
- Benchmark numbers comparing
HistoryWriter.InterceptorandHistoryWriter.Triggeragainst plain
EF Core —SaveChangesinsert / update / delete overhead and the cost of attaching a change
context — inREADME.mdand the History writers guide. Run with
dotnet run -c Release --project benchmarks/Hindsight.Benchmarks -- --filter '*'(needs Docker). IsTemporal()fluent configuration writingHindsight:*annotations.UseHindsight()onDbContextOptionsBuilder: enables the model-finalizing convention that
generates a property-bag history entity type for everyIsTemporal()entity, so
dotnet ef migrations addcreates the history table alongside the main table. The history table
carries the versioned columns (excluded properties omitted, every column nullable), the period
columnsvalid_from/valid_to, the change-context columns
(operation,changed_by,changed_by_name,correlation_id,reason,extra), an identity
history_idprimary key, and a(key…, valid_from desc)index.- Model validation: a temporal entity without a primary key throws
InvalidOperationException; a
temporal entity in a TPH hierarchy throwsNotSupportedException. - History is now written: inserts, updates and deletes of a temporal entity produce history rows in
the same transaction as the data change, with half-open[valid_from, valid_to)intervals and one
timestamp perSaveChangestaken from the registeredTimeProvider. A delete closes the open
version and records a tombstone row (operation = 3, empty interval), so a deleted entity has no
open history row. ASaveChangesthat touched only excluded properties writes nothing. HistoryWriterenum andUseHindsight(h => h.UseHistoryWriter(...))to choose the writer. The
default isHistoryWriter.Interceptor.HistoryWriter.Triggeris now implemented and recommended in production. Selecting it makes the
migration generate a<history_table>_write()plpgsql function and an
AFTER INSERT OR UPDATE OR DELETE<history_table>_trgtrigger on the main table, from the same
Hindsight:*annotations the history table is built from. It produces the same history schema and
the same half-open intervals as the interceptor, usesnow()for the timestamp (one per
transaction), closes the previous version with
GREATEST(now(), valid_from + interval '1 microsecond')so racing transactions stay contiguous, and
needs only table ownership — no superuser, no extension. Because it lives in the database it also
records history forExecuteUpdate/ExecuteDeleteand raw SQL (DESIGN.md D4). A later migration
that adds, drops or renames a column on a temporal entity re-emitsCREATE OR REPLACE FUNCTION; no
migration ever drops the function's history table. Under the trigger writer, anUPDATEthat sets a
versioned column to its current value writes no history row.HistoryWriter.Interceptordoes not seeExecuteUpdate/ExecuteDelete/ raw SQL (DESIGN.md D4);
HistoryWriter.Triggerdoes.- Change context under
HistoryWriter.Trigger: the registeredIChangeContextProvideris called once
perSaveChangesthat writes a temporal entity, and itsChangeContextis pushed into the
transaction withset_config('hindsight.<key>', value, true)for the trigger to read back with
current_setting(..., true). Hindsight opens a transaction for thatSaveChangesif the caller has
none.DbContext.WithReason("…")works the same as in interceptor mode. Bulk writes get history
rows withNULLcontext columns. - Change context:
IChangeContextProviderandUseHindsight(h => h.WithChangeContext<T>()). When a
provider is registered,HistoryWriter.Interceptorcalls it once perSaveChangesand writes the
returnedChangeContext(UserId,UserName,CorrelationId,Reason,ExtraJSON) into the
changed_by,changed_by_name,correlation_id,reasonandextracolumns of that call's
history rows. Without a provider those columns stayNULLandSaveChangesstill succeeds.
DbContext.WithReason("…")opens a scope that overrides the reason for theSaveChangescalls
inside it. Hindsight ships no default provider. queryable.AsOf(DateTimeOffset): reads each temporal entity as it stood at a point in time from
its history table.AsOfmust be the first operator on the query;Where,OrderBy,Select,
First,Any,Countcompose after it and translate to a single SQL query with the instant as a
parameter and the half-open predicatevalid_from <= @asOf AND valid_to > @asOf. Results are
always no-tracking;AsOf(...).AsTracking()throws.AsOfon a non-temporal entity throws
InvalidOperationException;AsOfwithInclude/ThenIncludethrowsNotSupportedException
(DESIGN.md D8);AsOfon an inheritance hierarchy or an entity with owned/complex members throws
NotSupportedException.queryable.AllVersions(): reads every stored version of a temporal entity from its history table —
one row per insert and update, newest first (byvalid_fromdescending, replaceable with your own
OrderBy). Thedeletetombstone is excluded, so a deleted entity's timeline ends at the version
that was open when it was deleted. Same rules asAsOf: first operator on the query, composes into
a single SQL query, always no-tracking, and the sameInvalidOperationException/
NotSupportedExceptionguards for non-temporal entities,Include,AsTracking, inheritance and
owned/complex members.db.History<T>(): reads every stored version of a temporal entity as aVersion<T>— the entity
snapshot plus the version's system-time period (ValidFrom/ValidToasDateTimeOffset,
IsCurrentfor the open one) and the change-context columns (Operation,ChangedBy,
ChangedByName,CorrelationId,Reason,Extra). UnlikeAllVersions()it includes the
deletetombstone asVersionwithOperation == VersionOperation.Deleteand an empty interval,
so it is the delete audit. Newest first (byvalid_fromdescending, thenhistory_id), replaceable
with your ownOrderBy.WhereandSelectcompose into a single SQL query through both the
metadata members and theEntitysnapshot (Where(v => v.Entity.Id == id)), always no-tracking.
SameInvalidOperationException/NotSupportedExceptionguards asAsOf/AllVersionsfor
non-temporal entities,Include,AsTracking, inheritance and owned/complex members. New public
Version<TEntity>andVersionOperationtypes.- History results are read-only, enforced (DESIGN.md D7). Re-attaching an entity that came from
AsOf()/AllVersions()/History<T>()(Update,Attach,Add,Remove, or a manual state
change) and callingSaveChangesthrowsInvalidOperationExceptioninstead of silently writing the
stale snapshot back as the current version.Version<T>.Entityis covered too. Copying values onto
a fresh instance, or onto one fromDbSet.Find()/ a normal query, is unaffected. No new public
API. - Removing a property from a temporal entity now keeps its history column (DESIGN.md D6, golden
rule 3).dotnet ef migrations adddrops the column from the main table only; on the history table
the column stays, forced nullable, and is taggedHindsight:Orphanedin the model. The convention
learns which columns to keep by reading the previousModelSnapshot, so no migration ever emits a
DropColumn/DropTable/ narrowingAlterColumnon a history table. Removing a primary-key
property from a temporal entity throwsInvalidOperationException— the history version index and
the writer's close-previous-version step need the key columns. No new public API. - Removing
IsTemporal()from an entity entirely — not just one property — now keeps its history
table too (DESIGN.md D6, golden rule 3).dotnet ef migrations addnever emits aDropTablefor a
history table just because its source stopped being temporal (or was removed from the model): the
whole history entity type is cloned back from the previousModelSnapshot— columns, key, indexes —
and taggedHindsight:Orphanedon the entity type itself, the same way a single removed column was
already retained.AsOf()/AllVersions()/History<T>()against the now-non-temporal entity
still throwInvalidOperationExceptionas before; only the table's fate changes. No new public API. - Removing
IsTemporal()from an entity underHistoryWriter.Triggernow also drops the trigger on the
main table. Previously the trigger — which lives on the main table, not the history table — kept
firing on every insert/update/delete and kept writing into the now-orphaned history table, even though
the entity was no longer temporal in the model. The migration that removesIsTemporal()now emits a
DROP FUNCTION ... CASCADEfor it (which also drops the dependent trigger); no data is deleted, only
the write path stops. No new public API.
Changed
HistoryWriter.Interceptornow writes all history rows of aSaveChangesin a single database
round-trip (a batchedDbBatch, chunked at 512 rows) instead of oneUPDATE+INSERTround-trip
per row. Same history rows, same intervals; large batch saves are dramatically faster.