EFCore.ComplexIndexes 5.5.0

dotnet add package EFCore.ComplexIndexes --version 5.5.0
                    
NuGet\Install-Package EFCore.ComplexIndexes -Version 5.5.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" Version="5.5.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="EFCore.ComplexIndexes" Version="5.5.0" />
                    
Directory.Packages.props
<PackageReference Include="EFCore.ComplexIndexes" />
                    
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 --version 5.5.0
                    
#r "nuget: EFCore.ComplexIndexes, 5.5.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@5.5.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&version=5.5.0
                    
Install as a Cake Addin
#tool nuget:?package=EFCore.ComplexIndexes&version=5.5.0
                    
Install as a Cake Tool

EFCore.ComplexIndexes

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.

Indexes on complex type properties in EF Core migrations, for models built on value objects.

EF Core lets you map value objects as complex properties, but dotnet ef migrations add writes no index on their columns. With this package you declare those indexes in OnModelCreating, and migrations add writes the CREATE INDEX and DROP INDEX for you.

This is the core package, and it works with any EF Core relational provider. For a provider's own index features, add its package. Each one brings this package with it:

Package Adds
EFCore.ComplexIndexes.PostgreSQL GIN/GiST/BRIN/SP-GiST/Hash methods, operator classes, INCLUDE, NULLS FIRST/LAST, expression & JSON indexes, temporal and exclusion constraints
EFCore.ComplexIndexes.SqlServer Clustered, covering (INCLUDE), online builds, fill factor, sort-in-tempdb, data compression

Setup

None. The package registers itself with EF Core's design-time tools. Install it, declare your indexes in OnModelCreating, and run dotnet ef migrations add.

Usage

Single-column index on a complex property

builder.ComplexProperty(x => x.EmailAddress, c =>
    c.Property(x => x.Value)
     .HasComplexIndex(isUnique: true, filter: "deleted_at IS NULL")
);

The package finds the real column names in your model. It follows both EF Core's naming conventions (Origin_Source) and names you set with HasColumnName.

A filter can name properties instead of columns. migrations add turns filter: "{DeletedAt} IS NULL" into the mapped column, quoted for your provider, and writes the result into the migration. The lambda that picks the column reaches into complex properties. For a value object stored through a converter, it reaches one step further: x => x.Email.Value is the converted column.

Several indexes over one column

builder.HasComplexIndex(x => x.EmailAddress.Value,
    isUnique: true, filter: "deleted_at IS NULL", indexName: "ux_person_email_active");
builder.HasComplexIndex(x => x.EmailAddress.Value,
    indexName: "ix_person_email_all");

A declaration on a property holds one index for that property. To give a column several indexes with different filters, as in the usual soft-delete pattern, declare them on the entity. The lambda can reach into complex properties. Each of these indexes needs its own explicit name, or the declaration throws.

migrations add checks every index name, so the database doesn't reject or quietly shorten one:

  • Unique where the provider needs it: across the database on SQLite, per schema on PostgreSQL, per table on SQL Server.
  • Within the provider's length limit: 63 bytes on PostgreSQL, which would otherwise shorten the name without an error.

On SQLite, some migrations rebuild a table: altering or dropping a column, changing a key or constraint, or renaming the table. A rebuild drops the table's complex indexes, and EF Core's SQL generator runs it after everything else. Call UseComplexIndexes() after UseSqlite(), both where dotnet ef builds the context and where the migration runs, and the indexes are created again after the rebuild. Without the call, dotnet ef migrations add fails and says how to split the change into several migrations (Upgrading).

Composite index across scalar and nested properties

builder.HasComplexCompositeIndex(
    x => new { x.Name, x.EmailAddress.Value },
    isUnique: true);

Per-column sort direction

builder.HasComplexCompositeIndex(
    c => new { c.Created, Counter = DbOrder.Desc(c.Version.Counter) },
    indexName: "ix_commits_created_counter");
// CREATE INDEX ix_commits_created_counter ON ... (created, counter DESC);

Every relational provider writes the direction, because it maps to EF Core's own CreateIndexOperation.IsDescending. A wrapped member is a method call, so C# needs you to name it in the anonymous type.

You can declare DbOrder.NullsFirst/NullsLast here too, but only the PostgreSQL package writes them. On SQL Server, and on providers without their own package such as SQLite, an index that uses them makes dotnet ef migrations add fail.


Reading declarations back

GetComplexIndexes() on an entity type, or on the model, returns every declaration: property-level, entity-level, composite and expression indexes. Each one has its parts, IsUnique, Filter and the Name you gave it, so you can check your own conventions in code. FindComplexIndex(name) finds one by that name. This works on the mutable model inside OnModelCreating too, and migrations add reads declarations through the same code.

var unfiltered = modelBuilder.Model.GetEntityTypes()
    .SelectMany(e => e.GetComplexIndexes())
    .Where(ix => ix.IsUnique && ix.Filter is null);

You can also apply a convention instead of checking it. On the mutable model, AddComplexIndexFilter(predicate, where) adds a predicate with AND to the filter of every declaration you select, and repeating the call changes nothing. AddComplexIndex(definition) adds a declaration, and follows the fluent API's rules for duplicates and names. Both change only what is declared when they run, so call them after the configurations.

