Skip to content

Releases: horesse/hindsight-efcore

v1.4.3

Choose a tag to compare

@github-actions github-actions released this 03 Oct 21:56
1b3cdda

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

Choose a tag to compare

@github-actions github-actions released this 03 Oct 21:02
05587e3

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, ContainedIn and History<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 throws NotSupportedException; 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

Choose a tag to compare

@github-actions github-actions released this 25 Sep 23:20
fe8b051

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 HDST001 analyzer 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 with CS9057. If you removed the analyzer from the build, you can put it back.
  • HistoryWriter.Trigger works with EnableRetryOnFailure(), which Aspire turns on by default. A plain SaveChanges no 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 Npgsql LTree, 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

Choose a tag to compare

@github-actions github-actions released this 25 Sep 22:32
8e5da59

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 / UserName are renamed to ChangedBy / ChangedByName, the names Version<TEntity> and the history columns already use. An IChangeContextProvider that 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 match ChangeContext.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 next dotnet ef migrations add then creates the hindsight_retention_horizon table and hindsight_history_retained function; nothing is deleted until you call PruneHistoryAsync. 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

Choose a tag to compare

@github-actions github-actions released this 24 Sep 21:05
fee6401

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 add now generates that INSERT … SELECT itself, and keeping both seeds every existing row twice. Migrations you already applied are unaffected. See Upgrading to 1.3.
  • HDST001 (Info) now flags ExecuteUpdate / ExecuteDelete against 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.PostgreSQL goes 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

Choose a tag to compare

@github-actions github-actions released this 17 Sep 19:05
4d40d05

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

Choose a tag to compare

@github-actions github-actions released this 13 Sep 22:12
5837d50

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

Choose a tag to compare

@github-actions github-actions released this 13 Sep 21:01
479250e

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

Choose a tag to compare

@horesse horesse released this 11 Sep 17:01
8e0f58e

