diff --git a/src/EFCore.Abstractions/IndexAttribute.cs b/src/EFCore.Abstractions/IndexAttribute.cs
index c60caefcb14..d190b1b1b0d 100644
--- a/src/EFCore.Abstractions/IndexAttribute.cs
+++ b/src/EFCore.Abstractions/IndexAttribute.cs
@@ -54,6 +54,12 @@ public bool IsUnique
set => _isUnique = value;
}
+ ///
+ /// Gets a set of values indicating whether each corresponding index column has descending sort order.
+ /// If less sort order values are provided than there are columns, the remaining columns will have ascending order.
+ ///
+ public bool[]? IsDescending { get; set; }
+
///
/// Checks whether has been explicitly set to a value.
///
diff --git a/src/EFCore.Design/Migrations/Design/CSharpMigrationOperationGenerator.cs b/src/EFCore.Design/Migrations/Design/CSharpMigrationOperationGenerator.cs
index 15380528cbb..3907dc3049f 100644
--- a/src/EFCore.Design/Migrations/Design/CSharpMigrationOperationGenerator.cs
+++ b/src/EFCore.Design/Migrations/Design/CSharpMigrationOperationGenerator.cs
@@ -911,6 +911,14 @@ protected virtual void Generate(CreateIndexOperation operation, IndentedStringBu
.Append("unique: true");
}
+ if (operation.IsDescending.Length > 0)
+ {
+ builder
+ .AppendLine(",")
+ .Append("descending: ")
+ .Append(Code.Literal(operation.IsDescending));
+ }
+
if (operation.Filter != null)
{
builder
diff --git a/src/EFCore.Design/Migrations/Design/CSharpSnapshotGenerator.cs b/src/EFCore.Design/Migrations/Design/CSharpSnapshotGenerator.cs
index 981cffe17a9..05a20a1b893 100644
--- a/src/EFCore.Design/Migrations/Design/CSharpSnapshotGenerator.cs
+++ b/src/EFCore.Design/Migrations/Design/CSharpSnapshotGenerator.cs
@@ -634,6 +634,15 @@ protected virtual void GenerateIndex(
.Append(".IsUnique()");
}
+ if (index.IsDescending.Count > 0)
+ {
+ stringBuilder
+ .AppendLine()
+ .Append(".IsDescending(")
+ .Append(string.Join(", ", index.IsDescending.Select(Code.Literal)))
+ .Append(')');
+ }
+
GenerateIndexAnnotations(indexBuilderName, index, stringBuilder);
}
diff --git a/src/EFCore.Design/Scaffolding/Internal/CSharpDbContextGenerator.cs b/src/EFCore.Design/Scaffolding/Internal/CSharpDbContextGenerator.cs
index cd186897fe9..dc410f2d135 100644
--- a/src/EFCore.Design/Scaffolding/Internal/CSharpDbContextGenerator.cs
+++ b/src/EFCore.Design/Scaffolding/Internal/CSharpDbContextGenerator.cs
@@ -567,6 +567,48 @@ private void GenerateIndex(IIndex index)
lines.Add($".{nameof(IndexBuilder.IsUnique)}()");
}
+ // Ascending/descending:
+ // If all ascending (IsDescending.Count == 0), no need to scaffold IsDescending() call (default)
+ // If all descending (IsDescending is all true), scaffold parameterless IsDescending()
+ // Otherwise, scaffold IsDescending() with values up until the last true (unspecified values default to false)
+ if (index.IsDescending.Count > 0)
+ {
+ var descendingBuilder = new StringBuilder()
+ .Append('.')
+ .Append(nameof(IndexBuilder.IsDescending))
+ .Append('(');
+
+ var isAnyAscending = false;
+ var lastDescending = -1;
+ for (var i = 0; i < index.IsDescending.Count; i++)
+ {
+ if (index.IsDescending[i])
+ {
+ lastDescending = i;
+ }
+ else
+ {
+ isAnyAscending = true;
+ }
+ }
+
+ if (isAnyAscending)
+ {
+ for (var i = 0; i <= lastDescending; i++)
+ {
+ if (i > 0)
+ {
+ descendingBuilder.Append(", ");
+ }
+
+ descendingBuilder.Append(_code.Literal(index.IsDescending[i]));
+ }
+ }
+
+ descendingBuilder.Append(')');
+ lines.Add(descendingBuilder.ToString());
+ }
+
GenerateAnnotations(index, annotations, lines);
AppendMultiLineFluentApi(index.DeclaringEntityType, lines);
diff --git a/src/EFCore.Design/Scaffolding/Internal/CSharpEntityTypeGenerator.cs b/src/EFCore.Design/Scaffolding/Internal/CSharpEntityTypeGenerator.cs
index be657806d10..cad7339c887 100644
--- a/src/EFCore.Design/Scaffolding/Internal/CSharpEntityTypeGenerator.cs
+++ b/src/EFCore.Design/Scaffolding/Internal/CSharpEntityTypeGenerator.cs
@@ -212,6 +212,11 @@ private void GenerateIndexAttributes(IEntityType entityType)
indexAttribute.AddParameter($"{nameof(IndexAttribute.IsUnique)} = {_code.Literal(index.IsUnique)}");
}
+ if (index.IsDescending.Count > 0)
+ {
+ indexAttribute.AddParameter($"{nameof(IndexAttribute.IsDescending)} = {_code.UnknownLiteral(index.IsDescending)}");
+ }
+
_sb.AppendLine(indexAttribute.ToString());
}
}
diff --git a/src/EFCore.Design/Scaffolding/Internal/RelationalScaffoldingModelFactory.cs b/src/EFCore.Design/Scaffolding/Internal/RelationalScaffoldingModelFactory.cs
index fc26a105b1d..2d9c154db81 100644
--- a/src/EFCore.Design/Scaffolding/Internal/RelationalScaffoldingModelFactory.cs
+++ b/src/EFCore.Design/Scaffolding/Internal/RelationalScaffoldingModelFactory.cs
@@ -625,7 +625,12 @@ protected virtual EntityTypeBuilder VisitIndexes(EntityTypeBuilder builder, ICol
? builder.HasIndex(propertyNames)
: builder.HasIndex(propertyNames, index.Name);
- indexBuilder = indexBuilder.IsUnique(index.IsUnique);
+ indexBuilder.IsUnique(index.IsUnique);
+
+ if (index.IsDescending.Any(desc => desc))
+ {
+ indexBuilder.IsDescending(index.IsDescending.ToArray());
+ }
if (index.Filter != null)
{
diff --git a/src/EFCore.Relational/Metadata/ITableIndex.cs b/src/EFCore.Relational/Metadata/ITableIndex.cs
index 93608beeadf..3ea33a757cf 100644
--- a/src/EFCore.Relational/Metadata/ITableIndex.cs
+++ b/src/EFCore.Relational/Metadata/ITableIndex.cs
@@ -39,6 +39,12 @@ public interface ITableIndex : IAnnotatable
///
bool IsUnique { get; }
+ ///
+ /// A set of values indicating whether each corresponding index column has descending sort order.
+ /// If less sort order values are provided than there are columns, the remaining columns will have ascending order.
+ ///
+ IReadOnlyList IsDescending { get; }
+
///
/// Gets the expression used as the index filter.
///
@@ -70,8 +76,12 @@ string ToDebugString(MetadataDebugStringOptions options = MetadataDebugStringOpt
builder
.Append(Name)
- .Append(' ')
- .Append(ColumnBase.Format(Columns));
+ .Append(" {")
+ .AppendJoin(
+ ", ",
+ Enumerable.Range(0, Columns.Count)
+ .Select(i => $"'{Columns[i].Name}'{(i < IsDescending.Count && IsDescending[i] ? " Desc" : "")}"))
+ .Append('}');
if (IsUnique)
{
diff --git a/src/EFCore.Relational/Metadata/Internal/RelationalModel.cs b/src/EFCore.Relational/Metadata/Internal/RelationalModel.cs
index 98db63d5249..6f25c2a2f41 100644
--- a/src/EFCore.Relational/Metadata/Internal/RelationalModel.cs
+++ b/src/EFCore.Relational/Metadata/Internal/RelationalModel.cs
@@ -952,7 +952,7 @@ private static void PopulateConstraints(Table table)
continue;
}
- tableIndex = new TableIndex(name, table, columns, index.IsUnique);
+ tableIndex = new TableIndex(name, table, columns, index.IsUnique, index.IsDescending);
table.Indexes.Add(name, tableIndex);
}
diff --git a/src/EFCore.Relational/Metadata/Internal/TableIndex.cs b/src/EFCore.Relational/Metadata/Internal/TableIndex.cs
index a23021c34bb..36d5a083ca2 100644
--- a/src/EFCore.Relational/Metadata/Internal/TableIndex.cs
+++ b/src/EFCore.Relational/Metadata/Internal/TableIndex.cs
@@ -21,12 +21,14 @@ public TableIndex(
string name,
Table table,
IReadOnlyList columns,
- bool unique)
+ bool unique,
+ IReadOnlyList isDescending)
{
Name = name;
Table = table;
Columns = columns;
IsUnique = unique;
+ IsDescending = isDescending;
}
///
@@ -68,6 +70,9 @@ public override bool IsReadOnly
///
public virtual bool IsUnique { get; }
+ ///
+ public virtual IReadOnlyList IsDescending { get; }
+
///
public virtual string? Filter
=> MappedIndexes.First().GetFilter(StoreObjectIdentifier.Table(Table.Name, Table.Schema));
diff --git a/src/EFCore.Relational/Migrations/Internal/MigrationsModelDiffer.cs b/src/EFCore.Relational/Migrations/Internal/MigrationsModelDiffer.cs
index 5fae40d6d3f..7e0e0bea530 100644
--- a/src/EFCore.Relational/Migrations/Internal/MigrationsModelDiffer.cs
+++ b/src/EFCore.Relational/Migrations/Internal/MigrationsModelDiffer.cs
@@ -1393,6 +1393,7 @@ protected virtual IEnumerable Diff(
private bool IndexStructureEquals(ITableIndex source, ITableIndex target, DiffContext diffContext)
=> source.IsUnique == target.IsUnique
+ && source.IsDescending.SequenceEqual(target.IsDescending)
&& source.Filter == target.Filter
&& !HasDifferences(source.GetAnnotations(), target.GetAnnotations())
&& source.Columns.Select(p => p.Name).SequenceEqual(
diff --git a/src/EFCore.Relational/Migrations/MigrationBuilder.cs b/src/EFCore.Relational/Migrations/MigrationBuilder.cs
index fb6fb2f1b8f..5b6c22b14ce 100644
--- a/src/EFCore.Relational/Migrations/MigrationBuilder.cs
+++ b/src/EFCore.Relational/Migrations/MigrationBuilder.cs
@@ -601,6 +601,11 @@ public virtual AlterOperationBuilder AlterTable(
/// The schema that contains the table, or to use the default schema.
/// Indicates whether or not the index enforces uniqueness.
/// The filter to apply to the index, or for no filter.
+ ///
+ /// A set of values indicating whether each corresponding index column has descending sort order.
+ /// If less sort order values are provided than there are columns, the remaining columns will have ascending order.
+ /// If , all columns will have ascending order.
+ ///
/// A builder to allow annotations to be added to the operation.
public virtual OperationBuilder CreateIndex(
string name,
@@ -608,14 +613,16 @@ public virtual OperationBuilder CreateIndex(
string column,
string? schema = null,
bool unique = false,
- string? filter = null)
+ string? filter = null,
+ bool[]? descending = null)
=> CreateIndex(
name,
table,
new[] { Check.NotEmpty(column, nameof(column)) },
schema,
unique,
- filter);
+ filter,
+ descending);
///
/// Builds a to create a new composite (multi-column) index.
@@ -629,6 +636,11 @@ public virtual OperationBuilder CreateIndex(
/// The schema that contains the table, or to use the default schema.
/// Indicates whether or not the index enforces uniqueness.
/// The filter to apply to the index, or for no filter.
+ ///
+ /// A set of values indicating whether each corresponding index column has descending sort order.
+ /// If less sort order values are provided than there are columns, the remaining columns will have ascending order.
+ /// If , all columns will have ascending order.
+ ///
/// A builder to allow annotations to be added to the operation.
public virtual OperationBuilder CreateIndex(
string name,
@@ -636,7 +648,8 @@ public virtual OperationBuilder CreateIndex(
string[] columns,
string? schema = null,
bool unique = false,
- string? filter = null)
+ string? filter = null,
+ bool[]? descending = null)
{
Check.NotEmpty(name, nameof(name));
Check.NotEmpty(table, nameof(table));
@@ -651,6 +664,12 @@ public virtual OperationBuilder CreateIndex(
IsUnique = unique,
Filter = filter
};
+
+ if (descending is not null)
+ {
+ operation.IsDescending = descending;
+ }
+
Operations.Add(operation);
return new OperationBuilder(operation);
diff --git a/src/EFCore.Relational/Migrations/MigrationsSqlGenerator.cs b/src/EFCore.Relational/Migrations/MigrationsSqlGenerator.cs
index f08c9c69118..5528830b693 100644
--- a/src/EFCore.Relational/Migrations/MigrationsSqlGenerator.cs
+++ b/src/EFCore.Relational/Migrations/MigrationsSqlGenerator.cs
@@ -428,9 +428,11 @@ protected virtual void Generate(
.Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Name))
.Append(" ON ")
.Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Table, operation.Schema))
- .Append(" (")
- .Append(ColumnList(operation.Columns))
- .Append(")");
+ .Append(" (");
+
+ GenerateIndexColumnList(operation, model, builder);
+
+ builder.Append(")");
IndexOptions(operation, model, builder);
@@ -1659,23 +1661,41 @@ protected virtual void CheckConstraint(
/// The operation.
/// The target model which may be if the operations exist without a model.
/// The command builder to use to add the SQL fragment.
- protected virtual void IndexTraits(
- MigrationOperation operation,
- IModel? model,
- MigrationCommandListBuilder builder)
+ protected virtual void IndexTraits(MigrationOperation operation, IModel? model, MigrationCommandListBuilder builder)
{
}
+ ///
+ /// Returns a SQL fragment for the column list of an index from a .
+ ///
+ /// The operation.
+ /// The target model which may be if the operations exist without a model.
+ /// The command builder to use to add the SQL fragment.
+ protected virtual void GenerateIndexColumnList(CreateIndexOperation operation, IModel? model, MigrationCommandListBuilder builder)
+ {
+ for (var i = 0; i < operation.Columns.Length; i++)
+ {
+ if (i > 0)
+ {
+ builder.Append(", ");
+ }
+
+ builder.Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Columns[i]));
+
+ if (operation.IsDescending is not null && i < operation.IsDescending.Length && operation.IsDescending[i])
+ {
+ builder.Append(" DESC");
+ }
+ }
+ }
+
///
/// Generates a SQL fragment for extras (filter, included columns, options) of an index from a .
///
/// The operation.
/// The target model which may be if the operations exist without a model.
/// The command builder to use to add the SQL fragment.
- protected virtual void IndexOptions(
- CreateIndexOperation operation,
- IModel? model,
- MigrationCommandListBuilder builder)
+ protected virtual void IndexOptions(CreateIndexOperation operation, IModel? model, MigrationCommandListBuilder builder)
{
if (!string.IsNullOrEmpty(operation.Filter))
{
diff --git a/src/EFCore.Relational/Migrations/Operations/CreateIndexOperation.cs b/src/EFCore.Relational/Migrations/Operations/CreateIndexOperation.cs
index 4b136221509..1b427aaae64 100644
--- a/src/EFCore.Relational/Migrations/Operations/CreateIndexOperation.cs
+++ b/src/EFCore.Relational/Migrations/Operations/CreateIndexOperation.cs
@@ -12,11 +12,6 @@ namespace Microsoft.EntityFrameworkCore.Migrations.Operations;
[DebuggerDisplay("CREATE INDEX {Name} ON {Table}")]
public class CreateIndexOperation : MigrationOperation, ITableMigrationOperation
{
- ///
- /// Indicates whether or not the index should enforce uniqueness.
- ///
- public virtual bool IsUnique { get; set; }
-
///
/// The name of the index.
///
@@ -37,6 +32,17 @@ public class CreateIndexOperation : MigrationOperation, ITableMigrationOperation
///
public virtual string[] Columns { get; set; } = null!;
+ ///
+ /// Indicates whether or not the index should enforce uniqueness.
+ ///
+ public virtual bool IsUnique { get; set; }
+
+ ///
+ /// A set of values indicating whether each corresponding index column has descending sort order.
+ /// If less sort order values are provided than there are columns, the remaining columns will have ascending order.
+ ///
+ public virtual bool[] IsDescending { get; set; } = Array.Empty();
+
///
/// An expression to use as the index filter.
///
@@ -53,11 +59,12 @@ public static CreateIndexOperation CreateFrom(ITableIndex index)
var operation = new CreateIndexOperation
{
- IsUnique = index.IsUnique,
Name = index.Name,
Schema = index.Table.Schema,
Table = index.Table.Name,
Columns = index.Columns.Select(p => p.Name).ToArray(),
+ IsUnique = index.IsUnique,
+ IsDescending = index.IsDescending.ToArray(),
Filter = index.Filter
};
operation.AddAnnotations(index.GetAnnotations());
diff --git a/src/EFCore.Relational/Scaffolding/Metadata/DatabaseIndex.cs b/src/EFCore.Relational/Scaffolding/Metadata/DatabaseIndex.cs
index 92de823af64..50102790d7d 100644
--- a/src/EFCore.Relational/Scaffolding/Metadata/DatabaseIndex.cs
+++ b/src/EFCore.Relational/Scaffolding/Metadata/DatabaseIndex.cs
@@ -28,10 +28,16 @@ public class DatabaseIndex : Annotatable
public virtual IList Columns { get; } = new List();
///
- /// Indicates whether or not the index constrains uniqueness.
+ /// Indicates whether or not the index enforces uniqueness.
///
public virtual bool IsUnique { get; set; }
+ ///
+ /// A set of values indicating whether each corresponding index column has descending sort order.
+ /// If less sort order values are provided than there are columns, the remaining columns will have ascending order.
+ ///
+ public virtual IList IsDescending { get; set; } = new List();
+
///
/// The filter expression, or if the index has no filter.
///
diff --git a/src/EFCore.SqlServer/Migrations/SqlServerMigrationsSqlGenerator.cs b/src/EFCore.SqlServer/Migrations/SqlServerMigrationsSqlGenerator.cs
index 255e4fdc683..878e053f7cf 100644
--- a/src/EFCore.SqlServer/Migrations/SqlServerMigrationsSqlGenerator.cs
+++ b/src/EFCore.SqlServer/Migrations/SqlServerMigrationsSqlGenerator.cs
@@ -748,10 +748,9 @@ protected override void Generate(
IndexTraits(operation, model, builder);
- builder
- .Append("(")
- .Append(ColumnList(operation.Columns))
- .Append(")");
+ builder.Append("(");
+ GenerateIndexColumnList(operation, model, builder);
+ builder.Append(")");
}
else
{
diff --git a/src/EFCore.SqlServer/Scaffolding/Internal/SqlServerDatabaseModelFactory.cs b/src/EFCore.SqlServer/Scaffolding/Internal/SqlServerDatabaseModelFactory.cs
index 0c7d9b78c32..71c4e39fb73 100644
--- a/src/EFCore.SqlServer/Scaffolding/Internal/SqlServerDatabaseModelFactory.cs
+++ b/src/EFCore.SqlServer/Scaffolding/Internal/SqlServerDatabaseModelFactory.cs
@@ -932,6 +932,7 @@ private void GetIndexes(DbConnection connection, IReadOnlyList ta
[i].[filter_definition],
[i].[fill_factor],
COL_NAME([ic].[object_id], [ic].[column_id]) AS [column_name],
+ [ic].[is_descending_key],
[ic].[is_included_column]
FROM [sys].[indexes] AS [i]
JOIN [sys].[tables] AS [t] ON [i].[object_id] = [t].[object_id]
@@ -1137,6 +1138,8 @@ bool TryGetIndex(
return false;
}
+ index.IsDescending.Add(dataRecord.GetValueOrDefault("is_descending_key"));
+
index.Columns.Add(column);
}
diff --git a/src/EFCore.Sqlite.Core/Scaffolding/Internal/SqliteDatabaseModelFactory.cs b/src/EFCore.Sqlite.Core/Scaffolding/Internal/SqliteDatabaseModelFactory.cs
index fb527ae7c37..b2a50da6bbf 100644
--- a/src/EFCore.Sqlite.Core/Scaffolding/Internal/SqliteDatabaseModelFactory.cs
+++ b/src/EFCore.Sqlite.Core/Scaffolding/Internal/SqliteDatabaseModelFactory.cs
@@ -458,8 +458,9 @@ private void GetIndexes(DbConnection connection, DatabaseTable table)
using (var command2 = connection.CreateCommand())
{
command2.CommandText = new StringBuilder()
- .AppendLine("SELECT \"name\"")
- .AppendLine("FROM pragma_index_info(@index)")
+ .AppendLine("SELECT \"name\", \"desc\"")
+ .AppendLine("FROM pragma_index_xinfo(@index)")
+ .AppendLine("WHERE key = 1")
.AppendLine("ORDER BY \"seqno\";")
.ToString();
@@ -477,6 +478,7 @@ private void GetIndexes(DbConnection connection, DatabaseTable table)
Check.DebugAssert(column != null, "column is null.");
index.Columns.Add(column);
+ index.IsDescending.Add(reader2.GetBoolean(1));
}
}
diff --git a/src/EFCore/Metadata/Builders/IConventionIndexBuilder.cs b/src/EFCore/Metadata/Builders/IConventionIndexBuilder.cs
index be420be010c..7253c79c301 100644
--- a/src/EFCore/Metadata/Builders/IConventionIndexBuilder.cs
+++ b/src/EFCore/Metadata/Builders/IConventionIndexBuilder.cs
@@ -27,18 +27,36 @@ public interface IConventionIndexBuilder : IConventionAnnotatableBuilder
///
/// A value indicating whether the index is unique.
/// Indicates whether the configuration was specified using a data annotation.
- ///
- /// The same builder instance if the uniqueness was configured,
- /// otherwise.
- ///
+ /// The same builder instance if the uniqueness was configured, otherwise.
IConventionIndexBuilder? IsUnique(bool? unique, bool fromDataAnnotation = false);
///
- /// Returns a value indicating whether this index uniqueness can be configured
- /// from the current configuration source.
+ /// Returns a value indicating whether this index uniqueness can be configured from the current configuration source.
///
/// A value indicating whether the index is unique.
/// Indicates whether the configuration was specified using a data annotation.
/// if the index uniqueness can be configured.
bool CanSetIsUnique(bool? unique, bool fromDataAnnotation = false);
+
+ ///
+ /// Configures the sort order(s) for the columns of this index (ascending or descending).
+ ///
+ ///
+ /// A set of values indicating whether each corresponding index column has descending sort order.
+ /// If less sort order values are provided than there are columns, the remaining columns will have ascending order.
+ ///
+ /// Indicates whether the configuration was specified using a data annotation.
+ /// The same builder instance if the uniqueness was configured, otherwise.
+ IConventionIndexBuilder? IsDescending(IReadOnlyList? descending, bool fromDataAnnotation = false);
+
+ ///
+ /// Returns a value indicating whether this index sort order can be configured from the current configuration source.
+ ///
+ ///
+ /// A set of values indicating whether each corresponding index column has descending sort order.
+ /// If less sort order values are provided than there are columns, the remaining columns will have ascending order.
+ ///
+ /// Indicates whether the configuration was specified using a data annotation.
+ /// if the index uniqueness can be configured.
+ bool CanSetIsDescending(IReadOnlyList? descending, bool fromDataAnnotation = false);
}
diff --git a/src/EFCore/Metadata/Builders/IndexBuilder.cs b/src/EFCore/Metadata/Builders/IndexBuilder.cs
index 9f3f1e899cc..39105d70492 100644
--- a/src/EFCore/Metadata/Builders/IndexBuilder.cs
+++ b/src/EFCore/Metadata/Builders/IndexBuilder.cs
@@ -77,6 +77,28 @@ public virtual IndexBuilder IsUnique(bool unique = true)
return this;
}
+ ///
+ /// Configures the sort order(s) for the columns of this index (ascending or descending).
+ ///
+ ///
+ /// If empty, all index columns have descending sort order. Otherwise, each value determines whether the corresponding index
+ /// column has descending sort order. If less sort order values are provided than there are columns, the remaining columns will have
+ /// ascending order.
+ ///
+ /// The same builder instance so that multiple configuration calls can be chained.
+ public virtual IndexBuilder IsDescending(params bool[] descending)
+ {
+ if (descending.Length == 0)
+ {
+ descending = new bool[Builder.Metadata.Properties.Count];
+ Array.Fill(descending, true);
+ }
+
+ Builder.IsDescending(descending, ConfigurationSource.Explicit);
+
+ return this;
+ }
+
#region Hidden System.Object members
///
diff --git a/src/EFCore/Metadata/Builders/IndexBuilder`.cs b/src/EFCore/Metadata/Builders/IndexBuilder`.cs
index 8bfc61e74b1..ed66c21ba61 100644
--- a/src/EFCore/Metadata/Builders/IndexBuilder`.cs
+++ b/src/EFCore/Metadata/Builders/IndexBuilder`.cs
@@ -49,4 +49,16 @@ public IndexBuilder(IMutableIndex index)
/// The same builder instance so that multiple configuration calls can be chained.
public new virtual IndexBuilder IsUnique(bool unique = true)
=> (IndexBuilder)base.IsUnique(unique);
+
+ ///
+ /// Configures the sort order(s) for the columns of this index (ascending or descending).
+ ///
+ ///
+ /// If empty, all index columns have descending sort order. Otherwise, each value determines whether the corresponding index
+ /// column has descending sort order. If less sort order values are provided than there are columns, the remaining columns will have
+ /// ascending order.
+ ///
+ /// The same builder instance so that multiple configuration calls can be chained.
+ public new virtual IndexBuilder IsDescending(params bool[] descending)
+ => (IndexBuilder)base.IsDescending(descending);
}
diff --git a/src/EFCore/Metadata/Conventions/ConventionSet.cs b/src/EFCore/Metadata/Conventions/ConventionSet.cs
index 3d168974fb8..54cc3869801 100644
--- a/src/EFCore/Metadata/Conventions/ConventionSet.cs
+++ b/src/EFCore/Metadata/Conventions/ConventionSet.cs
@@ -207,6 +207,12 @@ public class ConventionSet
public virtual IList IndexUniquenessChangedConventions { get; }
= new List();
+ ///
+ /// Conventions to run when the sort order of an index is changed.
+ ///
+ public virtual IList IndexSortOrderChangedConventions { get; }
+ = new List();
+
///
/// Conventions to run when an annotation is changed on an index.
///
diff --git a/src/EFCore/Metadata/Conventions/IIndexSortOrderChangedConvention.cs b/src/EFCore/Metadata/Conventions/IIndexSortOrderChangedConvention.cs
new file mode 100644
index 00000000000..b537fc71c71
--- /dev/null
+++ b/src/EFCore/Metadata/Conventions/IIndexSortOrderChangedConvention.cs
@@ -0,0 +1,22 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace Microsoft.EntityFrameworkCore.Metadata.Conventions;
+
+///
+/// Represents an operation that should be performed when the sort order of an index is changed.
+///
+///
+/// See Model building conventions for more information and examples.
+///
+public interface IIndexSortOrderChangedConvention : IConvention
+{
+ ///
+ /// Called after the uniqueness for an index is changed.
+ ///
+ /// The builder for the index.
+ /// Additional information associated with convention execution.
+ void ProcessIndexSortOrderChanged(
+ IConventionIndexBuilder indexBuilder,
+ IConventionContext?> context);
+}
diff --git a/src/EFCore/Metadata/Conventions/IndexAttributeConvention.cs b/src/EFCore/Metadata/Conventions/IndexAttributeConvention.cs
index 0f8f1cef2f6..3f08caf32a3 100644
--- a/src/EFCore/Metadata/Conventions/IndexAttributeConvention.cs
+++ b/src/EFCore/Metadata/Conventions/IndexAttributeConvention.cs
@@ -110,9 +110,17 @@ private static void CheckIndexAttributesAndEnsureIndex(
{
CheckIgnoredProperties(indexAttribute, entityType);
}
- else if (indexAttribute.IsUniqueHasValue)
+ else
{
- indexBuilder.IsUnique(indexAttribute.IsUnique, fromDataAnnotation: true);
+ if (indexAttribute.IsUniqueHasValue)
+ {
+ indexBuilder.IsUnique(indexAttribute.IsUnique, fromDataAnnotation: true);
+ }
+
+ if (indexAttribute.IsDescending is not null)
+ {
+ indexBuilder.IsDescending(indexAttribute.IsDescending, fromDataAnnotation: true);
+ }
}
}
}
diff --git a/src/EFCore/Metadata/Conventions/Internal/ConventionDispatcher.ConventionScope.cs b/src/EFCore/Metadata/Conventions/Internal/ConventionDispatcher.ConventionScope.cs
index 252512a3fea..6ea81d79fec 100644
--- a/src/EFCore/Metadata/Conventions/Internal/ConventionDispatcher.ConventionScope.cs
+++ b/src/EFCore/Metadata/Conventions/Internal/ConventionDispatcher.ConventionScope.cs
@@ -127,6 +127,8 @@ public int GetLeafCount()
IConventionIndex index);
public abstract bool? OnIndexUniquenessChanged(IConventionIndexBuilder indexBuilder);
+ public abstract IReadOnlyList? OnIndexSortOrderChanged(IConventionIndexBuilder indexBuilder);
+
public abstract IConventionKeyBuilder? OnKeyAdded(IConventionKeyBuilder keyBuilder);
public abstract IConventionAnnotation? OnKeyAnnotationChanged(
diff --git a/src/EFCore/Metadata/Conventions/Internal/ConventionDispatcher.DelayedConventionScope.cs b/src/EFCore/Metadata/Conventions/Internal/ConventionDispatcher.DelayedConventionScope.cs
index e7945a2f2f5..8e471dba230 100644
--- a/src/EFCore/Metadata/Conventions/Internal/ConventionDispatcher.DelayedConventionScope.cs
+++ b/src/EFCore/Metadata/Conventions/Internal/ConventionDispatcher.DelayedConventionScope.cs
@@ -183,6 +183,12 @@ public override IConventionIndex OnIndexRemoved(
return indexBuilder.Metadata.IsUnique;
}
+ public override IReadOnlyList? OnIndexSortOrderChanged(IConventionIndexBuilder indexBuilder)
+ {
+ Add(new OnIndexSortOrderChangedNode(indexBuilder));
+ return indexBuilder.Metadata.IsDescending;
+ }
+
public override IConventionAnnotation? OnIndexAnnotationChanged(
IConventionIndexBuilder indexBuilder,
string name,
@@ -901,6 +907,19 @@ public override void Run(ConventionDispatcher dispatcher)
=> dispatcher._immediateConventionScope.OnIndexUniquenessChanged(IndexBuilder);
}
+ private sealed class OnIndexSortOrderChangedNode : ConventionNode
+ {
+ public OnIndexSortOrderChangedNode(IConventionIndexBuilder indexBuilder)
+ {
+ IndexBuilder = indexBuilder;
+ }
+
+ public IConventionIndexBuilder IndexBuilder { get; }
+
+ public override void Run(ConventionDispatcher dispatcher)
+ => dispatcher._immediateConventionScope.OnIndexSortOrderChanged(IndexBuilder);
+ }
+
private sealed class OnIndexAnnotationChangedNode : ConventionNode
{
public OnIndexAnnotationChangedNode(
diff --git a/src/EFCore/Metadata/Conventions/Internal/ConventionDispatcher.ImmediateConventionScope.cs b/src/EFCore/Metadata/Conventions/Internal/ConventionDispatcher.ImmediateConventionScope.cs
index 846d88247b1..0312d7ec5db 100644
--- a/src/EFCore/Metadata/Conventions/Internal/ConventionDispatcher.ImmediateConventionScope.cs
+++ b/src/EFCore/Metadata/Conventions/Internal/ConventionDispatcher.ImmediateConventionScope.cs
@@ -29,6 +29,7 @@ private sealed class ImmediateConventionScope : ConventionScope
private readonly ConventionContext _stringConventionContext;
private readonly ConventionContext _fieldInfoConventionContext;
private readonly ConventionContext _boolConventionContext;
+ private readonly ConventionContext?> _boolListConventionContext;
public ImmediateConventionScope(ConventionSet conventionSet, ConventionDispatcher dispatcher)
{
@@ -54,6 +55,7 @@ public ImmediateConventionScope(ConventionSet conventionSet, ConventionDispatche
_stringConventionContext = new ConventionContext(dispatcher);
_fieldInfoConventionContext = new ConventionContext(dispatcher);
_boolConventionContext = new ConventionContext(dispatcher);
+ _boolListConventionContext = new ConventionContext?>(dispatcher);
}
public override void Run(ConventionDispatcher dispatcher)
@@ -969,6 +971,29 @@ public IConventionModelBuilder OnModelInitialized(IConventionModelBuilder modelB
return !indexBuilder.Metadata.IsInModel ? null : _boolConventionContext.Result;
}
+ public override IReadOnlyList? OnIndexSortOrderChanged(IConventionIndexBuilder indexBuilder)
+ {
+ using (_dispatcher.DelayConventions())
+ {
+ _boolListConventionContext.ResetState(indexBuilder.Metadata.IsDescending);
+ foreach (var indexConvention in _conventionSet.IndexSortOrderChangedConventions)
+ {
+ if (!indexBuilder.Metadata.IsInModel)
+ {
+ return null;
+ }
+
+ indexConvention.ProcessIndexSortOrderChanged(indexBuilder, _boolListConventionContext);
+ if (_boolConventionContext.ShouldStopProcessing())
+ {
+ return _boolListConventionContext.Result;
+ }
+ }
+ }
+
+ return !indexBuilder.Metadata.IsInModel ? null : _boolListConventionContext.Result;
+ }
+
public override IConventionAnnotation? OnIndexAnnotationChanged(
IConventionIndexBuilder indexBuilder,
string name,
diff --git a/src/EFCore/Metadata/Conventions/Internal/ConventionDispatcher.cs b/src/EFCore/Metadata/Conventions/Internal/ConventionDispatcher.cs
index 20f24d61469..69ef6bd3cd1 100644
--- a/src/EFCore/Metadata/Conventions/Internal/ConventionDispatcher.cs
+++ b/src/EFCore/Metadata/Conventions/Internal/ConventionDispatcher.cs
@@ -483,6 +483,15 @@ public virtual IConventionModelBuilder OnModelFinalizing(IConventionModelBuilder
public virtual bool? OnIndexUniquenessChanged(IConventionIndexBuilder indexBuilder)
=> _scope.OnIndexUniquenessChanged(indexBuilder);
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ public virtual IReadOnlyList? OnIndexSortOrderChanged(IConventionIndexBuilder indexBuilder)
+ => _scope.OnIndexSortOrderChanged(indexBuilder);
+
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
diff --git a/src/EFCore/Metadata/IConventionIndex.cs b/src/EFCore/Metadata/IConventionIndex.cs
index ee2cfcdc9a2..fdf7031aa6a 100644
--- a/src/EFCore/Metadata/IConventionIndex.cs
+++ b/src/EFCore/Metadata/IConventionIndex.cs
@@ -49,6 +49,17 @@ public interface IConventionIndex : IReadOnlyIndex, IConventionAnnotatable
/// The configured uniqueness.
bool? SetIsUnique(bool? unique, bool fromDataAnnotation = false);
+ ///
+ /// Sets the sort order(s) for this index (ascending or descending).
+ ///
+ ///
+ /// A set of values indicating whether each corresponding index column has descending sort order.
+ /// If less sort order values are provided than there are columns, the remaining columns will have ascending order.
+ ///
+ /// Indicates whether the configuration was specified using a data annotation.
+ /// The configured sort order(s).
+ IReadOnlyList? SetIsDescending(bool[]? descending, bool fromDataAnnotation = false);
+
///
/// Returns the configuration source for .
///
diff --git a/src/EFCore/Metadata/IMutableIndex.cs b/src/EFCore/Metadata/IMutableIndex.cs
index c1270b4995e..23db73a7ff0 100644
--- a/src/EFCore/Metadata/IMutableIndex.cs
+++ b/src/EFCore/Metadata/IMutableIndex.cs
@@ -23,6 +23,12 @@ public interface IMutableIndex : IReadOnlyIndex, IMutableAnnotatable
///
new bool IsUnique { get; set; }
+ ///
+ /// A set of values indicating whether each corresponding index column has descending sort order.
+ /// If less sort order values are provided than there are columns, the remaining columns will have ascending order.
+ ///
+ new IReadOnlyList IsDescending { get; set; }
+
///
/// Gets the properties that this index is defined on.
///
diff --git a/src/EFCore/Metadata/IReadOnlyIndex.cs b/src/EFCore/Metadata/IReadOnlyIndex.cs
index 6bcb12dbecf..d132969113d 100644
--- a/src/EFCore/Metadata/IReadOnlyIndex.cs
+++ b/src/EFCore/Metadata/IReadOnlyIndex.cs
@@ -28,6 +28,12 @@ public interface IReadOnlyIndex : IReadOnlyAnnotatable
///
bool IsUnique { get; }
+ ///
+ /// Gets a set of values indicating whether each corresponding index column has descending sort order.
+ /// If less sort order values are provided than there are columns, the remaining columns will have ascending order.
+ ///
+ IReadOnlyList IsDescending { get; }
+
///
/// Gets the entity type the index is defined on. This may be different from the type that
/// are defined on when the index is defined a derived type in an inheritance hierarchy (since the properties
diff --git a/src/EFCore/Metadata/Internal/Index.cs b/src/EFCore/Metadata/Internal/Index.cs
index f4a2bf54d69..766034567ed 100644
--- a/src/EFCore/Metadata/Internal/Index.cs
+++ b/src/EFCore/Metadata/Internal/Index.cs
@@ -15,10 +15,13 @@ namespace Microsoft.EntityFrameworkCore.Metadata.Internal;
public class Index : ConventionAnnotatable, IMutableIndex, IConventionIndex, IIndex
{
private bool? _isUnique;
+ private IReadOnlyList? _isDescending;
+
private InternalIndexBuilder? _builder;
private ConfigurationSource _configurationSource;
private ConfigurationSource? _isUniqueConfigurationSource;
+ private ConfigurationSource? _isDescendingConfigurationSource;
// Warning: Never access these fields directly as access needs to be thread-safe
private object? _nullableValueFactory;
@@ -178,8 +181,8 @@ public virtual bool IsUnique
: oldIsUnique;
}
- private static bool DefaultIsUnique
- => false;
+ private static readonly bool DefaultIsUnique
+ = false;
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
@@ -193,6 +196,74 @@ private static bool DefaultIsUnique
private void UpdateIsUniqueConfigurationSource(ConfigurationSource configurationSource)
=> _isUniqueConfigurationSource = configurationSource.Max(_isUniqueConfigurationSource);
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ public virtual IReadOnlyList IsDescending
+ {
+ get => _isDescending ?? DefaultIsDescending;
+ set => SetIsDescending(value, ConfigurationSource.Explicit);
+ }
+
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ public virtual IReadOnlyList? SetIsDescending(IReadOnlyList? descending, ConfigurationSource configurationSource)
+ {
+ EnsureMutable();
+
+ if (descending is not null && descending.Count > Properties.Count)
+ {
+ throw new ArgumentException(
+ CoreStrings.TooManyIndexSortOrderValues(descending.Count, Properties.Format(), Properties.Count), nameof(descending));
+ }
+
+ // Normalize all-false array to the simpler empty array representation
+ if (descending is not null && descending.All(d => d == false))
+ {
+ descending = DefaultIsDescending;
+ }
+
+ var oldIsDescending = IsDescending;
+ var isChanging = _isDescending is null != descending is null
+ || descending is not null && !oldIsDescending.SequenceEqual(descending);
+ _isDescending = descending;
+
+ if (descending == null)
+ {
+ _isDescendingConfigurationSource = null;
+ }
+ else
+ {
+ UpdateIsDescendingConfigurationSource(configurationSource);
+ }
+
+ return isChanging
+ ? DeclaringEntityType.Model.ConventionDispatcher.OnIndexSortOrderChanged(Builder)
+ : oldIsDescending;
+ }
+
+ private static readonly bool[] DefaultIsDescending
+ = Array.Empty();
+
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ public virtual ConfigurationSource? GetIsDescendingConfigurationSource()
+ => _isDescendingConfigurationSource;
+
+ private void UpdateIsDescendingConfigurationSource(ConfigurationSource configurationSource)
+ => _isDescendingConfigurationSource = configurationSource.Max(_isDescendingConfigurationSource);
+
///
/// Runs the conventions when an annotation was set or removed.
///
@@ -369,4 +440,26 @@ IEntityType IIndex.DeclaringEntityType
[DebuggerStepThrough]
bool? IConventionIndex.SetIsUnique(bool? unique, bool fromDataAnnotation)
=> SetIsUnique(unique, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention);
+
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ [DebuggerStepThrough]
+ IReadOnlyList? IConventionIndex.SetIsDescending(bool[]? descending, bool fromDataAnnotation)
+ => SetIsDescending(descending, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention);
+
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ IReadOnlyList IReadOnlyIndex.IsDescending
+ {
+ [DebuggerStepThrough]
+ get => IsDescending;
+ }
}
diff --git a/src/EFCore/Metadata/Internal/InternalIndexBuilder.cs b/src/EFCore/Metadata/Internal/InternalIndexBuilder.cs
index 55ff70cd7ac..19c2a65c49a 100644
--- a/src/EFCore/Metadata/Internal/InternalIndexBuilder.cs
+++ b/src/EFCore/Metadata/Internal/InternalIndexBuilder.cs
@@ -49,6 +49,34 @@ public virtual bool CanSetIsUnique(bool? unique, ConfigurationSource? configurat
=> Metadata.IsUnique == unique
|| configurationSource.Overrides(Metadata.GetIsUniqueConfigurationSource());
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ public virtual InternalIndexBuilder? IsDescending(IReadOnlyList? descending, ConfigurationSource configurationSource)
+ {
+ if (!CanSetIsDescending(descending, configurationSource))
+ {
+ return null;
+ }
+
+ Metadata.SetIsDescending(descending, configurationSource);
+ return this;
+ }
+
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ public virtual bool CanSetIsDescending(IReadOnlyList? descending, ConfigurationSource? configurationSource)
+ => descending is null
+ || Metadata.IsDescending.SequenceEqual(descending)
+ || configurationSource.Overrides(Metadata.GetIsDescendingConfigurationSource());
+
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
@@ -107,4 +135,26 @@ bool IConventionIndexBuilder.CanSetIsUnique(bool? unique, bool fromDataAnnotatio
=> CanSetIsUnique(
unique,
fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention);
+
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ IConventionIndexBuilder? IConventionIndexBuilder.IsDescending(IReadOnlyList? descending, bool fromDataAnnotation)
+ => IsDescending(
+ descending,
+ fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention);
+
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ bool IConventionIndexBuilder.CanSetIsDescending(IReadOnlyList? descending, bool fromDataAnnotation)
+ => CanSetIsDescending(
+ descending,
+ fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention);
}
diff --git a/src/EFCore/Metadata/RuntimeIndex.cs b/src/EFCore/Metadata/RuntimeIndex.cs
index 109d734f507..306c3b13af2 100644
--- a/src/EFCore/Metadata/RuntimeIndex.cs
+++ b/src/EFCore/Metadata/RuntimeIndex.cs
@@ -55,6 +55,15 @@ public RuntimeIndex(
///
public virtual RuntimeEntityType DeclaringEntityType { get; }
+ ///
+ /// Always returns an empty array for .
+ ///
+ IReadOnlyList IReadOnlyIndex.IsDescending
+ {
+ [DebuggerStepThrough]
+ get => Array.Empty();
+ }
+
///
/// Returns a string that represents the current object.
///
diff --git a/src/EFCore/Properties/CoreStrings.Designer.cs b/src/EFCore/Properties/CoreStrings.Designer.cs
index ecebccc29e4..acdd0fb22bf 100644
--- a/src/EFCore/Properties/CoreStrings.Designer.cs
+++ b/src/EFCore/Properties/CoreStrings.Designer.cs
@@ -2585,6 +2585,14 @@ public static string TempValuePersists(object? property, object? entityType, obj
GetString("TempValuePersists", "0_property", "1_entityType", nameof(state)),
property, entityType, state);
+ ///
+ /// Too many index sort order values ({numDescendingValues}) specified for index {indexProperties}, which has only {numProperties} properties.
+ ///
+ public static string TooManyIndexSortOrderValues(object? numDescendingValues, object? indexProperties, object? numProperties)
+ => string.Format(
+ GetString("TooManyIndexSortOrderValues", nameof(numDescendingValues), nameof(indexProperties), nameof(numProperties)),
+ numDescendingValues, indexProperties, numProperties);
+
///
/// The instance of entity type '{runtimeEntityType}' cannot be tracked as the entity type '{entityType}' because the two types are not in the same hierarchy.
///
diff --git a/src/EFCore/Properties/CoreStrings.resx b/src/EFCore/Properties/CoreStrings.resx
index e2872bc118d..5481f527d8d 100644
--- a/src/EFCore/Properties/CoreStrings.resx
+++ b/src/EFCore/Properties/CoreStrings.resx
@@ -1397,6 +1397,9 @@
The property '{1_entityType}.{0_property}' has a temporary value while attempting to change the entity's state to '{state}'. Either set a permanent value explicitly, or ensure that the database is configured to generate values for this property.
+
+ Too many index sort order values ({numDescendingValues}) specified for {indexProperties}, which has only {numProperties} properties.
+
The instance of entity type '{runtimeEntityType}' cannot be tracked as the entity type '{entityType}' because the two types are not in the same hierarchy.
diff --git a/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationOperationGeneratorTest.cs b/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationOperationGeneratorTest.cs
index 22778a022c3..55454c273bb 100644
--- a/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationOperationGeneratorTest.cs
+++ b/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationOperationGeneratorTest.cs
@@ -963,6 +963,9 @@ public void CreateIndexOperation_required_args()
Assert.Equal("IX_Post_Title", o.Name);
Assert.Equal("Post", o.Table);
Assert.Equal(new[] { "Title" }, o.Columns);
+ Assert.False(o.IsUnique);
+ Assert.Empty(o.IsDescending);
+ Assert.Null(o.Filter);
});
[ConditionalFact]
@@ -973,8 +976,9 @@ public void CreateIndexOperation_all_args()
Name = "IX_Post_Title",
Schema = "dbo",
Table = "Post",
- Columns = new[] { "Title" },
+ Columns = new[] { "Title", "Name" },
IsUnique = true,
+ IsDescending = new[] { true, false },
Filter = "[Title] IS NOT NULL"
},
"mb.CreateIndex("
@@ -985,18 +989,22 @@ public void CreateIndexOperation_all_args()
+ _eol
+ " table: \"Post\","
+ _eol
- + " column: \"Title\","
+ + " columns: new[] { \"Title\", \"Name\" },"
+ _eol
+ " unique: true,"
+ _eol
+ + " descending: new[] { true, false },"
+ + _eol
+ " filter: \"[Title] IS NOT NULL\");",
o =>
{
Assert.Equal("IX_Post_Title", o.Name);
Assert.Equal("dbo", o.Schema);
Assert.Equal("Post", o.Table);
- Assert.Equal(new[] { "Title" }, o.Columns);
+ Assert.Equal(new[] { "Title", "Name" }, o.Columns);
Assert.True(o.IsUnique);
+ Assert.Equal(new[] { true, false }, o.IsDescending);
+ Assert.Equal("[Title] IS NOT NULL", o.Filter);
});
[ConditionalFact]
diff --git a/test/EFCore.Design.Tests/Migrations/ModelSnapshotSqlServerTest.cs b/test/EFCore.Design.Tests/Migrations/ModelSnapshotSqlServerTest.cs
index 2779ba35261..469756bed64 100644
--- a/test/EFCore.Design.Tests/Migrations/ModelSnapshotSqlServerTest.cs
+++ b/test/EFCore.Design.Tests/Migrations/ModelSnapshotSqlServerTest.cs
@@ -164,6 +164,14 @@ private class EntityWithGenericProperty
public TProperty Property { get; set; }
}
+ private class EntityWithThreeProperties
+ {
+ public int Id { get; set; }
+ public int X { get; set; }
+ public int Y { get; set; }
+ public int Z { get; set; }
+ }
+
[Index(nameof(FirstName), nameof(LastName))]
private class EntityWithIndexAttribute
{
@@ -4231,7 +4239,7 @@ public virtual void Index_Fluent_APIs_are_properly_generated()
o => Assert.True(o.GetEntityTypes().Single().GetIndexes().Single().IsClustered()));
[ConditionalFact]
- public virtual void Index_isUnique_is_stored_in_snapshot()
+ public virtual void Index_IsUnique_is_stored_in_snapshot()
=> Test(
builder =>
{
@@ -4261,6 +4269,75 @@ public virtual void Index_isUnique_is_stored_in_snapshot()
});"),
o => Assert.True(o.GetEntityTypes().First().GetIndexes().First().IsUnique));
+ [ConditionalFact]
+ public virtual void Index_IsDescending_is_stored_in_snapshot()
+ => Test(
+ builder =>
+ {
+ builder.Entity(
+ e =>
+ {
+ e.HasIndex(t => new { t.X, t.Y, t.Z }, "IX_empty");
+ e.HasIndex(t => new { t.X, t.Y, t.Z }, "IX_all_ascending")
+ .IsDescending(false, false, false);
+ e.HasIndex(t => new { t.X, t.Y, t.Z }, "IX_all_descending")
+ .IsDescending(true, true, true);
+ e.HasIndex(t => new { t.X, t.Y, t.Z }, "IX_mixed")
+ .IsDescending(false, true, false);
+ });
+ },
+ AddBoilerPlate(
+ GetHeading()
+ + @"
+ modelBuilder.Entity(""Microsoft.EntityFrameworkCore.Migrations.ModelSnapshotSqlServerTest+EntityWithThreeProperties"", b =>
+ {
+ b.Property(""Id"")
+ .ValueGeneratedOnAdd()
+ .HasColumnType(""int"");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property(""Id""), 1L, 1);
+
+ b.Property(""X"")
+ .HasColumnType(""int"");
+
+ b.Property(""Y"")
+ .HasColumnType(""int"");
+
+ b.Property(""Z"")
+ .HasColumnType(""int"");
+
+ b.HasKey(""Id"");
+
+ b.HasIndex(new[] { ""X"", ""Y"", ""Z"" }, ""IX_all_ascending"");
+
+ b.HasIndex(new[] { ""X"", ""Y"", ""Z"" }, ""IX_all_descending"")
+ .IsDescending(true, true, true);
+
+ b.HasIndex(new[] { ""X"", ""Y"", ""Z"" }, ""IX_empty"");
+
+ b.HasIndex(new[] { ""X"", ""Y"", ""Z"" }, ""IX_mixed"")
+ .IsDescending(false, true, false);
+
+ b.ToTable(""EntityWithThreeProperties"");
+ });"),
+ o =>
+ {
+ var entityType = o.GetEntityTypes().Single();
+ Assert.Equal(4, entityType.GetIndexes().Count());
+
+ var emptyIndex = Assert.Single(entityType.GetIndexes(), i => i.Name == "IX_empty");
+ Assert.Equal(Array.Empty(), emptyIndex.IsDescending);
+
+ var allAscendingIndex = Assert.Single(entityType.GetIndexes(), i => i.Name == "IX_all_ascending");
+ Assert.Equal(Array.Empty(), allAscendingIndex.IsDescending);
+
+ var allDescendingIndex = Assert.Single(entityType.GetIndexes(), i => i.Name == "IX_all_descending");
+ Assert.Equal(new[] { true, true, true }, allDescendingIndex.IsDescending);
+
+ var mixedIndex = Assert.Single(entityType.GetIndexes(), i => i.Name == "IX_mixed");
+ Assert.Equal(new[] { false, true, false }, mixedIndex.IsDescending);
+ });
+
[ConditionalFact]
public virtual void Index_database_name_annotation_is_stored_in_snapshot_as_fluent_api()
=> Test(
diff --git a/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpDbContextGeneratorTest.cs b/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpDbContextGeneratorTest.cs
index b0a56787422..74d3f331aad 100644
--- a/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpDbContextGeneratorTest.cs
+++ b/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpDbContextGeneratorTest.cs
@@ -553,7 +553,8 @@ public void Entity_with_indexes_and_use_data_annotations_false_always_generates_
x.Property("C");
x.HasKey("Id");
x.HasIndex(new[] { "A", "B" }, "IndexOnAAndB")
- .IsUnique();
+ .IsUnique()
+ .IsDescending();
x.HasIndex(new[] { "B", "C" }, "IndexOnBAndC")
.HasFilter("Filter SQL")
.HasAnnotation("AnnotationName", "AnnotationValue");
@@ -598,7 +599,8 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
modelBuilder.Entity(entity =>
{
entity.HasIndex(e => new { e.A, e.B }, ""IndexOnAAndB"")
- .IsUnique();
+ .IsUnique()
+ .IsDescending();
entity.HasIndex(e => new { e.B, e.C }, ""IndexOnBAndC"")
.HasFilter(""Filter SQL"")
@@ -633,7 +635,8 @@ public void Entity_with_indexes_and_use_data_annotations_true_generates_fluent_A
x.Property("C");
x.HasKey("Id");
x.HasIndex(new[] { "A", "B" }, "IndexOnAAndB")
- .IsUnique();
+ .IsUnique()
+ .IsDescending();
x.HasIndex(new[] { "B", "C" }, "IndexOnBAndC")
.HasFilter("Filter SQL")
.HasAnnotation("AnnotationName", "AnnotationValue");
@@ -696,6 +699,106 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
model =>
Assert.Equal(2, model.FindEntityType("TestNamespace.EntityWithIndexes").GetIndexes().Count()));
+ [ConditionalFact]
+ public void Indexes_with_descending()
+ => Test(
+ modelBuilder => modelBuilder
+ .Entity(
+ "EntityWithIndexes",
+ x =>
+ {
+ x.Property("Id");
+ x.Property("X");
+ x.Property("Y");
+ x.Property("Z");
+ x.HasKey("Id");
+ x.HasIndex(new[] { "X", "Y", "Z" }, "IX_empty");
+ x.HasIndex(new[] { "X", "Y", "Z" }, "IX_all_ascending")
+ .IsDescending(false, false, false);
+ x.HasIndex(new[] { "X", "Y", "Z" }, "IX_all_descending")
+ .IsDescending(true, true, true);
+ x.HasIndex(new[] { "X", "Y", "Z" }, "IX_mixed")
+ .IsDescending(false, true, false);
+ }),
+ new ModelCodeGenerationOptions { UseDataAnnotations = false },
+ code =>
+ {
+ AssertFileContents(
+ @"using System;
+using System.Collections.Generic;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata;
+
+namespace TestNamespace
+{
+ public partial class TestDbContext : DbContext
+ {
+ public TestDbContext()
+ {
+ }
+
+ public TestDbContext(DbContextOptions options)
+ : base(options)
+ {
+ }
+
+ public virtual DbSet EntityWithIndexes { get; set; }
+
+ protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
+ {
+ if (!optionsBuilder.IsConfigured)
+ {
+#warning "
+ + DesignStrings.SensitiveInformationWarning
+ + @"
+ optionsBuilder.UseSqlServer(""Initial Catalog=TestDatabase"");
+ }
+ }
+
+ protected override void OnModelCreating(ModelBuilder modelBuilder)
+ {
+ modelBuilder.Entity(entity =>
+ {
+ entity.HasIndex(e => new { e.X, e.Y, e.Z }, ""IX_all_ascending"");
+
+ entity.HasIndex(e => new { e.X, e.Y, e.Z }, ""IX_all_descending"")
+ .IsDescending();
+
+ entity.HasIndex(e => new { e.X, e.Y, e.Z }, ""IX_empty"");
+
+ entity.HasIndex(e => new { e.X, e.Y, e.Z }, ""IX_mixed"")
+ .IsDescending(false, true);
+
+ entity.Property(e => e.Id).UseIdentityColumn();
+ });
+
+ OnModelCreatingPartial(modelBuilder);
+ }
+
+ partial void OnModelCreatingPartial(ModelBuilder modelBuilder);
+ }
+}
+",
+ code.ContextFile);
+ },
+ model =>
+ {
+ var entityType = model.FindEntityType("TestNamespace.EntityWithIndexes")!;
+ Assert.Equal(4, entityType.GetIndexes().Count());
+
+ var emptyIndex = Assert.Single(entityType.GetIndexes(), i => i.Name == "IX_empty");
+ Assert.Equal(Array.Empty(), emptyIndex.IsDescending);
+
+ var allAscendingIndex = Assert.Single(entityType.GetIndexes(), i => i.Name == "IX_all_ascending");
+ Assert.Equal(Array.Empty(), allAscendingIndex.IsDescending);
+
+ var allDescendingIndex = Assert.Single(entityType.GetIndexes(), i => i.Name == "IX_all_descending");
+ Assert.Equal(new[] { true, true, true }, allDescendingIndex.IsDescending);
+
+ var mixedIndex = Assert.Single(entityType.GetIndexes(), i => i.Name == "IX_mixed");
+ Assert.Equal(new[] { false, true }, mixedIndex.IsDescending);
+ });
+
[ConditionalFact]
public void Entity_lambda_uses_correct_identifiers()
=> Test(
diff --git a/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpEntityTypeGeneratorTest.cs b/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpEntityTypeGeneratorTest.cs
index b368816418b..07dcd6095c4 100644
--- a/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpEntityTypeGeneratorTest.cs
+++ b/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpEntityTypeGeneratorTest.cs
@@ -260,7 +260,8 @@ public void IndexAttribute_is_generated_for_multiple_indexes_with_name_unique()
x.Property("C");
x.HasKey("Id");
x.HasIndex(new[] { "A", "B" }, "IndexOnAAndB")
- .IsUnique();
+ .IsUnique()
+ .IsDescending();
x.HasIndex(new[] { "B", "C" }, "IndexOnBAndC");
x.HasIndex("C");
}),
@@ -277,7 +278,7 @@ public void IndexAttribute_is_generated_for_multiple_indexes_with_name_unique()
namespace TestNamespace
{
[Index(""C"")]
- [Index(""A"", ""B"", Name = ""IndexOnAAndB"", IsUnique = true)]
+ [Index(""A"", ""B"", Name = ""IndexOnAAndB"", IsUnique = true, IsDescending = new[] { true, true })]
[Index(""B"", ""C"", Name = ""IndexOnBAndC"")]
public partial class EntityWithIndexes
{
diff --git a/test/EFCore.Design.Tests/Scaffolding/Internal/RelationalScaffoldingModelFactoryTest.cs b/test/EFCore.Design.Tests/Scaffolding/Internal/RelationalScaffoldingModelFactoryTest.cs
index acfee0d4baa..58f9bad1376 100644
--- a/test/EFCore.Design.Tests/Scaffolding/Internal/RelationalScaffoldingModelFactoryTest.cs
+++ b/test/EFCore.Design.Tests/Scaffolding/Internal/RelationalScaffoldingModelFactoryTest.cs
@@ -1376,6 +1376,90 @@ public void Unique_index_composite_foreign_key()
Assert.Equal(parent.FindPrimaryKey(), fk.PrincipalKey);
}
+ [ConditionalFact]
+ public void Index_descending()
+ {
+ var table = new DatabaseTable
+ {
+ Database = Database,
+ Name = "SomeTable",
+ Columns =
+ {
+ new DatabaseColumn
+ {
+ Table = Table,
+ Name = "X",
+ StoreType = "int"
+ },
+ new DatabaseColumn
+ {
+ Table = Table,
+ Name = "Y",
+ StoreType = "int"
+ },
+ new DatabaseColumn
+ {
+ Table = Table,
+ Name = "Z",
+ StoreType = "int"
+ }
+ }
+ };
+
+ table.Indexes.Add(
+ new DatabaseIndex
+ {
+ Table = Table,
+ Name = "IX_empty",
+ Columns = { table.Columns[0], table.Columns[1], table.Columns[2] }
+ });
+
+ table.Indexes.Add(
+ new DatabaseIndex
+ {
+ Table = Table,
+ Name = "IX_all_ascending",
+ Columns = { table.Columns[0], table.Columns[1], table.Columns[2] },
+ IsDescending = { false, false, false }
+ });
+
+ table.Indexes.Add(
+ new DatabaseIndex
+ {
+ Table = Table,
+ Name = "IX_all_descending",
+ Columns = { table.Columns[0], table.Columns[1], table.Columns[2] },
+ IsDescending = { true, true, true }
+ });
+
+ table.Indexes.Add(
+ new DatabaseIndex
+ {
+ Table = Table,
+ Name = "IX_mixed",
+ Columns = { table.Columns[0], table.Columns[1], table.Columns[2] },
+ IsDescending = { false, true, false }
+ });
+
+ var model = _factory.Create(
+ new DatabaseModel { Tables = { table } },
+ new ModelReverseEngineerOptions { NoPluralize = true });
+
+ var entityType = model.FindEntityType("SomeTable")!;
+
+ var emptyIndex = Assert.Single(entityType.GetIndexes(), i => i.Name == "IX_empty");
+ Assert.Equal(Array.Empty(), emptyIndex.IsDescending);
+
+ var allAscendingIndex = Assert.Single(entityType.GetIndexes(), i => i.Name == "IX_all_ascending");
+ Assert.Equal(Array.Empty(), allAscendingIndex.IsDescending);
+
+ var allDescendingIndex = Assert.Single(entityType.GetIndexes(), i => i.Name == "IX_all_descending");
+ Assert.Equal(new[] { true, true, true }, allDescendingIndex.IsDescending);
+
+ var mixedIndex = Assert.Single(entityType.GetIndexes(), i => i.Name == "IX_mixed");
+ Assert.Equal(new[] { false, true, false }, mixedIndex.IsDescending);
+ }
+
[ConditionalFact]
public void Unique_names()
{
diff --git a/test/EFCore.Relational.Specification.Tests/Migrations/MigrationsTestBase.cs b/test/EFCore.Relational.Specification.Tests/Migrations/MigrationsTestBase.cs
index ade454b8d5a..3d6baf6133c 100644
--- a/test/EFCore.Relational.Specification.Tests/Migrations/MigrationsTestBase.cs
+++ b/test/EFCore.Relational.Specification.Tests/Migrations/MigrationsTestBase.cs
@@ -1042,6 +1042,12 @@ public virtual Task Create_index()
Assert.Same(table.Columns.Single(c => c.Name == "FirstName"), Assert.Single(index.Columns));
Assert.Equal("IX_People_FirstName", index.Name);
Assert.False(index.IsUnique);
+
+ if (index.IsDescending.Count > 0)
+ {
+ Assert.Collection(index.IsDescending, descending => Assert.False(descending));
+ }
+
Assert.Null(index.Filter);
});
@@ -1064,6 +1070,46 @@ public virtual Task Create_index_unique()
Assert.True(index.IsUnique);
});
+ [ConditionalFact]
+ public virtual Task Create_index_descending()
+ => Test(
+ builder => builder.Entity(
+ "People", e =>
+ {
+ e.Property("Id");
+ e.Property("X");
+ }),
+ builder => { },
+ builder => builder.Entity("People").HasIndex("X").IsDescending(),
+ model =>
+ {
+ var table = Assert.Single(model.Tables);
+ var index = Assert.Single(table.Indexes);
+ Assert.Collection(index.IsDescending, Assert.True);
+ });
+
+ [ConditionalFact]
+ public virtual Task Create_index_descending_mixed()
+ => Test(
+ builder => builder.Entity(
+ "People", e =>
+ {
+ e.Property("Id");
+ e.Property("X");
+ e.Property("Y");
+ e.Property("Z");
+ }),
+ builder => { },
+ builder => builder.Entity("People")
+ .HasIndex("X", "Y", "Z")
+ .IsDescending(false, true),
+ model =>
+ {
+ var table = Assert.Single(model.Tables);
+ var index = Assert.Single(table.Indexes);
+ Assert.Collection(index.IsDescending, Assert.False, Assert.True, Assert.False);
+ });
+
[ConditionalFact]
public virtual Task Create_index_with_filter()
=> Test(
diff --git a/test/EFCore.Specification.Tests/TestUtilities/TestHelpers.cs b/test/EFCore.Specification.Tests/TestUtilities/TestHelpers.cs
index 31229cd649b..9daa1ebd398 100644
--- a/test/EFCore.Specification.Tests/TestUtilities/TestHelpers.cs
+++ b/test/EFCore.Specification.Tests/TestUtilities/TestHelpers.cs
@@ -505,6 +505,7 @@ public void RemoveAllConventions()
Conventions.IndexAnnotationChangedConventions.Clear();
Conventions.IndexRemovedConventions.Clear();
Conventions.IndexUniquenessChangedConventions.Clear();
+ Conventions.IndexSortOrderChangedConventions.Clear();
Conventions.KeyAddedConventions.Clear();
Conventions.KeyAnnotationChangedConventions.Clear();
Conventions.KeyRemovedConventions.Clear();
diff --git a/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsSqlServerTest.cs
index 9e317426c62..f6871e70a40 100644
--- a/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsSqlServerTest.cs
+++ b/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsSqlServerTest.cs
@@ -1246,6 +1246,22 @@ FROM [sys].[default_constraints] [d]
@"CREATE UNIQUE INDEX [IX_People_FirstName_LastName] ON [People] ([FirstName], [LastName]) WHERE [FirstName] IS NOT NULL AND [LastName] IS NOT NULL;");
}
+ public override async Task Create_index_descending()
+ {
+ await base.Create_index_descending();
+
+ AssertSql(
+ @"CREATE INDEX [IX_People_X] ON [People] ([X] DESC);");
+ }
+
+ public override async Task Create_index_descending_mixed()
+ {
+ await base.Create_index_descending_mixed();
+
+ AssertSql(
+ @"CREATE INDEX [IX_People_X_Y_Z] ON [People] ([X], [Y] DESC, [Z]);");
+ }
+
public override async Task Create_index_with_filter()
{
await base.Create_index_with_filter();
diff --git a/test/EFCore.Tests/Metadata/Conventions/IndexAttributeConventionTest.cs b/test/EFCore.Tests/Metadata/Conventions/IndexAttributeConventionTest.cs
index 829a87585ef..e989e1dfc84 100644
--- a/test/EFCore.Tests/Metadata/Conventions/IndexAttributeConventionTest.cs
+++ b/test/EFCore.Tests/Metadata/Conventions/IndexAttributeConventionTest.cs
@@ -28,6 +28,7 @@ public void IndexAttribute_overrides_configuration_from_convention()
var indexProperties = new List { propABuilder.Metadata.Name, propBBuilder.Metadata.Name };
var indexBuilder = entityBuilder.HasIndex(indexProperties, "IndexOnAAndB", ConfigurationSource.Convention);
indexBuilder.IsUnique(false, ConfigurationSource.Convention);
+ indexBuilder.IsDescending(new[] { false, true }, ConfigurationSource.Convention);
RunConvention(entityBuilder);
RunConvention(modelBuilder);
@@ -35,8 +36,11 @@ public void IndexAttribute_overrides_configuration_from_convention()
var index = entityBuilder.Metadata.GetIndexes().Single();
Assert.Equal(ConfigurationSource.DataAnnotation, index.GetConfigurationSource());
Assert.Equal("IndexOnAAndB", index.Name);
+
Assert.True(index.IsUnique);
Assert.Equal(ConfigurationSource.DataAnnotation, index.GetIsUniqueConfigurationSource());
+ Assert.Equal(new[] { true, false }, index.IsDescending);
+ Assert.Equal(ConfigurationSource.DataAnnotation, index.GetIsDescendingConfigurationSource());
Assert.Collection(
index.Properties,
prop0 => Assert.Equal("A", prop0.Name),
@@ -50,7 +54,8 @@ public void IndexAttribute_can_be_overriden_using_explicit_configuration()
var entityBuilder = modelBuilder.Entity();
entityBuilder.HasIndex(new[] { "A", "B" }, "IndexOnAAndB")
- .IsUnique(false);
+ .IsUnique(false)
+ .IsDescending(false, true);
modelBuilder.Model.FinalizeModel();
@@ -59,6 +64,8 @@ public void IndexAttribute_can_be_overriden_using_explicit_configuration()
Assert.Equal("IndexOnAAndB", index.Name);
Assert.False(index.IsUnique);
Assert.Equal(ConfigurationSource.Explicit, index.GetIsUniqueConfigurationSource());
+ Assert.Equal(new[] { false, true }, index.IsDescending);
+ Assert.Equal(ConfigurationSource.Explicit, index.GetIsDescendingConfigurationSource());
Assert.Collection(
index.Properties,
prop0 => Assert.Equal("A", prop0.Name),
@@ -316,7 +323,7 @@ private IndexAttributeConvention CreateIndexAttributeConvention()
private ProviderConventionSetBuilderDependencies CreateDependencies()
=> InMemoryTestHelpers.Instance.CreateContextServices().GetRequiredService();
- [Index(nameof(A), nameof(B), Name = "IndexOnAAndB", IsUnique = true)]
+ [Index(nameof(A), nameof(B), Name = "IndexOnAAndB", IsUnique = true, IsDescending = new[] { true, false })]
private class EntityWithIndex
{
public int Id { get; set; }
diff --git a/test/EFCore.Tests/ModelBuilding/ModelBuilderGenericTest.cs b/test/EFCore.Tests/ModelBuilding/ModelBuilderGenericTest.cs
index f4e025e6f25..c7a2e0af068 100644
--- a/test/EFCore.Tests/ModelBuilding/ModelBuilderGenericTest.cs
+++ b/test/EFCore.Tests/ModelBuilding/ModelBuilderGenericTest.cs
@@ -581,6 +581,9 @@ public override TestIndexBuilder HasAnnotation(string annotation, objec
public override TestIndexBuilder IsUnique(bool isUnique = true)
=> new GenericTestIndexBuilder(IndexBuilder.IsUnique(isUnique));
+ public override TestIndexBuilder IsDescending(params bool[] isDescending)
+ => new GenericTestIndexBuilder(IndexBuilder.IsDescending(isDescending));
+
IndexBuilder IInfrastructure>.Instance
=> IndexBuilder;
}
diff --git a/test/EFCore.Tests/ModelBuilding/ModelBuilderNonGenericTest.cs b/test/EFCore.Tests/ModelBuilding/ModelBuilderNonGenericTest.cs
index a657349bf48..6904a9b0078 100644
--- a/test/EFCore.Tests/ModelBuilding/ModelBuilderNonGenericTest.cs
+++ b/test/EFCore.Tests/ModelBuilding/ModelBuilderNonGenericTest.cs
@@ -686,6 +686,9 @@ public override TestIndexBuilder HasAnnotation(string annotation, objec
public override TestIndexBuilder IsUnique(bool isUnique = true)
=> new NonGenericTestIndexBuilder(IndexBuilder.IsUnique(isUnique));
+ public override TestIndexBuilder IsDescending(params bool[] isDescending)
+ => new NonGenericTestIndexBuilder(IndexBuilder.IsDescending(isDescending));
+
IndexBuilder IInfrastructure.Instance
=> IndexBuilder;
}
diff --git a/test/EFCore.Tests/ModelBuilding/ModelBuilderTestBase.cs b/test/EFCore.Tests/ModelBuilding/ModelBuilderTestBase.cs
index c54b376fa60..368b3aaa84f 100644
--- a/test/EFCore.Tests/ModelBuilding/ModelBuilderTestBase.cs
+++ b/test/EFCore.Tests/ModelBuilding/ModelBuilderTestBase.cs
@@ -362,6 +362,7 @@ public abstract class TestIndexBuilder
public abstract TestIndexBuilder HasAnnotation(string annotation, object? value);
public abstract TestIndexBuilder IsUnique(bool isUnique = true);
+ public abstract TestIndexBuilder IsDescending(params bool[] isDescending);
}
public abstract class TestPropertyBuilder
diff --git a/test/EFCore.Tests/ModelBuilding/NonRelationshipTestBase.cs b/test/EFCore.Tests/ModelBuilding/NonRelationshipTestBase.cs
index af02579ec2f..7af816c8073 100644
--- a/test/EFCore.Tests/ModelBuilding/NonRelationshipTestBase.cs
+++ b/test/EFCore.Tests/ModelBuilding/NonRelationshipTestBase.cs
@@ -1722,6 +1722,7 @@ public virtual void Can_add_multiple_indexes()
entityBuilder.HasIndex(ix => ix.Id).IsUnique();
entityBuilder.HasIndex(ix => ix.Name).HasAnnotation("A1", "V1");
entityBuilder.HasIndex(ix => ix.Id, "Named");
+ entityBuilder.HasIndex(ix => ix.Id, "Descending").IsDescending();
var model = modelBuilder.FinalizeModel();
@@ -1729,7 +1730,7 @@ public virtual void Can_add_multiple_indexes()
var idProperty = entityType.FindProperty(nameof(Customer.Id));
var nameProperty = entityType.FindProperty(nameof(Customer.Name));
- Assert.Equal(3, entityType.GetIndexes().Count());
+ Assert.Equal(4, entityType.GetIndexes().Count());
var firstIndex = entityType.FindIndex(idProperty);
Assert.True(firstIndex.IsUnique);
var secondIndex = entityType.FindIndex(nameProperty);
@@ -1737,6 +1738,8 @@ public virtual void Can_add_multiple_indexes()
Assert.Equal("V1", secondIndex["A1"]);
var namedIndex = entityType.FindIndex("Named");
Assert.False(namedIndex.IsUnique);
+ var descendingIndex = entityType.FindIndex("Descending");
+ Assert.Equal(new[] { true }, descendingIndex.IsDescending);
}
[ConditionalFact]