EFCore.ComplexIndexes.PostgreSQL
5.6.0
dotnet add package EFCore.ComplexIndexes.PostgreSQL --version 5.6.0
NuGet\Install-Package EFCore.ComplexIndexes.PostgreSQL -Version 5.6.0
<PackageReference Include="EFCore.ComplexIndexes.PostgreSQL" Version="5.6.0" />
<PackageVersion Include="EFCore.ComplexIndexes.PostgreSQL" Version="5.6.0" />
<PackageReference Include="EFCore.ComplexIndexes.PostgreSQL" />
paket add EFCore.ComplexIndexes.PostgreSQL --version 5.6.0
#r "nuget: EFCore.ComplexIndexes.PostgreSQL, 5.6.0"
#:package EFCore.ComplexIndexes.PostgreSQL@5.6.0
#addin nuget:?package=EFCore.ComplexIndexes.PostgreSQL&version=5.6.0
#tool nuget:?package=EFCore.ComplexIndexes.PostgreSQL&version=5.6.0
EFCore.ComplexIndexes.PostgreSQL
On EF Core 11 or later? This package supports EF Core 10 only. From 5.5.0 it throws when you register it on any other major version. Its successor for EF Core 11 is a new package,
CodoMetis.EFCore.SchemaKit, tracked in #37.
PostgreSQL index and constraint features for EFCore.ComplexIndexes, through Npgsql. This package brings the core package with it.
On top of the core package's complex-property, composite, unique and filtered indexes, you get:
- Index methods. GIN, GiST, BRIN, SP-GiST and Hash, with operator classes, covering (
INCLUDE) indexes, concurrent creation, nulls-distinct control, per-column collation and storage parameters (WITH (fillfactor=70)). NULLS FIRST/NULLS LASTfor each column.- Expression indexes, in raw SQL or as a typed LINQ lambda, on any entity, with complex properties or without.
- JSON indexes. Index members of
ToJson()complex properties with the same expressions Npgsql's queries use. Or index the whole document, or a complex collection, with a GIN index on thejsonbcolumn. - Temporal
UNIQUE … WITHOUT OVERLAPSconstraints and temporal foreign keys (PostgreSQL 18). - Exclusion (
EXCLUDE) constraints, a more general form of uniqueness that also takes aWHEREfilter, on every supported version.
Setup
For migrations, most features need nothing beyond installing the package. Two need one call in
your context's configuration: expression indexes and null ordering. EF Core's index operation has no
place for them, so this package's SQL generator writes their SQL when the migration runs. The same
call also covers EnsureCreated(), as the last row shows.
| Feature | Needs UseNpgsqlComplexIndexes() |
|---|---|
Index methods, operator classes, INCLUDE, concurrent creation, nulls-distinct |
no |
| Temporal constraints and temporal foreign keys | no (since 5.0.2) |
| Exclusion constraints | no |
| Expression indexes (raw SQL, typed LINQ, JSON member) | yes |
DbOrder.NullsFirst / NullsLast |
yes |
EnsureCreated() / GenerateCreateScript() including the declarations |
yes (since 5.1.0) |
services.AddDbContext<AppDbContext>(options =>
options
.UseNpgsql(connectionString)
.UseNpgsqlComplexIndexes());
If you forget the call, the migration fails rather than creating an index that is quietly wrong. These indexes carry a placeholder column named
__requires_UseNpgsqlComplexIndexes__. Npgsql's own generator writes it into theCREATE INDEX, and the migration fails when it runs, with that name in the error message.
Since 5.1.0 the same call also covers Database.EnsureCreated() and GenerateCreateScript(), so
they create your complex indexes and constraints. The pending-model-changes check in Migrate()
then also notices a complex index that no migration has created yet. These use your context's own
services, which the automatic design-time registration never reaches. Without the call,
EnsureCreated() creates the tables and none of the indexes, with no error.
Building your own internal service provider? Register the package on your service collection instead:
var provider = new ServiceCollection()
.AddEntityFrameworkNpgsql()
.AddNpgsqlComplexIndexes()
.BuildServiceProvider();
Usage
Index methods and options
builder.ComplexProperty(x => x.Payload, c =>
c.Property(x => x.Json)
.HasComplexIndex(idx => idx.UseGin().HasOperators("jsonb_path_ops"))
);
The builder has UseGin(), UseGist(), UseBrin(), UseHash(), UseSpGist(),
HasOperators(...), IncludeProperties(...), IsCreatedConcurrently() and AreNullsDistinct(...).
Null ordering
builder.HasComplexCompositeIndex(
x => new { x.Name, Reviewed = DbOrder.NullsLast(DbOrder.Desc(x.ReviewedAt)) });
// CREATE INDEX ... (name, reviewed_at DESC NULLS LAST);
Expression indexes
The package writes raw SQL exactly as you give it. It doesn't map properties to columns and doesn't add quotes:
builder.HasExpressionIndex("lower(email)", isUnique: true, filter: "deleted_at IS NULL");
builder.HasExpressionIndex(idx => idx
.Expression("country")
.Expression("lower(email)").Descending()
.UseGin()
.HasName("ix_person_country_email_ci"));
Or pass a lambda. The package maps its property paths to columns in your finalized model, so it
follows HasColumnName, complex-property columns and ToJson() members:
builder.HasExpressionIndex(x => x.Email.Value.ToLower(), isUnique: true);
// CREATE UNIQUE INDEX ... ON people ((lower("email")));
The lambda can use a small set of operations on purpose: ToLower/ToUpper, the Trim methods
without arguments,
Substring, Replace, string.Length, concatenation, ?? and constants. Anything else throws
NotSupportedException where you declare the index, and the message points you to the raw-SQL
overload. A value object stored through a converter maps through its member: x.Email.Value is the
email column.
JSON member indexes
The members of a complex property mapped with ToJson() have no table columns. You declare their
indexes the same way, and the package indexes an expression that reads the member:
builder.ComplexProperty(x => x.Name, c => c.ToJson("name"));
builder.HasComplexIndex(x => x.Name.ShortName, isUnique: true, indexName: "ux_employer_short_name");
// CREATE UNIQUE INDEX "ux_employer_short_name" ON employers (("name" ->> 'ShortName'));
Each member is read the way Npgsql's queries read it, so those queries use the index. A nested member
is read with #>> '{Address,City}', a typed member is cast to its store type
(CAST("profile" ->> 'Rank' AS integer)), and HasJsonPropertyName is followed.
Date and time members stay text, because PostgreSQL can't index their cast. A unique index on one
still keeps the values unique. A non-unique index that starts with one makes migrations add fail.
If you upgrade from 5.3 or earlier, the first migration re-creates the affected indexes once, under
their existing names.
Temporal constraints — PostgreSQL 18
builder.HasTemporalConstraint(keyColumns: b => b.RoomId, period: b => b.ValidPeriod);
// ALTER TABLE bookings ADD CONSTRAINT ... UNIQUE (room_id, valid_period WITHOUT OVERLAPS);
The period must be a range or multirange column (daterange, tstzrange, NpgsqlRange<T>, …).
Any other type makes migrations add throw. The period stays a plain mapped column, and on purpose
it is not part of an EF key: EF Core doesn't allow range types in keys, since they can't be
compared. For temporal foreign keys, use HasTemporalForeignKey. It needs a matching temporal
constraint on the principal.
Exclusion constraints
An exclusion constraint is a more general kind of uniqueness. Unlike UNIQUE … WITHOUT OVERLAPS, it
takes a WHERE filter, so it is the only way to keep periods from overlapping for some rows:
builder.HasExclusionConstraint(
equalityColumns: x => new { x.GranteeId, x.RoleId },
overlapsColumn: x => x.Period,
filter: "revoked_at IS NULL",
name: "ex_role_grant_active_period");
Two declarations are the same constraint when they have the same elements in the same order and the same filter. So the same columns with different filters give you two constraints side by side, each over its own rows. Name both.
Filters, here and on indexes, can name properties as {Property.Path} placeholders. migrations add
turns them into columns or JSON reads. Every filter: also has a typed form: x => x.RevokedAt == null,
or HasFilter(x => …) on the builders (HasFilter<TEntity> on the non-generic index builders). The
typed form supports a small set: null checks, comparisons, &&/||/!, and the string functions
above. Where you declare it, it throws on an enum comparison, and on a value with no portable SQL
spelling, such as a DateTime or a Guid.
You can read declared constraints back with GetExclusionConstraints() on an entity type or the
model, or FindExclusionConstraint(name). You get the elements, method, filter, deferrability and
name, from the mutable model in OnModelCreating as well as the finalized one. You can amend them
there too: AddExclusionConstraintFilter(predicate, where) adds a predicate with AND to the filter
of every constraint you select, and repeating the call changes nothing.
btree_gist
A scalar element in a gist constraint needs the btree_gist extension, whatever its operator. The
package adds CREATE EXTENSION IF NOT EXISTS btree_gist to the migration for you. To manage it
yourself, call modelBuilder.UseBtreeGist(). To leave it out, call
SuppressTemporalExtensionAutoInjection().
Documentation
- PostgreSQL — indexes — index methods, expression and typed LINQ indexes, JSON member indexes, null ordering
- PostgreSQL — temporal and exclusion constraints
—
WITHOUT OVERLAPS, temporal foreign keys,EXCLUDE,btree_gist - Full documentation
Changelog
This package's changelog, or the root changelog covering all three packages.
MIT licensed.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- EFCore.ComplexIndexes (= 5.6.0)
- Npgsql.EntityFrameworkCore.PostgreSQL (>= 10.0.0 && < 11.0.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 5.6.0 | 58 | 10/23/2026 |
| 5.5.0 | 271 | 10/20/2026 |
| 5.4.1 | 1,759 | 9/22/2026 |
| 5.4.0 | 1,680 | 9/22/2026 |
| 5.3.0 | 274 | 9/5/2026 |
| 5.2.0 | 165 | 9/5/2026 |
| 5.1.0 | 139 | 9/5/2026 |
| 5.0.3 | 415 | 8/15/2026 |
| 5.0.2 | 129 | 8/15/2026 |
| 5.0.1 | 148 | 8/10/2026 |
| 5.0.0 | 121 | 8/10/2026 |
| 4.0.0 | 2,619 | 6/14/2026 |
| 3.1.5 | 180 | 6/5/2026 |
| 3.1.0 | 150 | 6/4/2026 |
| 3.0.0 | 135 | 6/3/2026 |
| 2.0.5 | 388 | 5/14/2026 |
| 2.0.0 | 2,679 | 2/14/2026 |
Migrations keep the indexes and constraints a column change used to drop or rebuild: Changed answers in 5.6.0 (https://github.com/CaffeinatedCoder/EFCore.ComplexIndexes/blob/v5.6.0/docs/upgrading.md#changed-answers-in-560).
Changed answers
- Migrations: on PostgreSQL, changing a generated column's expression, its storage or a tsvector column's configuration drops the complex indexes, exclusion constraints, temporal constraints and temporal foreign keys on the column before the change and adds them after it. The migration dropped them with the column and never added them back (#184 (https://github.com/CaffeinatedCoder/EFCore.ComplexIndexes/issues/184), Upgrading (https://github.com/CaffeinatedCoder/EFCore.ComplexIndexes/blob/v5.6.0/docs/upgrading.md#a-changed-generated-column-keeps-what-reads-it)).
- Migrations: moving a table into the default schema keeps the complex indexes and constraints on it. They were dropped and created again, and on PostgreSQL, where Npgsql leaves such a table in its schema, the migration failed (42P01) (#185 (https://github.com/CaffeinatedCoder/EFCore.ComplexIndexes/issues/185), Upgrading (https://github.com/CaffeinatedCoder/EFCore.ComplexIndexes/blob/v5.6.0/docs/upgrading.md#a-move-into-the-default-schema-keeps-what-is-on-the-table)).
- Migrations: renaming a column keeps the complex indexes, exclusion constraints and temporal constraints that read it, and renames a default name that includes the column. They were dropped and created again around the rename. SQL Server still re-creates an index whose filter reads the column (#182 (https://github.com/CaffeinatedCoder/EFCore.ComplexIndexes/issues/182), Upgrading (https://github.com/CaffeinatedCoder/EFCore.ComplexIndexes/blob/v5.6.0/docs/upgrading.md#a-column-rename-keeps-what-reads-the-column)).
Fixed
- Constraints: on PostgreSQL, a temporal foreign key whose name changes in the same migration as a key column type change or a rebuild of its principal constraint applies. A renamed principal table changes a default name. It failed at apply with 42804 or 2BP01 (#196 (https://github.com/CaffeinatedCoder/EFCore.ComplexIndexes/issues/196), Temporal constraints (https://github.com/CaffeinatedCoder/EFCore.ComplexIndexes/blob/v5.6.0/docs/postgresql-constraints.md#change-remove-or-rename-a-constraint)).
- Constraints: on PostgreSQL, a temporal foreign key to a generic entity type such as Room<int> diffs from its migration snapshot, where every migrations add after the first, and Migrate(), failed because the principal could not be resolved (#197 (https://github.com/CaffeinatedCoder/EFCore.ComplexIndexes/issues/197), Temporal foreign keys (https://github.com/CaffeinatedCoder/EFCore.ComplexIndexes/blob/v5.6.0/docs/postgresql-constraints.md#temporal-foreign-keys-period--requires-postgresql-18)).
- Constraints: on PostgreSQL, a migration that drops a temporal foreign key's dependent table and drops or changes the principal's temporal constraint applies, where it failed at apply with 2BP01 (#187 (https://github.com/CaffeinatedCoder/EFCore.ComplexIndexes/issues/187), Temporal constraints (https://github.com/CaffeinatedCoder/EFCore.ComplexIndexes/blob/v5.6.0/docs/postgresql-constraints.md#change-remove-or-rename-a-constraint)).
- Constraints: on PostgreSQL, changing a temporal foreign key's key column type on both tables applies, where it failed at apply with 42804 when EF Core altered the dependent table first (#188 (https://github.com/CaffeinatedCoder/EFCore.ComplexIndexes/issues/188), Temporal constraints (https://github.com/CaffeinatedCoder/EFCore.ComplexIndexes/blob/v5.6.0/docs/postgresql-constraints.md#change-remove-or-rename-a-constraint)).
Full changelog: https://github.com/CaffeinatedCoder/EFCore.ComplexIndexes/blob/v5.6.0/src/EFCore.ComplexIndexes.PostgreSQL/CHANGELOG.md