EFCore.ComplexIndexes.PostgreSQL 5.3.0

dotnet add package EFCore.ComplexIndexes.PostgreSQL --version 5.3.0
                    
NuGet\Install-Package EFCore.ComplexIndexes.PostgreSQL -Version 5.3.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.3.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.3.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.3.0
                    
#r "nuget: EFCore.ComplexIndexes.PostgreSQL, 5.3.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.3.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.3.0
                    
Install as a Cake Addin
#tool nuget:?package=EFCore.ComplexIndexes.PostgreSQL&version=5.3.0
                    
Install as a Cake Tool

EFCore.ComplexIndexes.PostgreSQL

PostgreSQL index and constraint features for EFCore.ComplexIndexes, via Npgsql. The core package is included automatically.

Adds, on top of the core's complex-property, composite, unique, and filtered indexes:

  • Index methods — GIN, GiST, BRIN, SP-GiST, Hash — plus operator classes, covering (INCLUDE) indexes, concurrent creation, nulls-distinct control, per-column collation, and storage parameters (WITH (fillfactor=70))
  • NULLS FIRST / NULLS LAST per-column null ordering
  • Expression (functional) indexes — raw SQL or typed LINQ, on any entity, complex or not
  • JSON indexes — index members of ToJson() complex properties as ->> extractions, or the whole document (or a complex collection) with a GIN over the jsonb column
  • Temporal UNIQUE … WITHOUT OVERLAPS constraints and temporal foreign keys (PostgreSQL 18)
  • Exclusion (EXCLUDE) constraints — filtered overlap protection, on every supported version

Setup

Most features need nothing beyond installing the package. Two are rendered when migrations are applied rather than at design time, because they have no slot on EF Core's native index operation, and those need a one-time opt-in:

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());

Forgetting this does not produce a silently wrong index: affected indexes carry a sentinel column named __requires_UseNpgsqlComplexIndexes__, so the stock generator fails loudly with that name in the error message.

Since 5.1.0 the same call also registers the PostgreSQL differ at runtime, so Database.EnsureCreated() and GenerateCreateScript() build the declared indexes and constraints, and the pending-model-changes check in Migrate() sees a complex index that was never scaffolded. Both run the runtime differ, which the design-time wiring never reaches; without the call, EnsureCreated() creates the tables and silently none of the indexes.

Building your own internal service provider? Register the generator and differ directly 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"))
);

UseGin(), UseGist(), UseBrin(), UseHash(), UseSpGist(), HasOperators(...), IncludeProperties(...), IsCreatedConcurrently(), 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

Raw SQL is emitted verbatim — no property-to-column resolution, no automatic quoting:

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 and let property paths resolve against the finalized model, so HasColumnName, complex-property columns, and ToJson() members are honored automatically:

builder.HasExpressionIndex(x => x.Email.Value.ToLower(), isUnique: true);
// CREATE UNIQUE INDEX ... ON people ((lower("email")));

The translated subset is deliberately small — ToLower/ToUpper, Trim variants, Substring, Replace, string.Length, concatenation, ??, constants — and anything else throws NotSupportedException at declaration time, pointing at the raw-SQL overload. A value object mapped through a converter resolves through its member: x.Email.Value is the email column.

JSON member indexes

When a complex property is mapped with ToJson(), its members have no table columns — yet the same index declarations keep working, resolving to extraction expressions instead:

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'));

Nested complex types become -> segments and HasJsonPropertyName is honored.

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>, …); anything else throws at migrations add. It stays a plain mapped column, deliberately not part of an EF key — EF Core forbids non-comparable range types in keys. Temporal foreign keys are available as HasTemporalForeignKey, and require a matching constraint on the principal.

Exclusion constraints

An exclusion constraint generalizes uniqueness, and unlike UNIQUE … WITHOUT OVERLAPS it accepts a WHERE predicate — so a filtered overlap guarantee can only be expressed this way:

builder.HasExclusionConstraint(
    equalityColumns: x => new { x.GranteeId, x.RoleId },
    overlapsColumn:  x => x.Period,
    filter:          "revoked_at IS NULL",
    name:            "ex_role_grant_active_period");

Constraint identity is the ordered elements plus the filter, so the same columns under different predicates give you two coexisting partial constraints (both must be named). Filters — here and on indexes — resolve {Property.Path} placeholders to columns or JSON extractions at migrations add, and every filter: has a typed form: x => x.RevokedAt == null, or HasFilter(x => …) on the builders (HasFilter<TEntity> on the non-generic index builders). The translated subset is small — null checks, comparisons, &&/||/!, the string functions above — and refuses enums and dates at the declaration.

Declared constraints can be read back — GetExclusionConstraints() on an entity type or the model, FindExclusionConstraint(name) — with elements, method, filter, deferrability and name, from the mutable model in OnModelCreating as well as the finalized one; and amended there: AddExclusionConstraintFilter(predicate, where) ANDs a predicate onto every selected constraint's filter, idempotently.

btree_gist

Scalar equality elements under gist need the extension; the differ injects CREATE EXTENSION IF NOT EXISTS btree_gist automatically. Use modelBuilder.UseBtreeGist() for explicit control or SuppressTemporalExtensionAutoInjection() to opt out.


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.3.0 41 9/5/2026
5.2.0 62 9/5/2026
5.1.0 53 9/5/2026
5.0.3 362 8/15/2026
5.0.2 104 8/15/2026
5.0.1 128 8/10/2026
5.0.0 94 8/10/2026
4.0.0 1,881 6/14/2026
3.1.5 160 6/5/2026
3.1.0 136 6/4/2026
3.0.0 121 6/3/2026
2.0.5 370 5/14/2026
2.0.0 2,386 2/14/2026