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
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="EFCore.ComplexIndexes.PostgreSQL" Version="5.6.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="EFCore.ComplexIndexes.PostgreSQL" Version="5.6.0" />
                    
Directory.Packages.props
<PackageReference Include="EFCore.ComplexIndexes.PostgreSQL" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add EFCore.ComplexIndexes.PostgreSQL --version 5.6.0
                    
#r "nuget: EFCore.ComplexIndexes.PostgreSQL, 5.6.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package EFCore.ComplexIndexes.PostgreSQL@5.6.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=EFCore.ComplexIndexes.PostgreSQL&version=5.6.0
                    
Install as a Cake Addin
#tool nuget:?package=EFCore.ComplexIndexes.PostgreSQL&version=5.6.0
                    
Install as a Cake Tool

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 LAST for 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 the jsonb column.
  • Temporal UNIQUE … WITHOUT OVERLAPS constraints and temporal foreign keys (PostgreSQL 18).
  • Exclusion (EXCLUDE) constraints, a more general form of uniqueness that also takes a WHERE filter, 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 the CREATE 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

Changelog

This package's changelog, or the root changelog covering all three packages.

MIT licensed.

Product 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

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