Documentation

Full documentation, including every provider's features: https://github.com/CaffeinatedCoder/EFCore.ComplexIndexes

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

Showing the top 4 NuGet packages that depend on EFCore.ComplexIndexes:

Package Downloads
SIL.Harmony

CRDT application library for building offline-first apps with Entity Framework Core.

SIL.Harmony.Linq2db

linq2db integration for the Harmony CRDT library.

EFCore.ComplexIndexes.PostgreSQL

PostgreSQL provider extensions for EFCore.ComplexIndexes — GIN, GiST, BRIN, SP-GiST, and Hash index support for complex type properties.

EFCore.ComplexIndexes.SqlServer

SQL Server provider extensions for EFCore.ComplexIndexes — clustered, covering (INCLUDE), online-built, and fill-factor index options for complex type properties.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
5.5.0 101 10/20/2026
5.4.1 1,932 9/22/2026
5.4.0 1,710 9/22/2026
5.3.0 803 9/5/2026
5.2.0 182 9/5/2026
5.1.0 148 9/5/2026
5.0.3 1,314 8/15/2026
5.0.2 149 8/15/2026
5.0.1 178 8/10/2026
5.0.0 143 8/10/2026
4.0.0 3,153 6/14/2026
3.1.5 10,057 6/5/2026
3.1.0 159 6/4/2026
3.0.0 143 6/3/2026
2.0.5 456 5/14/2026
2.0.2 1,334 2/14/2026
1.0.2 149 2/12/2026
1.0.1 147 2/12/2026
Loading failed

The last 5.x minor with a change of its own: the line refuses EF Core 11, whose successor is CodoMetis.EFCore.SchemaKit.

Changed answers

