EFCore.ComplexIndexes
5.5.0
dotnet add package EFCore.ComplexIndexes --version 5.5.0
NuGet\Install-Package EFCore.ComplexIndexes -Version 5.5.0
<PackageReference Include="EFCore.ComplexIndexes" Version="5.5.0" />
<PackageVersion Include="EFCore.ComplexIndexes" Version="5.5.0" />
<PackageReference Include="EFCore.ComplexIndexes" />
paket add EFCore.ComplexIndexes --version 5.5.0
#r "nuget: EFCore.ComplexIndexes, 5.5.0"
#:package EFCore.ComplexIndexes@5.5.0
#addin nuget:?package=EFCore.ComplexIndexes&version=5.5.0
#tool nuget:?package=EFCore.ComplexIndexes&version=5.5.0
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 | 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
- Microsoft.EntityFrameworkCore.Abstractions (>= 10.0.0 && < 11.0.0)
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 |
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