Repository navigation
Releases: CaffeinatedCoder/EFCore.ComplexIndexes
Release list
v5.4.1
5.4.1
One fix, to the index-name check: it compared names per table on every provider, so on SQLite a name
reused on another table scaffolded cleanly and failed when the migration was applied.
- Fixed: on SQLite, a complex index name is checked across the whole database at
migrations add, not per table. SQLite rejects a secondCREATE INDEXunder a name already used on any other table, and ignores the schemas a model configures, so two tables sharing an index name — two complex indexes, or a complex index and a nativeHasIndex— scaffolded cleanly and failed when applied ("index … already exists"). The scope is now the provider's: per database in the core, which also serves providers without a satellite; per schema on PostgreSQL; per table on SQL Server, where reusing a name across tables stays allowed. Upgrading: a SQLite model that reuses a complex index name across tables now fails atmigrations add, naming both tables; rename one. On a provider without a satellite that scopes names per table, such as MySQL, that rename is one the database would not have needed, and with the differ registered at runtime (UseComplexIndexes())Migrate()raises the same error until it is made. A snapshot holding such a name stays diffable.
Published to nuget.org through Trusted Publishing by this run. The CycloneDX SBOM for each package is attached below.
v5.4.0
5.4.0
Fixes to what 5.x already ships, found while testing the packages against EF Core 11 and planning
the next features: indexes on JSON members that no query could use, and constraint names that only
failed — or quietly vanished — when the migration was applied.
- Fixed: indexes on members of a
ToJson()complex property are written the way Npgsql's queries read the member, so PostgreSQL can use them. It uses an expression index only for a query whose expression matches, and until now every member was extracted as"doc" -> 'A' ->> 'B'text with no cast, which matched only a top-level string. A nested member (#>> '{Address,City}'in the query) or a typed one (CAST("doc" ->> 'Rank' AS integer)) got an index that applied cleanly, enforced uniqueness, and was never used by a single query. Members now render as Npgsql renders them —->>or#>>, cast to the member's store type unless it is a string,decode(…, 'base64')forbyte[],jsonbfor a primitive collection or ajson/jsonbmember — in index parts, typed expression indexes and index filters. That also makes a filter such asx => x.Profile.Rank > 5valid SQL: it compared text with an integer and failed at apply time. Default index names do not change. Upgrading: the firstmigrations adddrops and re-creates each affected index under its existing name; indexes on top-level string members are untouched. Until that migration exists,has-pending-model-changesreports changes andMigrate()raises EF Core's pending-model-changes error. The re-create blocks writes while it builds, so on a large table declare the index withIsCreatedConcurrently()first. Exclusion constraint filters keep their rendering. The snapshot gains oneCustomIndex:RenderingVersionannotation, which records the rules a model was declared under and is what lets the change reach databases built by earlier versions. - Changed: a non-unique index that starts with a date or time JSON member (
DateTime,DateTimeOffset,DateOnly,TimeOnly) is rejected atmigrations add. EF Core's queries cast such a member totimestamptz,dateortime, and PostgreSQL cannot index those casts (the conversion from text is not IMMUTABLE), so no query could ever use the index. A unique one is still allowed and enforces uniqueness on the stored text; a member in a later position leaves the index usable through the parts before it. - Fixed: on PostgreSQL, a name this package introduces is checked against every object it shares a namespace with, at
migrations add. PostgreSQL keeps constraint names unique per table and index names unique per schema — including the index behind every primary key, unique, exclusion and temporal constraint — but each kind was checked against its own kind on one table at most. Two temporal constraints with one name, a temporal foreign key named like a foreign key on its table, or a temporal constraint named like an index anywhere in the schema scaffolded cleanly and failed when applied (42710, 42P07). Against an exclusion constraint it did not even fail: every exclusion constraint is added afterDROP CONSTRAINT IF EXISTS, so a same-named temporal constraint was dropped and the migration applied clean, one declared guarantee short. Two explicitly named complex indexes on different tables of one schema are caught the same way. A collision between two of EF Core's own objects is left to EF; a snapshot holding a collision stays diffable.
Published to nuget.org through Trusted Publishing by this run. The CycloneDX SBOM for each package is attached below.
v5.3.0
5.3.0
One fix, found the first time a 5.2.0 converter-member path met the model snapshot
it had just been scaffolded into.
- Fixed: a property path through a value converter (
x => x.Email.Value) now resolves against a model snapshot as well as against the configured model. A snapshot persists a converted property as its provider type —string, on a property-bag type — and drops the converter, so the member check that guardsx.CreatedAt.Yearhad nothing to check against and the path failed to resolve. The firstdotnet ef migrations addsucceeded, because the snapshot did not hold the path yet; everything that diffed the resulting snapshot then threwCould not resolve property path 'Email.Value'— the nextmigrations add,has-pending-model-changes, andMigrate(), whose pending-model-changes check runs the differ this package registers before applying anything. On a property-bag type the persisted scalar is now accepted for a path the configured model already validated; against a configured model the provider-type check is unchanged. Covers expression templates, column parts, composite parts, filter placeholders and PostgreSQL exclusion elements, at the top level and inside a complex type.
Published to nuget.org through Trusted Publishing by this run. The CycloneDX SBOM for each package is attached below.
v5.2.0
5.2.0
The features AuditOffice's review of its own workarounds asked for, in the order they pay off:
a validation for a failure that reports nothing, a read model so an application can check its
own obligations, filters that resolve property paths the way index parts already do, an amend
API, and typed filter predicates.
-
Changed: an index or constraint name longer than the provider's identifier limit is rejected at
dotnet ef migrations add. PostgreSQL truncates a name past 63 bytes with a NOTICE and applies the migration cleanly, so the index exists under a name that neither the declaration nor a later constraint-violation error reports — a slice dispatching on the constraint name falls through in silence; SQL Server rejects the statement at apply time instead. Explicit and default names alike are checked, measured the way the provider measures them (bytes on PostgreSQL, characters on SQL Server), on the target model only. The names this package derives are never truncated, unlike EF Core's own default names, so a long table name plus a long column path reaches the limit quietly, and a default temporal foreign key name, built from two table names, is the first to. Give the declaration a name; a name-only change renames in place. -
New: a read model.
GetComplexIndexes()on an entity type or the model returns every declaration this package holds — property-level, entity-level, composite and expression indexes alike — asComplexIndexDeclarations carrying the parts as property paths,IsUnique,Filter, the explicitNameand, for entity-level declarations, the provider options;FindComplexIndex(name)looks one up by explicit name. The PostgreSQL package addsGetExclusionConstraints()andFindExclusionConstraint(name)for exclusion constraints, whose element typeExclusionPartDefinitionand annotation keyNpgsqlExclusionAnnotations.Constraintsare now public. Both work on the mutable model insideOnModelCreating, so an application can check a convention such as "every unique index and exclusion constraint on a withdrawable aggregate is filtered to live rows" while the model is built. The differ builds its own descriptors from the same readers, so what the read model reports is what the migration is scaffolded from. -
New: filters resolve
{Property.Path}placeholders, the way expression parts already did — on complex, composite and expression indexes, and on PostgreSQL exclusion constraints.filter: "{RevokedAt} IS NULL"becomes"revoked_at" IS NULL([revoked_at]on SQL Server) atmigrations add, honouringHasColumnNameand, on PostgreSQL,ToJson()members; the resolved text is what the migration carries, rendered by the stock generator with no runtime wiring, and what the snapshot is compared on. Only a brace pair holding a dotted identifier path outside a single-quoted literal is a placeholder, so existing filters with array or JSON literals ('{urgent}','{"a": 1}') are unaffected, and a placeholder naming no property fails loudly. Template resolution now lives in the core differ over a newQuoteIdentifierseam, which is also why the SQL Server satellite resolves them. -
New: property paths see through a value converter. For a value object mapped as one column,
x => x.Email.Value— inHasComplexIndex, a typedHasExpressionIndex, an exclusion element, or a filter placeholder — resolves to that column, provided the member's type is the converter's provider type. Without that guardx.CreatedAt.Yearwould have indexed the whole column; it still fails, and says so. -
New: a mutable API. On an
IMutableEntityType,AddComplexIndexFilter(predicate, where)ANDs a predicate onto the filter of every selected complex index — property-level and entity-level alike — so a shared convention can install a live-rows filter the way it installs the query filter, instead of every configuration repeating it;AddComplexIndex(definition)adds an entity-level declaration with the fluent API's identity and name rules. The PostgreSQL package addsAddExclusionConstraintFilter. An unfiltered declaration gets the predicate, a filtered one(existing) AND (predicate), one that already carries it is left alone, so the calls are safe to repeat. They amend what is declared at the time of the call, so they belong after the configurations. -
New: typed filter predicates on PostgreSQL. Every
filter:string has a lambda form —HasComplexIndex(x => x.Email, x => x.RevokedAt == null),HasComplexCompositeIndex(…, x => …),HasExpressionIndex(…, x => …),HasExclusionConstraint(…, x => …)— and the index and constraint builders takeHasFilter(x => …)(HasFilter<TEntity>on the non-generic index builders). The predicate is translated at the declaration into a filter template, so the column names come from the model:==/!=(IS NULL/IS NOT NULLagainst null),<,<=,>,>=,&&,||,!, boolean properties, and on the operands the string operations and constants a typed expression index accepts. Enums are refused — how one is stored depends on a value conversion the filter cannot see, soStatus == Status.Activewould compare a text column against0and fail at apply time — as are values with no portable SQL spelling (DateTime,Guid), which the typed expression translator used to render as bare text. The amend calls have typed forms too.
Published to nuget.org through Trusted Publishing by this run. The CycloneDX SBOM for each package is attached below.
v5.1.0
5.1.0
Small enhancements around the two seams, plus the silent failures found while planning the next
major: a pending-changes check that never saw this package, and two ways a declaration could vanish
from a migration without a word.
- Fixed:
HasDifferencesnow reports changes to complex indexes, exclusion constraints and temporal constraints. EF Core's base implementation runs its ownDiffrather than theGetDifferencesthis package overrides, so every check built on it reported "no changes" when only a declaration from this package had changed:dotnet ef migrations has-pending-model-changes, the pending-model-changes warningMigrate()raises since EF Core 9, and the snapshot check inmigrations remove. A CI gate built onhas-pending-model-changesmay now fail where it previously passed — that is the gate working. - Fixed: a complex index whose name matches a native
HasIndexon the same table is now rejected atdotnet ef migrations add. The base differ emitted one and this package the other, neither seeing the other, so the migration scaffolded twoCREATE INDEXstatements under one name and failed when applied (42P07). Only the target model's native indexes are consulted, so an index moving between a native declaration and a complex one under the same name still diffs as before. - Fixed: a provider option from the other satellite on a property-level complex index is now rejected at
migrations add, like an entity-level one, instead of being dropped by the forwarding whitelist without a word —.UseGin()on a property-level index diffed by the SQL Server satellite scaffolded a plain B-tree. The PostgreSQL differ likewise rejects SQL Server options, which it previously passed through to a generator that ignored them. - New: runtime registration of the differ.
Database.EnsureCreated(),GenerateCreateScript()and the pending-model-changes checkMigrate()performs run the context's runtime differ, which the design-time wiring never reaches — soEnsureCreated()created the tables and silently none of the complex indexes, andMigrate()never warned about one that was not scaffolded.UseNpgsqlComplexIndexes()now registers the PostgreSQL differ alongside the generator; SQL Server getsUseSqlServerComplexIndexes(); providers without a satellite getUseComplexIndexes()from the core package. Each has anAdd…ComplexIndexescounterpart for a custom internal service provider. With a satellite installed, call the satellite's method only. - New: whole-document JSON indexes on PostgreSQL. Pointing
HasComplexIndexat aToJson()complex property — or at a complex collection, which is always JSON — now indexes itsjsonbcontainer column, soHasComplexIndex(x => x.Payload, ix => ix.UseGin().HasOperators("jsonb_path_ops"))produces the idiomatic GIN index. Previously the path failed with "could not resolve property path", and complex collections could not be indexed at all. The container is a real column, so no runtime wiring is involved; a complex property nested inside the document resolves to a->extraction and renders like any expression index. - New:
HasStorageParameter(name, value)on PostgreSQL complex and expression indexes —WITH (fillfactor=70),WITH (fastupdate=false), and so on. Column indexes render through Npgsql's own generator; expression indexes through this package's, in the same clause position. - New:
UseCollation(params string[])on PostgreSQL complex and expression indexes — per-column index collations ("Name" COLLATE "C"), positional, with an empty entry leaving that column on its default. Independent of the column's own collation, which is never copied onto the index. - New: the property-level
HasComplexIndexoverloads also exist on the non-genericComplexTypePropertyBuilder, so a property configured by name (c.Property("Value")) or by type can carry a complex index. - Changed: a complex index, exclusion constraint or temporal constraint declared on an entity type that is mapped to no table — typically the abstract base of a TPC hierarchy — now fails at
migrations addinstead of producing nothing without a word. Declarations on view-mapped and query-mapped types are still ignored.
Published to nuget.org through Trusted Publishing by this run. The CycloneDX SBOM for each package is attached below.
v5.0.3
What changed in 5.0.3
A packaging and documentation release. No behaviour changes to the differ or the generated SQL.
- Changed: the EF Core dependency now declares an exclusive upper bound —
[10.0.0, 11.0.0)onMicrosoft.EntityFrameworkCore.Abstractionsfor the core package, and on the provider package for each satellite. This package subclassesMigrationsModelDifferand calls internals EF marks as changeable without notice in any release, so an open-ended>= 10.0.0let NuGet resolve a future major where the differ can break — surfacing as a confusingdotnet effailure in your project rather than anywhere visible from here. Nothing changes for existing consumers: NuGet resolves the lowest version in a range, so restore still picks 10.0.0. Adopting EF Core 11 will need a release that lifts the ceiling deliberately, once the differ has been tested against it. - New: the public API is now fully documented, so IntelliSense no longer comes up empty on the fluent API, the annotation keys,
CompositeIndexDefinition, orIndexPartDefinition. The shipped.xmlhad 64 holes in it;TreatWarningsAsErrorsnow keeps it complete. - Tests: a consumer smoke test runs on every PR and on release. It packs the packages, installs them into a throwaway project created outside this repository, and runs a real
dotnet ef migrations add— then asserts on the scaffolded content, because the failure it guards against is a migration that succeeds while silently omitting every index. Nothing previously exercised the delivery chain end to end: NuGet restore, the packaged.targetsinjecting the design-time attribute, EF's host discovering it, and the right differ winning.
v5.0.2
What changed in 5.0.2
A review of the 5.0.1 tree turned up eleven issues. The first three produced migrations that
scaffolded and applied cleanly while being silently wrong; the rest turn late, obscure, or silent
failures into errors raised at the declaration or during dotnet ef migrations add.
- Fixed: the design-time differ is now selected deterministically. A satellite package's
DesignTimeServicesReferenceAttributeis scoped to its provider (ForProvider), and the core registration backs off when a satellite is present — previously, because the core package's attribute rides along transitively and EF resolves last-registration-wins, NuGet's restore order decided which differ ran. A solution referencing two satellites could hand one provider's model to the other provider's differ, silently dropping its index options. - Fixed: temporal
UNIQUE … WITHOUT OVERLAPSconstraints and temporal foreign keys are now rendered at design time, like exclusion constraints, and no longer needUseNpgsqlComplexIndexes(). Previously a consumer without that wiring got a plainUNIQUE (key, period)— valid DDL that applied cleanly and silently dropped the entire non-overlap guarantee. Migrations scaffolded before this change keep working: the SQL generator still renders the old stamped operations. - Fixed: exclusion-constraint identity now includes the filter, so two
EXCLUDEconstraints over the same columns with different predicates coexist (both must be named) instead of the second silently replacing the first — the filtered-overlap case the API exists for. Re-declaring with the same filter still updates in place. - Fixed: duplicate index and exclusion-constraint names are now rejected instead of producing a migration that fails at apply time (42P07) — or, for exclusion constraints, one that applies silently and leaves only the last constraint standing. Reusing an explicit name throws at the declaration; collisions between default names, or between a property-level and an entity-level declaration, throw during
migrations add. - Fixed:
CompositeIndexDefinitionequality compares array-valued provider annotations (operator classes, INCLUDE lists) by content instead of by reference. - Fixed: index, temporal-constraint, and exclusion-constraint selectors that read a captured variable or static member instead of the lambda parameter (
x => captured.Name) now throw at the declaration, naming the offending selector — previously they produced an unmatchable property path that failed much later with an opaque resolution error. - Fixed: provider validation no longer inspects index operations this package did not create. The satellites previously swept every
CreateIndexOperationin the migration, so a plain nativeHasIndexcarrying a provider option outside the satellite's whitelist would have failed the entiremigrations add— harmless with today's providers, but it tied your migrations to the exact index-option set each satellite knows about. - Fixed:
DbOrder.Ascnow marks a column ascending, and combining it withDbOrder.Desc(orNullsFirstwithNullsLast) throws instead of silently picking one. Repeating the same marker is still fine. - Fixed:
Npgsql:IndexSortOrder/IndexNullSortOrderare no longer forwarded onto complex indexes, and setting either now throws with a pointer toDbOrder. They duplicated whatDbOrder.Asc/Desc/NullsFirst/NullsLastalready express per column, giving one index two sources of truth for its sort options — with the annotation's half silently losing whenever the index rendered through this package's generator. - Fixed: clustered-index combinations SQL Server rejects are now caught at
migrations addrather than at apply time: a clustered index withINCLUDEcolumns, a clustered filtered index, two clustered complex indexes on one table, and — the common one — a clustered complex index on a table whose primary key already holds the clustered slot, which is the SQL Server default. - New:
UseDataCompression(DataCompressionType)on SQL Server complex indexes — the annotation was already forwarded but had no way to set it.
v5.0.1
What changed in 5.0.1
- Changed: exclusion-constraint
ADD CONSTRAINTDDL is now preceded byDROP CONSTRAINT IF EXISTS, so adopting a pre-existing hand-written constraint of the same name applies cleanly instead of failing with42P07. The standalone drop path also usesIF EXISTS. - Fixed: renaming a table no longer drops and recreates the exclusion and temporal constraints it carries (the same normalization complex indexes already had).
- Changed: a name-only change to an exclusion constraint, temporal constraint, or temporal foreign key — including the implicit one when a table rename changes a default-derived name — now emits
ALTER TABLE … RENAME CONSTRAINTinstead of dropping and rebuilding. Dependent temporal foreign keys survive such renames untouched. - Tests: the differ is now exercised against real model snapshots — generated as C#, compiled in-memory, and rebuilt exactly as
dotnet ef migrations adddoes — guarding the whole feature set against snapshot round-trip churn.
v5.0.0
What changed in 5.0.0
- Fixed: custom
DROP INDEXoperations are now ordered before the base migration operations. Previously, moving an index between a nativeHasIndexand a complex-index declaration scaffolded a migration that created the new index before dropping the same-named old one — colliding at apply time. - Fixed: descending parts of expression indexes now render
DESC(declarable viaExpressionIndexBuilder.Descending()). - Fixed: integral provider-annotation values (e.g. fill factor) survive snapshot round-trips as
intinstead of degrading todouble, which made generators drop them. - Changed: property annotations are forwarded onto index operations through a provider whitelist instead of a blacklist. Column facets such as
Relational:ColumnNameno longer leak into scaffolded migrations, and the class of phantom drop/create churn caused by snapshot/code-model annotation asymmetries is closed for good. - Changed: an indexed property that resolves to no column now throws at
migrations addinstead of silently dropping the index — unless it is aToJson()member, which now resolves to a JSON expression index (PostgreSQL). - Changed: two indexes over the same columns may now coexist when their filters differ (both must be named); re-declaring with the same filter still updates in place.
- New: entity-level
HasComplexIndex(x => x.Complex.Prop, …)for single-column indexes, enabling multiple filtered indexes per column. - New:
HasExclusionConstraint—EXCLUDEconstraints withWHEREpredicates (see above). - New: typed LINQ expression indexes —
HasExpressionIndex(x => x.Email.ToLower()). - New: JSON member indexes for
ToJson()complex properties. - New:
NULLS FIRST/NULLS LASTviaDbOrder.NullsFirst/NullsLastandExpressionIndexBuilder.NullsFirst()/NullsLast()(PostgreSQL). - New: the EFCore.ComplexIndexes.SqlServer satellite — clustered, covering, online, fill-factor, and sort-in-tempdb options.
- Changed:
IncludeProperties(...)entries are now resolved as property paths (complex members included) with verbatim column-name fallback —IncludeProperties("Email.Value")finds the real column. - Changed: a name-only index change now emits
RenameIndexOperation(PostgreSQL, SQL Server) instead of dropping and rebuilding the index; the core default remains drop + create for providers that cannot rename standalone. - Changed: renaming a table no longer drops and recreates the complex indexes it carries.
- Changed: indexes requiring the custom PostgreSQL generator carry a loud sentinel column, so a missing
UseNpgsqlComplexIndexes()fails at apply time with an actionable error instead of applying a silently wrong index.
v4.0.0
What's New in Version 4.0.0
This release introduces comprehensive support for PostgreSQL 18 Temporal Constraints, allowing you to enforce temporal referential integrity directly at the database level while keeping your EF Core models clean.
-
Temporal UNIQUE Constraints (WITHOUT OVERLAPS)
- Added the
HasTemporalConstraint()builder method to define standalone temporal unique constraints over scalar keys and a period (range) column. - Automatically handles the injection of the required
btree_gistextension during migrations.
- Added the
-
Temporal FOREIGN KEY Constraints (PERIOD)
- Added the
HasTemporalForeignKey()builder method to enforce temporal referential integrity between dependent and principal entities. - Ensures that a dependent's period is fully covered by matching principal periods.
- Supports composite keys and includes built-in validation to ensure matching key counts and correct range type usage.
- Added the
Both features generate proper standalone DDL constraints during EF Core migrations (requires UseNpgsqlComplexIndexes()), bypassing EF's limitation on using non-comparable range types in primary keys.
Full Changelog: v3.1.5...v4.0.0