Added

  • Repository scaffold: solution, packaging, CI, release workflow.
  • Benchmark numbers comparing HistoryWriter.Interceptor and HistoryWriter.Trigger against plain
    EF Core — SaveChanges insert / update / delete overhead and the cost of attaching a change
    context — in README.md and the History writers guide. Run with
    dotnet run -c Release --project benchmarks/Hindsight.Benchmarks -- --filter '*' (needs Docker).
  • IsTemporal() fluent configuration writing Hindsight:* annotations.
  • UseHindsight() on DbContextOptionsBuilder: enables the model-finalizing convention that
    generates a property-bag history entity type for every IsTemporal() entity, so
    dotnet ef migrations add creates the history table alongside the main table. The history table
    carries the versioned columns (excluded properties omitted, every column nullable), the period
    columns valid_from / valid_to, the change-context columns
    (operation, changed_by, changed_by_name, correlation_id, reason, extra), an identity
    history_id primary 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 throws NotSupportedException.
  • 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 per SaveChanges taken from the registered TimeProvider. A delete closes the open
    version and records a tombstone row (operation = 3, empty interval), so a deleted entity has no
    open history row. A SaveChanges that touched only excluded properties writes nothing.
  • HistoryWriter enum and UseHindsight(h => h.UseHistoryWriter(...)) to choose the writer. The
    default is HistoryWriter.Interceptor.
  • HistoryWriter.Trigger is 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>_trg trigger 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, uses now() 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 for ExecuteUpdate / ExecuteDelete and raw SQL (DESIGN.md D4). A later migration
    that adds, drops or renames a column on a temporal entity re-emits CREATE OR REPLACE FUNCTION; no
    migration ever drops the function's history table. Under the trigger writer, an UPDATE that sets a
    versioned column to its current value writes no history row.
  • HistoryWriter.Interceptor does not see ExecuteUpdate / ExecuteDelete / raw SQL (DESIGN.md D4);
    HistoryWriter.Trigger does.
  • Change context under HistoryWriter.Trigger: the registered IChangeContextProvider is called once
    per SaveChanges that writes a temporal entity, and its ChangeContext is pushed into the
    transaction with set_config('hindsight.<key>', value, true) for the trigger to read back with
    current_setting(..., true). Hindsight opens a transaction for that SaveChanges if the caller has
    none. DbContext.WithReason("…") works the same as in interceptor mode. Bulk writes get history
    rows with NULL context columns.
  • Change context: IChangeContextProvider and UseHindsight(h => h.WithChangeContext<T>()). When a
    provider is registered, HistoryWriter.Interceptor calls it once per SaveChanges and writes the
    returned ChangeContext (UserId, UserName, CorrelationId, Reason, Extra JSON) into the
    changed_by, changed_by_name, correlation_id, reason and extra columns of that call's
    history rows. Without a provider those columns stay NULL and SaveChanges still succeeds.
    DbContext.WithReason("…") opens a scope that overrides the reason for the SaveChanges calls
    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. AsOf must be the first operator on the query; Where, OrderBy, Select,
    First, Any, Count compose after it and translate to a single SQL query with the instant as a
    parameter and the half-open predicate valid_from <= @asOf AND valid_to > @asOf. Results are
    always no-tracking; AsOf(...).AsTracking() throws. AsOf on a non-temporal entity throws
    InvalidOperationException; AsOf with Include / ThenInclude throws NotSupportedException
    (DESIGN.md D8); AsOf on 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 (by valid_from descending, replaceable with your own
    OrderBy). The delete tombstone is excluded, so a deleted entity's timeline ends at the version
    that was open when it was deleted. Same rules as AsOf: first operator on the query, composes into
    a single SQL query, always no-tracking, and the same InvalidOperationException /
    NotSupportedException guards for non-temporal entities, Include, AsTracking, inheritance and
    owned/complex members.
  • db.History<T>(): reads every stored version of a temporal entity as a Version<T> — the entity
    snapshot plus the version's system-time period (ValidFrom / ValidTo as DateTimeOffset,
    IsCurrent for the open one) and the change-context columns (Operation, ChangedBy,
    ChangedByName, CorrelationId, Reason, Extra). Unlike AllVersions() it includes the
    delete tombstone
    as Version with Operation == VersionOperation.Delete and an empty interval,
    so it is the delete audit. Newest first (by valid_from descending, then history_id), replaceable
    with your own OrderBy. Where and Select compose into a single SQL query through both the
    metadata members and the Entity snapshot (Where(v => v.Entity.Id == id)), always no-tracking.
    Same InvalidOperationException / NotSupportedException guards as AsOf / AllVersions for
    non-temporal entities, Include, AsTracking, inheritance and owned/complex members. New public
    Version<TEntity> and VersionOperation types.
  • 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 calling SaveChanges throws InvalidOperationException instead of silently writing the
    stale snapshot back as the current version. Version<T>.Entity is covered too. Copying values onto
    a fresh instance, or onto one from DbSet.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 add drops the column from the main table only; on the history table
    the column stays, forced nullable, and is tagged Hindsight:Orphaned in the model. The convention
    learns which columns to keep by reading the previous ModelSnapshot, so no migration ever emits a
    DropColumn / DropTable / narrowing AlterColumn on a history table. Removing a primary-key
    property from a temporal entity throws InvalidOperationException — 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 add never emits a DropTable for 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 previous ModelSnapshot — columns, key, indexes —
    and tagged Hindsight:Orphaned on 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 throw InvalidOperationException as before; only the table's fate changes. No new public API.
  • Removing IsTemporal() from an entity under HistoryWriter.Trigger now 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 removes IsTemporal() now emits a
    DROP FUNCTION ... CASCADE for it (which also drops the dependent trigger); no data is deleted, only
    the write path stops. No new public API.

Changed

  • HistoryWriter.Interceptor now writes all history rows of a SaveChanges in a single database
    round-trip (a batched DbBatch, chunked at 512 rows) instead of one UPDATE + INSERT round-trip
    per row. Same history rows, same intervals; large batch saves are dramatically faster.