EFCore.ComplexIndexes
5.6.0
dotnet add package EFCore.ComplexIndexes --version 5.6.0
NuGet\Install-Package EFCore.ComplexIndexes -Version 5.6.0
<PackageReference Include="EFCore.ComplexIndexes" Version="5.6.0" />
<PackageVersion Include="EFCore.ComplexIndexes" Version="5.6.0" />
<PackageReference Include="EFCore.ComplexIndexes" />
paket add EFCore.ComplexIndexes --version 5.6.0
#r "nuget: EFCore.ComplexIndexes, 5.6.0"
#:package EFCore.ComplexIndexes@5.6.0
#addin nuget:?package=EFCore.ComplexIndexes&version=5.6.0
#tool nuget:?package=EFCore.ComplexIndexes&version=5.6.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.6.0 | 74 | 10/23/2026 |
| 5.5.0 | 405 | 10/20/2026 |
| 5.4.1 | 1,988 | 9/22/2026 |
| 5.4.0 | 1,711 | 9/22/2026 |
| 5.3.0 | 1,064 | 9/5/2026 |
| 5.2.0 | 183 | 9/5/2026 |
| 5.1.0 | 151 | 9/5/2026 |
| 5.0.3 | 1,394 | 8/15/2026 |
| 5.0.2 | 149 | 8/15/2026 |
| 5.0.1 | 179 | 8/10/2026 |
| 5.0.0 | 143 | 8/10/2026 |
| 4.0.0 | 3,156 | 6/14/2026 |
| 3.1.5 | 10,122 | 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,395 | 2/14/2026 |
| 1.0.2 | 150 | 2/12/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: 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
- Indexes: on SQL Server, a complex index whose filter or INCLUDE entry names a column in another case, [note] for Note, is dropped before an ALTER COLUMN or rename of that column and created after it, where the migration failed at apply, since SQL Server rejects either while the index exists (Msg 5074 for ALTER COLUMN) (#189 (https://github.com/CaffeinatedCoder/EFCore.ComplexIndexes/issues/189)).
Full changelog: https://github.com/CaffeinatedCoder/EFCore.ComplexIndexes/blob/v5.6.0/src/EFCore.ComplexIndexes/CHANGELOG.md