- Registration: on SQL Server and PostgreSQL, the core differ refuses a complex index carrying that provider's index options and names UseSqlServerComplexIndexes() or UseNpgsqlComplexIndexes(). With UseComplexIndexes(), EnsureCreated() created the index without its property-level options (#92 (https://github.com/CaffeinatedCoder/EFCore.ComplexIndexes/issues/92), Upgrading (https://github.com/CaffeinatedCoder/EFCore.ComplexIndexes/blob/v5.5.0/docs/upgrading.md#the-core-differ-refuses-sql-server-and-postgresql-index-options)).
- Registration: on PostgreSQL, the core differ also refuses exclusion constraints, temporal constraints and temporal foreign keys, and on SQL Server a unique complex index over nullable columns. With UseComplexIndexes(), EnsureCreated() created the tables without those constraints and the index without its IS NOT NULL filter (#150 (https://github.com/CaffeinatedCoder/EFCore.ComplexIndexes/issues/150), Upgrading (https://github.com/CaffeinatedCoder/EFCore.ComplexIndexes/blob/v5.5.0/docs/upgrading.md#the-core-differ-refuses-the-satellites-constraints-and-null-filter)).
- Indexes: a complex index declared on a TPC type is created on the table of every type derived from it, as EF Core's own indexes are. On a concrete base it was created on the base's table only, so a unique index admitted duplicates among the derived types' rows (#93 (https://github.com/CaffeinatedCoder/EFCore.ComplexIndexes/issues/93), Upgrading (https://github.com/CaffeinatedCoder/EFCore.ComplexIndexes/blob/v5.5.0/docs/upgrading.md#declarations-on-a-tpc-type-cover-every-table-of-the-hierarchy)).
- Indexes: a complex index with DbOrder.NullsFirst/NullsLast ordering or an expression part fails at migrations add on SQLite and other providers without a satellite, and under the core differ on SQL Server and PostgreSQL. Where SQLite accepts double-quoted strings, it applied with a constant column in place of its ordering or expression (#134 (https://github.com/CaffeinatedCoder/EFCore.ComplexIndexes/issues/134), #172 (https://github.com/CaffeinatedCoder/EFCore.ComplexIndexes/issues/172), Upgrading (https://github.com/CaffeinatedCoder/EFCore.ComplexIndexes/blob/v5.5.0/docs/upgrading.md#nulls-ordering-and-expression-parts-fail-on-sqlite)).
- Indexes: on PostgreSQL and SQL Server, an IncludeProperties(…) entry that names neither a property nor a column of the table fails at migrations add. When its property was removed, the migration dropped only the column: PostgreSQL dropped the index with it and applied cleanly, SQL Server failed at apply (#142 (https://github.com/CaffeinatedCoder/EFCore.ComplexIndexes/issues/142), Upgrading (https://github.com/CaffeinatedCoder/EFCore.ComplexIndexes/blob/v5.5.0/docs/upgrading.md#include-entries-must-name-a-column)).
- Migrations: on SQLite without UseComplexIndexes(), migrations add refuses a migration that rebuilds a table with a complex index, naming the indexes and the ways out. It scaffolded a migration that applied cleanly and dropped every complex index on the table (#86 (https://github.com/CaffeinatedCoder/EFCore.ComplexIndexes/issues/86), Upgrading (https://github.com/CaffeinatedCoder/EFCore.ComplexIndexes/blob/v5.5.0/docs/upgrading.md#sqlite-refuses-a-table-rebuild-that-would-drop-complex-indexes)).
- Migrations: on SQLite with UseComplexIndexes() after UseSqlite(), a migration that rebuilds a table drops its complex indexes before the rebuild and creates them after it. Applied without that registration, the migration fails before the rebuild (#95 (https://github.com/CaffeinatedCoder/EFCore.ComplexIndexes/issues/95), Upgrading (https://github.com/CaffeinatedCoder/EFCore.ComplexIndexes/blob/v5.5.0/docs/upgrading.md#sqlite-re-creates-complex-indexes-after-a-table-rebuild)).

Breaking

- Registration: UseComplexIndexes(), AddComplexIndexes() and the design-time registration dotnet ef runs throw InvalidOperationException on an EF Core major other than 10, naming CodoMetis.EFCore.SchemaKit for EF Core 11. EF Core 10 on .NET 11 keeps working (Upgrading (https://github.com/CaffeinatedCoder/EFCore.ComplexIndexes/blob/v5.5.0/docs/upgrading.md#ef-core-11-is-refused-at-registration)).
- Indexes: on a provider without a satellite whose index names keep their case, complex index names that differ only in case fail at migrations add; rename one (Upgrading (https://github.com/CaffeinatedCoder/EFCore.ComplexIndexes/blob/v5.5.0/docs/upgrading.md#rename-complex-index-names-that-differ-only-in-case)).
- Indexes: a complex index on a TPC type with derived types fails at migrations add when it reads a member of a complex property, which EF Core 10 maps to no column in a derived table; map the member as a regular property (Upgrading (https://github.com/CaffeinatedCoder/EFCore.ComplexIndexes/blob/v5.5.0/docs/upgrading.md#declarations-on-a-tpc-type-cover-every-table-of-the-hierarchy)).

Added

- CustomMigrationsModelDiffer.RequiresIndexRecreation, a protected hook through which a provider's differ drops an unchanged complex index before a base operation that cannot run while it exists, and creates it after.
- CustomMigrationsModelDiffer.ImplicitFilter, a protected hook through which a provider's differ gives an index declared without a filter the one its EF Core convention would give it.
- CustomMigrationsModelDiffer.IndexNameComparer, a protected hook through which a provider's differ says how it compares index names: without regard to case by default and on SQL Server, ordinally on PostgreSQL.
- CustomMigrationsModelDiffer(…, ICurrentDbContext), a constructor taking the context the migration is diffed for, and ComplexIndexAnnotations.AfterTableRebuild, the mark on a create that runs after SQLite's table rebuild (Upgrading (https://github.com/CaffeinatedCoder/EFCore.ComplexIndexes/blob/v5.5.0/docs/upgrading.md#sqlite-re-creates-complex-indexes-after-a-table-rebuild)).

Changed

- Indexes: a complex index declared on the abstract base of a TPC hierarchy is created on every concrete table, as EF Core's own indexes are, where it failed at migrations add (Upgrading (https://github.com/CaffeinatedCoder/EFCore.ComplexIndexes/blob/v5.5.0/docs/upgrading.md#declarations-on-a-tpc-type-cover-every-table-of-the-hierarchy)).
- Indexes: on SQLite, an explicit name on a complex index of a TPC type stays on that type's table, and each derived table's copy takes the name followed by its table. A copy's name past the identifier limit is shortened as EF Core's are (#145 (https://github.com/CaffeinatedCoder/EFCore.ComplexIndexes/issues/145), #167 (https://github.com/CaffeinatedCoder/EFCore.ComplexIndexes/issues/167), Upgrading (https://github.com/CaffeinatedCoder/EFCore.ComplexIndexes/blob/v5.5.0/docs/upgrading.md#declarations-on-a-tpc-type-cover-every-table-of-the-hierarchy)).
- Packaging: each package carries the version's section of its changelog as its NuGet release notes, which nuget.org's Release Notes tab and Visual Studio's Updates tab show.
- Packaging: the release notes, the README on nuget.org and the GitHub release link the documentation at the version's tag, so they keep describing the version they shipped with after main moves on to the next line.
- Packaging: the PostgreSQL and SQL Server packages depend on exactly their own version of EFCore.ComplexIndexes, and the core differ refuses a satellite of another version, naming both. Update the packages together: a 5.4.x satellite beside core 5.5.0 was refused as the core differ (#169 (https://github.com/CaffeinatedCoder/EFCore.ComplexIndexes/issues/169)).

Fixed

- Indexes: on SQLite, complex index names that differ only in case, from each other or from a native index, fail at migrations add, where they failed at apply because SQLite compares them without regard to case (Index names (https://github.com/CaffeinatedCoder/EFCore.ComplexIndexes/blob/v5.5.0/README.md#single-column-index-on-a-complex-property)).

Full changelog: https://github.com/CaffeinatedCoder/EFCore.ComplexIndexes/blob/v5.5.0/src/EFCore.ComplexIndexes/CHANGELOG.md