Repository: dotnet/efcore

Issue:
Support for index ordering (ASC/DESC)
After discussion on #11846, we decided that the way to handle this is to allow multiple named indexes to be created over columns, while still preserving the current semantics (i.e. addressing by ordered property set) for unnamed indexes.

Original issue:
Add support for column sort order to migrations, metadata, and reverse engineering.
- Add API to specific ascending/descending columns in an index  (
  `CREATE INDEX IX_1 ON Table ( Col1 DESC, Col2 ASC)` )
- Add to reverse engineering database model ([prototype here](https://github.com/natemcmaster/EntityFramework/tree/scaffold-sort-order))
- Adapt to older version of SQLite (sort order is not supported < 3.8.9)



Hints:
Is this going to happen anytime soon? Using EfCore.Shaman to workaround this issue and others at the moment...
@jjxtra This issue is in the Backlog milestone. This means that it is not going to happen for the 2.1 release. We will re-assess the backlog following the 2.1 release and consider this item at that time. However, keep in mind that there are many other high priority features with which it will be competing for resources.
Note that this is being done in Npgsql as part of https://github.com/npgsql/Npgsql.EntityFrameworkCore.PostgreSQL/pull/705. It would ideally be better to have implemented here as well so that Npgsql can align (and reuse the same SortOrder enum, possibly).
Hello any news ont this feature

@bhugot This issue is in the Backlog milestone. This means that it is not currently planned for any upcoming release. We will re-assess the backlog for 5.0 and consider this item at that time. However, keep in mind that there are many other high priority features with which it will be competing for resources.
Another variant of this issue is for having multiple indexes on the same column, but with different collations (supported by PostgreSQL and Sqlite, but not SQL Server and MySQL).
Clearing Milestone to reconsider this, or part of this, for 5.0 per [this comment](https://github.com/dotnet/efcore/pull/21012#issuecomment-632859604). 
I hope this makes it for the next release.

Our entity has a primary key `TenantId bigint, IsPriority bit`
Ideally, we want to be able specify `DESC` on the `IsPriority`, so that values of 1 are naturally ordered before values of 0 when we select the rows of a given tenant.

As a work around, we have had to changed the name of the property to `IsNotPriority`, so that the priority rows have a 0 and are ordered first in the index. It's a shame to have to improperly name things because of limitations of EF.






Patch:
diff --git a/src/EFCore.Abstractions/IndexAttribute.cs b/src/EFCore.Abstractions/IndexAttribute.cs
index c60caefcb14..4b93178c115 100644
--- a/src/EFCore.Abstractions/IndexAttribute.cs
+++ b/src/EFCore.Abstractions/IndexAttribute.cs
@@ -15,8 +15,9 @@ namespace Microsoft.EntityFrameworkCore;
 [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
 public sealed class IndexAttribute : Attribute
 {
-    private bool? _isUnique;
     private string? _name;
+    private bool? _isUnique;
+    private bool[]? _isDescending;
 
     /// <summary>
     ///     Initializes a new instance of the <see cref="IndexAttribute" /> class.
@@ -54,6 +55,24 @@ public bool IsUnique
         set => _isUnique = value;
     }
 
+    /// <summary>
+    ///     A set of values indicating whether each corresponding index column has descending sort order.
+    /// </summary>
+    public bool[]? IsDescending
+    {
+        get => _isDescending;
+        set
+        {
+            if (value is not null && value.Length != PropertyNames.Count)
+            {
+                throw new ArgumentException(
+                    AbstractionsStrings.InvalidNumberOfIndexSortOrderValues(value.Length, PropertyNames.Count), nameof(IsDescending));
+            }
+
+            _isDescending = value;
+        }
+    }
+
     /// <summary>
     ///     Checks whether <see cref="IsUnique" /> has been explicitly set to a value.
     /// </summary>
diff --git a/src/EFCore.Abstractions/Properties/AbstractionsStrings.Designer.cs b/src/EFCore.Abstractions/Properties/AbstractionsStrings.Designer.cs
index 9c2fdfcc201..059dddee5cf 100644
--- a/src/EFCore.Abstractions/Properties/AbstractionsStrings.Designer.cs
+++ b/src/EFCore.Abstractions/Properties/AbstractionsStrings.Designer.cs
@@ -52,6 +52,14 @@ public static string CollectionArgumentIsEmpty(object? argumentName)
                 GetString("CollectionArgumentIsEmpty", nameof(argumentName)),
                 argumentName);
 
+        /// <summary>
+        ///     Invalid number of index sort order values: {numValues} values were provided, but the index has {numProperties} properties.
+        /// </summary>
+        public static string InvalidNumberOfIndexSortOrderValues(object? numValues, object? numProperties)
+            => string.Format(
+                GetString("CollectionArgumentIsEmpty", nameof(numValues), nameof(numProperties)),
+                numValues, numProperties);
+
         private static string GetString(string name, params string[] formatterNames)
         {
             var value = _resourceManager.GetString(name)!;
diff --git a/src/EFCore.Abstractions/Properties/AbstractionsStrings.resx b/src/EFCore.Abstractions/Properties/AbstractionsStrings.resx
index 7bdf6f59663..568f47d97cd 100644
--- a/src/EFCore.Abstractions/Properties/AbstractionsStrings.resx
+++ b/src/EFCore.Abstractions/Properties/AbstractionsStrings.resx
@@ -129,4 +129,7 @@
   <data name="CollectionArgumentIsEmpty" xml:space="preserve">
     <value>The collection argument '{argumentName}' must contain at least one element.</value>
   </data>
+  <data name="InvalidNumberOfIndexSortOrderValues" xml:space="preserve">
+    <value>Invalid number of index sort order values: {numValues} values were provided, but the index has {numProperties} properties.</value>
+  </data>
 </root>
\ No newline at end of file
diff --git a/src/EFCore.Design/Migrations/Design/CSharpMigrationOperationGenerator.cs b/src/EFCore.Design/Migrations/Design/CSharpMigrationOperationGenerator.cs
index 15380528cbb..b8aa23b7459 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 is not null)
+            {
+                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..533cb70ebf9 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 is not null)
+        {
+            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..78e4ff45d77 100644
--- a/src/EFCore.Design/Scaffolding/Internal/CSharpDbContextGenerator.cs
+++ b/src/EFCore.Design/Scaffolding/Internal/CSharpDbContextGenerator.cs
@@ -567,6 +567,11 @@ private void GenerateIndex(IIndex index)
             lines.Add($".{nameof(IndexBuilder.IsUnique)}()");
         }
 
+        if (index.IsDescending is not null)
+        {
+            lines.Add($".{nameof(IndexBuilder.IsDescending)}({string.Join(", ", index.IsDescending.Select(d => _code.Literal(d)))})");
+        }
+
         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..bade36e8011 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 is not null)
+                {
+                    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..839dfbcf7c4 100644
--- a/src/EFCore.Design/Scaffolding/Internal/RelationalScaffoldingModelFactory.cs
+++ b/src/EFCore.Design/Scaffolding/Internal/RelationalScaffoldingModelFactory.cs
@@ -627,9 +627,14 @@ protected virtual EntityTypeBuilder VisitIndexes(EntityTypeBuilder builder, ICol
 
         indexBuilder = indexBuilder.IsUnique(index.IsUnique);
 
+        if (index.IsDescending.Any(desc => desc))
+        {
+            indexBuilder = indexBuilder.IsDescending(index.IsDescending.ToArray());
+        }
+
         if (index.Filter != null)
         {
-            indexBuilder.HasFilter(index.Filter);
+            indexBuilder = indexBuilder.HasFilter(index.Filter);
         }
 
         indexBuilder.Metadata.AddAnnotations(index.GetAnnotations());
diff --git a/src/EFCore.Relational/Metadata/ITableIndex.cs b/src/EFCore.Relational/Metadata/ITableIndex.cs
index 93608beeadf..86ab50b304b 100644
--- a/src/EFCore.Relational/Metadata/ITableIndex.cs
+++ b/src/EFCore.Relational/Metadata/ITableIndex.cs
@@ -39,6 +39,11 @@ public interface ITableIndex : IAnnotatable
     /// </summary>
     bool IsUnique { get; }
 
+    /// <summary>
+    ///     A set of values indicating whether each corresponding index column has descending sort order.
+    /// </summary>
+    IReadOnlyList<bool>? IsDescending { get; }
+
     /// <summary>
     ///     Gets the expression used as the index filter.
     /// </summary>
@@ -70,8 +75,21 @@ 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}'{(
+                                MappedIndexes.First() is not RuntimeIndex
+                                && IsDescending is not null
+                                && i < IsDescending.Count
+                                && IsDescending[i]
+                                    ? " Desc"
+                                    : ""
+                                )}"))
+            .Append('}');
 
         if (IsUnique)
         {
diff --git a/src/EFCore.Relational/Metadata/Internal/RelationalIndexExtensions.cs b/src/EFCore.Relational/Metadata/Internal/RelationalIndexExtensions.cs
index fdf330b8323..2dd404a4500 100644
--- a/src/EFCore.Relational/Metadata/Internal/RelationalIndexExtensions.cs
+++ b/src/EFCore.Relational/Metadata/Internal/RelationalIndexExtensions.cs
@@ -32,9 +32,9 @@ public static bool AreCompatible(
             {
                 throw new InvalidOperationException(
                     RelationalStrings.DuplicateIndexTableMismatch(
-                        index.Properties.Format(),
+                        index.DisplayName(),
                         index.DeclaringEntityType.DisplayName(),
-                        duplicateIndex.Properties.Format(),
+                        duplicateIndex.DisplayName(),
                         duplicateIndex.DeclaringEntityType.DisplayName(),
                         index.GetDatabaseName(storeObject),
                         index.DeclaringEntityType.GetSchemaQualifiedTableName(),
@@ -50,9 +50,9 @@ public static bool AreCompatible(
             {
                 throw new InvalidOperationException(
                     RelationalStrings.DuplicateIndexColumnMismatch(
-                        index.Properties.Format(),
+                        index.DisplayName(),
                         index.DeclaringEntityType.DisplayName(),
-                        duplicateIndex.Properties.Format(),
+                        duplicateIndex.DisplayName(),
                         duplicateIndex.DeclaringEntityType.DisplayName(),
                         index.DeclaringEntityType.GetSchemaQualifiedTableName(),
                         index.GetDatabaseName(storeObject),
@@ -69,9 +69,29 @@ public static bool AreCompatible(
             {
                 throw new InvalidOperationException(
                     RelationalStrings.DuplicateIndexUniquenessMismatch(
-                        index.Properties.Format(),
+                        index.DisplayName(),
                         index.DeclaringEntityType.DisplayName(),
-                        duplicateIndex.Properties.Format(),
+                        duplicateIndex.DisplayName(),
+                        duplicateIndex.DeclaringEntityType.DisplayName(),
+                        index.DeclaringEntityType.GetSchemaQualifiedTableName(),
+                        index.GetDatabaseName(storeObject)));
+            }
+
+            return false;
+        }
+
+        if (index.IsDescending is null != duplicateIndex.IsDescending is null
+            || (index.IsDescending is not null
+                && duplicateIndex.IsDescending is not null
+                && !index.IsDescending.SequenceEqual(duplicateIndex.IsDescending)))
+        {
+            if (shouldThrow)
+            {
+                throw new InvalidOperationException(
+                    RelationalStrings.DuplicateIndexSortOrdersMismatch(
+                        index.DisplayName(),
+                        index.DeclaringEntityType.DisplayName(),
+                        duplicateIndex.DisplayName(),
                         duplicateIndex.DeclaringEntityType.DisplayName(),
                         index.DeclaringEntityType.GetSchemaQualifiedTableName(),
                         index.GetDatabaseName(storeObject)));
@@ -82,16 +102,21 @@ public static bool AreCompatible(
 
         if (index.GetFilter(storeObject) != duplicateIndex.GetFilter(storeObject))
         {
-            throw new InvalidOperationException(
-                RelationalStrings.DuplicateIndexFiltersMismatch(
-                    index.Properties.Format(),
-                    index.DeclaringEntityType.DisplayName(),
-                    duplicateIndex.Properties.Format(),
-                    duplicateIndex.DeclaringEntityType.DisplayName(),
-                    index.DeclaringEntityType.GetSchemaQualifiedTableName(),
-                    index.GetDatabaseName(storeObject),
-                    index.GetFilter(),
-                    duplicateIndex.GetFilter()));
+            if (shouldThrow)
+            {
+                throw new InvalidOperationException(
+                    RelationalStrings.DuplicateIndexFiltersMismatch(
+                        index.DisplayName(),
+                        index.DeclaringEntityType.DisplayName(),
+                        duplicateIndex.DisplayName(),
+                        duplicateIndex.DeclaringEntityType.DisplayName(),
+                        index.DeclaringEntityType.GetSchemaQualifiedTableName(),
+                        index.GetDatabaseName(storeObject),
+                        index.GetFilter(),
+                        duplicateIndex.GetFilter()));
+            }
+
+            return false;
         }
 
         return true;
diff --git a/src/EFCore.Relational/Metadata/Internal/TableIndex.cs b/src/EFCore.Relational/Metadata/Internal/TableIndex.cs
index a23021c34bb..318a1c97453 100644
--- a/src/EFCore.Relational/Metadata/Internal/TableIndex.cs
+++ b/src/EFCore.Relational/Metadata/Internal/TableIndex.cs
@@ -68,6 +68,10 @@ public override bool IsReadOnly
     /// <inheritdoc />
     public virtual bool IsUnique { get; }
 
+    /// <inheritdoc />
+    public virtual IReadOnlyList<bool>? IsDescending
+        => MappedIndexes.First().IsDescending;
+
     /// <inheritdoc />
     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..cba40a8ebbd 100644
--- a/src/EFCore.Relational/Migrations/Internal/MigrationsModelDiffer.cs
+++ b/src/EFCore.Relational/Migrations/Internal/MigrationsModelDiffer.cs
@@ -1393,6 +1393,10 @@ protected virtual IEnumerable<MigrationOperation> Diff(
 
     private bool IndexStructureEquals(ITableIndex source, ITableIndex target, DiffContext diffContext)
         => source.IsUnique == target.IsUnique
+            && ((source.IsDescending is null && target.IsDescending is null)
+                || (source.IsDescending is not null
+                    && target.IsDescending is not null
+                    && 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..d5c37ae52d2 100644
--- a/src/EFCore.Relational/Migrations/MigrationBuilder.cs
+++ b/src/EFCore.Relational/Migrations/MigrationBuilder.cs
@@ -601,6 +601,10 @@ public virtual AlterOperationBuilder<AlterTableOperation> AlterTable(
     /// <param name="schema">The schema that contains the table, or <see langword="null" /> to use the default schema.</param>
     /// <param name="unique">Indicates whether or not the index enforces uniqueness.</param>
     /// <param name="filter">The filter to apply to the index, or <see langword="null" /> for no filter.</param>
+    /// <param name="descending">
+    ///     A set of values indicating whether each corresponding index column has descending sort order.
+    ///     If <see langword="null" />, all columns will have ascending order.
+    /// </param>
     /// <returns>A builder to allow annotations to be added to the operation.</returns>
     public virtual OperationBuilder<CreateIndexOperation> CreateIndex(
         string name,
@@ -608,14 +612,16 @@ public virtual OperationBuilder<CreateIndexOperation> 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);
 
     /// <summary>
     ///     Builds a <see cref="CreateIndexOperation" /> to create a new composite (multi-column) index.
@@ -629,6 +635,10 @@ public virtual OperationBuilder<CreateIndexOperation> CreateIndex(
     /// <param name="schema">The schema that contains the table, or <see langword="null" /> to use the default schema.</param>
     /// <param name="unique">Indicates whether or not the index enforces uniqueness.</param>
     /// <param name="filter">The filter to apply to the index, or <see langword="null" /> for no filter.</param>
+    /// <param name="descending">
+    ///     A set of values indicating whether each corresponding index column has descending sort order.
+    ///     If <see langword="null" />, all columns will have ascending order.
+    /// </param>
     /// <returns>A builder to allow annotations to be added to the operation.</returns>
     public virtual OperationBuilder<CreateIndexOperation> CreateIndex(
         string name,
@@ -636,7 +646,8 @@ public virtual OperationBuilder<CreateIndexOperation> 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));
@@ -649,8 +660,10 @@ public virtual OperationBuilder<CreateIndexOperation> CreateIndex(
             Name = name,
             Columns = columns,
             IsUnique = unique,
+            IsDescending = descending,
             Filter = filter
         };
+
         Operations.Add(operation);
 
         return new OperationBuilder<CreateIndexOperation>(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(
     /// <param name="operation">The operation.</param>
     /// <param name="model">The target model which may be <see langword="null" /> if the operations exist without a model.</param>
     /// <param name="builder">The command builder to use to add the SQL fragment.</param>
-    protected virtual void IndexTraits(
-        MigrationOperation operation,
-        IModel? model,
-        MigrationCommandListBuilder builder)
+    protected virtual void IndexTraits(MigrationOperation operation, IModel? model, MigrationCommandListBuilder builder)
     {
     }
 
+    /// <summary>
+    ///     Returns a SQL fragment for the column list of an index from a <see cref="CreateIndexOperation" />.
+    /// </summary>
+    /// <param name="operation">The operation.</param>
+    /// <param name="model">The target model which may be <see langword="null" /> if the operations exist without a model.</param>
+    /// <param name="builder">The command builder to use to add the SQL fragment.</param>
+    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");
+            }
+        }
+    }
+
     /// <summary>
     ///     Generates a SQL fragment for extras (filter, included columns, options) of an index from a <see cref="CreateIndexOperation" />.
     /// </summary>
     /// <param name="operation">The operation.</param>
     /// <param name="model">The target model which may be <see langword="null" /> if the operations exist without a model.</param>
     /// <param name="builder">The command builder to use to add the SQL fragment.</param>
-    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..a156281704e 100644
--- a/src/EFCore.Relational/Migrations/Operations/CreateIndexOperation.cs
+++ b/src/EFCore.Relational/Migrations/Operations/CreateIndexOperation.cs
@@ -12,10 +12,8 @@ namespace Microsoft.EntityFrameworkCore.Migrations.Operations;
 [DebuggerDisplay("CREATE INDEX {Name} ON {Table}")]
 public class CreateIndexOperation : MigrationOperation, ITableMigrationOperation
 {
-    /// <summary>
-    ///     Indicates whether or not the index should enforce uniqueness.
-    /// </summary>
-    public virtual bool IsUnique { get; set; }
+    private string[]? _columns;
+    private bool[]? _isDescending;
 
     /// <summary>
     ///     The name of the index.
@@ -35,7 +33,41 @@ public class CreateIndexOperation : MigrationOperation, ITableMigrationOperation
     /// <summary>
     ///     The ordered list of column names for the column that make up the index.
     /// </summary>
-    public virtual string[] Columns { get; set; } = null!;
+    public virtual string[] Columns
+    {
+        get => _columns!;
+        set
+        {
+            if (_isDescending is not null && value.Length != _isDescending.Length)
+            {
+                throw new ArgumentException(RelationalStrings.CreateIndexOperationWithInvalidSortOrder(_isDescending.Length, value.Length));
+            }
+
+            _columns = value;
+        }
+    }
+
+    /// <summary>
+    ///     Indicates whether or not the index should enforce uniqueness.
+    /// </summary>
+    public virtual bool IsUnique { get; set; }
+
+    /// <summary>
+    ///     A set of values indicating whether each corresponding index column has descending sort order.
+    /// </summary>
+    public virtual bool[]? IsDescending
+    {
+        get => _isDescending;
+        set
+        {
+            if (value is not null && _columns is not null && value.Length != _columns.Length)
+            {
+                throw new ArgumentException(RelationalStrings.CreateIndexOperationWithInvalidSortOrder(value.Length, _columns.Length));
+            }
+
+            _isDescending = value;
+        }
+    }
 
     /// <summary>
     ///     An expression to use as the index filter.
@@ -53,11 +85,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/Properties/RelationalStrings.Designer.cs b/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs
index 7e7db5126aa..496cd075aaa 100644
--- a/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs
+++ b/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs
@@ -151,6 +151,14 @@ public static string ConflictingRowValuesSensitive(object? firstEntityType, obje
                 GetString("ConflictingRowValuesSensitive", nameof(firstEntityType), nameof(secondEntityType), nameof(keyValue), nameof(firstConflictingValue), nameof(secondConflictingValue), nameof(column)),
                 firstEntityType, secondEntityType, keyValue, firstConflictingValue, secondConflictingValue, column);
 
+        /// <summary>
+        ///     {numSortOrderProperties} values were provided in CreateIndexOperations.IsDescending, but the operation has {numColumns} columns.
+        /// </summary>
+        public static string CreateIndexOperationWithInvalidSortOrder(object? numSortOrderProperties, object? numColumns)
+            => string.Format(
+                GetString("CreateIndexOperationWithInvalidSortOrder", nameof(numSortOrderProperties), nameof(numColumns)),
+                numSortOrderProperties, numColumns);
+
         /// <summary>
         ///     There is no property mapped to the column '{table}.{column}' which is used in a data operation. Either add a property mapped to this column, or specify the column types in the data operation.
         /// </summary>
@@ -470,36 +478,44 @@ public static string DuplicateForeignKeyUniquenessMismatch(object? foreignKeyPro
                 foreignKeyProperties1, entityType1, foreignKeyProperties2, entityType2, table, foreignKeyName);
 
         /// <summary>
-        ///     The indexes {indexProperties1} on '{entityType1}' and {indexProperties2} on '{entityType2}' are both mapped to '{table}.{indexName}', but with different columns ({columnNames1} and {columnNames2}).
+        ///     The indexes {index1} on '{entityType1}' and {index2} on '{entityType2}' are both mapped to '{table}.{indexName}', but with different columns ({columnNames1} and {columnNames2}).
+        /// </summary>
+        public static string DuplicateIndexColumnMismatch(object? index1, object? entityType1, object? index2, object? entityType2, object? table, object? indexName, object? columnNames1, object? columnNames2)
+            => string.Format(
+                GetString("DuplicateIndexColumnMismatch", nameof(index1), nameof(entityType1), nameof(index2), nameof(entityType2), nameof(table), nameof(indexName), nameof(columnNames1), nameof(columnNames2)),
+                index1, entityType1, index2, entityType2, table, indexName, columnNames1, columnNames2);
+
+        /// <summary>
+        ///     The indexes {index1} on '{entityType1}' and {index2} on '{entityType2}' are both mapped to '{table}.{indexName}', but with different filters ('{filter1}' and '{filter2}').
         /// </summary>
-        public static string DuplicateIndexColumnMismatch(object? indexProperties1, object? entityType1, object? indexProperties2, object? entityType2, object? table, object? indexName, object? columnNames1, object? columnNames2)
+        public static string DuplicateIndexFiltersMismatch(object? index1, object? entityType1, object? index2, object? entityType2, object? table, object? indexName, object? filter1, object? filter2)
             => string.Format(
-                GetString("DuplicateIndexColumnMismatch", nameof(indexProperties1), nameof(entityType1), nameof(indexProperties2), nameof(entityType2), nameof(table), nameof(indexName), nameof(columnNames1), nameof(columnNames2)),
-                indexProperties1, entityType1, indexProperties2, entityType2, table, indexName, columnNames1, columnNames2);
+                GetString("DuplicateIndexFiltersMismatch", nameof(index1), nameof(entityType1), nameof(index2), nameof(entityType2), nameof(table), nameof(indexName), nameof(filter1), nameof(filter2)),
+                index1, entityType1, index2, entityType2, table, indexName, filter1, filter2);
 
         /// <summary>
-        ///     The indexes {indexProperties1} on '{entityType1}' and {indexProperties2} on '{entityType2}' are both mapped to '{table}.{indexName}', but with different filters ('{filter1}' and '{filter2}').
+        ///     The indexes {index1} on '{entityType1}' and {index2} on '{entityType2}' are both mapped to '{table}.{indexName}', but with different sort orders.
         /// </summary>
-        public static string DuplicateIndexFiltersMismatch(object? indexProperties1, object? entityType1, object? indexProperties2, object? entityType2, object? table, object? indexName, object? filter1, object? filter2)
+        public static string DuplicateIndexSortOrdersMismatch(object? index1, object? entityType1, object? index2, object? entityType2, object? table, object? indexName)
             => string.Format(
-                GetString("DuplicateIndexFiltersMismatch", nameof(indexProperties1), nameof(entityType1), nameof(indexProperties2), nameof(entityType2), nameof(table), nameof(indexName), nameof(filter1), nameof(filter2)),
-                indexProperties1, entityType1, indexProperties2, entityType2, table, indexName, filter1, filter2);
+                GetString("DuplicateIndexSortOrdersMismatch", nameof(index1), nameof(entityType1), nameof(index2), nameof(entityType2), nameof(table), nameof(indexName)),
+                index1, entityType1, index2, entityType2, table, indexName);
 
         /// <summary>
-        ///     The indexes {indexProperties1} on '{entityType1}' and {indexProperties2} on '{entityType2}' are both mapped to '{indexName}', but are declared on different tables ('{table1}' and '{table2}').
+        ///     The indexes {index1} on '{entityType1}' and {index2} on '{entityType2}' are both mapped to '{indexName}', but are declared on different tables ('{table1}' and '{table2}').
         /// </summary>
-        public static string DuplicateIndexTableMismatch(object? indexProperties1, object? entityType1, object? indexProperties2, object? entityType2, object? indexName, object? table1, object? table2)
+        public static string DuplicateIndexTableMismatch(object? index1, object? entityType1, object? index2, object? entityType2, object? indexName, object? table1, object? table2)
             => string.Format(
-                GetString("DuplicateIndexTableMismatch", nameof(indexProperties1), nameof(entityType1), nameof(indexProperties2), nameof(entityType2), nameof(indexName), nameof(table1), nameof(table2)),
-                indexProperties1, entityType1, indexProperties2, entityType2, indexName, table1, table2);
+                GetString("DuplicateIndexTableMismatch", nameof(index1), nameof(entityType1), nameof(index2), nameof(entityType2), nameof(indexName), nameof(table1), nameof(table2)),
+                index1, entityType1, index2, entityType2, indexName, table1, table2);
 
         /// <summary>
-        ///     The indexes {indexProperties1} on '{entityType1}' and {indexProperties2} on '{entityType2}' are both mapped to '{table}.{indexName}', but with different uniqueness configurations.
+        ///     The indexes {index1} on '{entityType1}' and {index2} on '{entityType2}' are both mapped to '{table}.{indexName}', but with different uniqueness configurations.
         /// </summary>
-        public static string DuplicateIndexUniquenessMismatch(object? indexProperties1, object? entityType1, object? indexProperties2, object? entityType2, object? table, object? indexName)
+        public static string DuplicateIndexUniquenessMismatch(object? index1, object? entityType1, object? index2, object? entityType2, object? table, object? indexName)
             => string.Format(
-                GetString("DuplicateIndexUniquenessMismatch", nameof(indexProperties1), nameof(entityType1), nameof(indexProperties2), nameof(entityType2), nameof(table), nameof(indexName)),
-                indexProperties1, entityType1, indexProperties2, entityType2, table, indexName);
+                GetString("DuplicateIndexUniquenessMismatch", nameof(index1), nameof(entityType1), nameof(index2), nameof(entityType2), nameof(table), nameof(indexName)),
+                index1, entityType1, index2, entityType2, table, indexName);
 
         /// <summary>
         ///     The keys {keyProperties1} on '{entityType1}' and {keyProperties2} on '{entityType2}' are both mapped to '{table}.{keyName}', but with different columns ({columnNames1} and {columnNames2}).
diff --git a/src/EFCore.Relational/Properties/RelationalStrings.resx b/src/EFCore.Relational/Properties/RelationalStrings.resx
index 366f1912ce3..19e96b800f6 100644
--- a/src/EFCore.Relational/Properties/RelationalStrings.resx
+++ b/src/EFCore.Relational/Properties/RelationalStrings.resx
@@ -169,6 +169,9 @@
   <data name="ConflictingRowValuesSensitive" xml:space="preserve">
     <value>Instances of entity types '{firstEntityType}' and '{secondEntityType}' are mapped to the same row with the key value '{keyValue}', but have different property values '{firstConflictingValue}' and '{secondConflictingValue}' for the column '{column}'.</value>
   </data>
+  <data name="CreateIndexOperationWithInvalidSortOrder" xml:space="preserve">
+    <value>{numSortOrderProperties} values were provided in CreateIndexOperations.IsDescending, but the operation has {numColumns} columns.</value>
+  </data>
   <data name="DataOperationNoProperty" xml:space="preserve">
     <value>There is no property mapped to the column '{table}.{column}' which is used in a data operation. Either add a property mapped to this column, or specify the column types in the data operation.</value>
   </data>
@@ -290,16 +293,19 @@
     <value>The foreign keys {foreignKeyProperties1} on '{entityType1}' and {foreignKeyProperties2} on '{entityType2}' are both mapped to '{table}.{foreignKeyName}', but with different uniqueness configurations.</value>
   </data>
   <data name="DuplicateIndexColumnMismatch" xml:space="preserve">
-    <value>The indexes {indexProperties1} on '{entityType1}' and {indexProperties2} on '{entityType2}' are both mapped to '{table}.{indexName}', but with different columns ({columnNames1} and {columnNames2}).</value>
+    <value>The indexes {index1} on '{entityType1}' and {index2} on '{entityType2}' are both mapped to '{table}.{indexName}', but with different columns ({columnNames1} and {columnNames2}).</value>
   </data>
   <data name="DuplicateIndexFiltersMismatch" xml:space="preserve">
-    <value>The indexes {indexProperties1} on '{entityType1}' and {indexProperties2} on '{entityType2}' are both mapped to '{table}.{indexName}', but with different filters ('{filter1}' and '{filter2}').</value>
+    <value>The indexes {index1} on '{entityType1}' and {index2} on '{entityType2}' are both mapped to '{table}.{indexName}', but with different filters ('{filter1}' and '{filter2}').</value>
+  </data>
+  <data name="DuplicateIndexSortOrdersMismatch" xml:space="preserve">
+    <value>The indexes {index1} on '{entityType1}' and {index2} on '{entityType2}' are both mapped to '{table}.{indexName}', but with different sort orders.</value>
   </data>
   <data name="DuplicateIndexTableMismatch" xml:space="preserve">
-    <value>The indexes {indexProperties1} on '{entityType1}' and {indexProperties2} on '{entityType2}' are both mapped to '{indexName}', but are declared on different tables ('{table1}' and '{table2}').</value>
+    <value>The indexes {index1} on '{entityType1}' and {index2} on '{entityType2}' are both mapped to '{indexName}', but are declared on different tables ('{table1}' and '{table2}').</value>
   </data>
   <data name="DuplicateIndexUniquenessMismatch" xml:space="preserve">
-    <value>The indexes {indexProperties1} on '{entityType1}' and {indexProperties2} on '{entityType2}' are both mapped to '{table}.{indexName}', but with different uniqueness configurations.</value>
+    <value>The indexes {index1} on '{entityType1}' and {index2} on '{entityType2}' are both mapped to '{table}.{indexName}', but with different uniqueness configurations.</value>
   </data>
   <data name="DuplicateKeyColumnMismatch" xml:space="preserve">
     <value>The keys {keyProperties1} on '{entityType1}' and {keyProperties2} on '{entityType2}' are both mapped to '{table}.{keyName}', but with different columns ({columnNames1} and {columnNames2}).</value>
diff --git a/src/EFCore.Relational/Scaffolding/Metadata/DatabaseIndex.cs b/src/EFCore.Relational/Scaffolding/Metadata/DatabaseIndex.cs
index 92de823af64..57f19fcc024 100644
--- a/src/EFCore.Relational/Scaffolding/Metadata/DatabaseIndex.cs
+++ b/src/EFCore.Relational/Scaffolding/Metadata/DatabaseIndex.cs
@@ -28,10 +28,15 @@ public class DatabaseIndex : Annotatable
     public virtual IList<DatabaseColumn> Columns { get; } = new List<DatabaseColumn>();
 
     /// <summary>
-    ///     Indicates whether or not the index constrains uniqueness.
+    ///     Indicates whether or not the index enforces uniqueness.
     /// </summary>
     public virtual bool IsUnique { get; set; }
 
+    /// <summary>
+    ///     A set of values indicating whether each corresponding index column has descending sort order.
+    /// </summary>
+    public virtual IList<bool> IsDescending { get; set; } = new List<bool>();
+
     /// <summary>
     ///     The filter expression, or <see langword="null" /> if the index has no filter.
     /// </summary>
diff --git a/src/EFCore.SqlServer/Infrastructure/Internal/SqlServerModelValidator.cs b/src/EFCore.SqlServer/Infrastructure/Internal/SqlServerModelValidator.cs
index d4e4f6dff32..3d066e9c478 100644
--- a/src/EFCore.SqlServer/Infrastructure/Internal/SqlServerModelValidator.cs
+++ b/src/EFCore.SqlServer/Infrastructure/Internal/SqlServerModelValidator.cs
@@ -163,7 +163,7 @@ protected virtual void ValidateIndexIncludeProperties(
                     throw new InvalidOperationException(
                         SqlServerStrings.IncludePropertyNotFound(
                             notFound,
-                            index.Name == null ? index.Properties.Format() : "'" + index.Name + "'",
+                            index.DisplayName(),
                             index.DeclaringEntityType.DisplayName()));
                 }
 
@@ -179,7 +179,7 @@ protected virtual void ValidateIndexIncludeProperties(
                         SqlServerStrings.IncludePropertyDuplicated(
                             index.DeclaringEntityType.DisplayName(),
                             duplicateProperty,
-                            index.Name == null ? index.Properties.Format() : "'" + index.Name + "'"));
+                            index.DisplayName()));
                 }
 
                 var coveredProperty = includeProperties
@@ -191,7 +191,7 @@ protected virtual void ValidateIndexIncludeProperties(
                         SqlServerStrings.IncludePropertyInIndex(
                             index.DeclaringEntityType.DisplayName(),
                             coveredProperty,
-                            index.Name == null ? index.Properties.Format() : "'" + index.Name + "'"));
+                            index.DisplayName()));
                 }
             }
         }
diff --git a/src/EFCore.SqlServer/Metadata/Internal/SqlServerIndexExtensions.cs b/src/EFCore.SqlServer/Metadata/Internal/SqlServerIndexExtensions.cs
index ed2791568b5..49f21891482 100644
--- a/src/EFCore.SqlServer/Metadata/Internal/SqlServerIndexExtensions.cs
+++ b/src/EFCore.SqlServer/Metadata/Internal/SqlServerIndexExtensions.cs
@@ -35,9 +35,9 @@ public static bool AreCompatibleForSqlServer(
                 {
                     throw new InvalidOperationException(
                         SqlServerStrings.DuplicateIndexIncludedMismatch(
-                            index.Properties.Format(),
+                            index.DisplayName(),
                             index.DeclaringEntityType.DisplayName(),
-                            duplicateIndex.Properties.Format(),
+                            duplicateIndex.DisplayName(),
                             duplicateIndex.DeclaringEntityType.DisplayName(),
                             index.DeclaringEntityType.GetSchemaQualifiedTableName(),
                             index.GetDatabaseName(storeObject),
@@ -55,9 +55,9 @@ public static bool AreCompatibleForSqlServer(
             {
                 throw new InvalidOperationException(
                     SqlServerStrings.DuplicateIndexOnlineMismatch(
-                        index.Properties.Format(),
+                        index.DisplayName(),
                         index.DeclaringEntityType.DisplayName(),
-                        duplicateIndex.Properties.Format(),
+                        duplicateIndex.DisplayName(),
                         duplicateIndex.DeclaringEntityType.DisplayName(),
                         index.DeclaringEntityType.GetSchemaQualifiedTableName(),
                         index.GetDatabaseName(storeObject)));
@@ -72,9 +72,9 @@ public static bool AreCompatibleForSqlServer(
             {
                 throw new InvalidOperationException(
                     SqlServerStrings.DuplicateIndexClusteredMismatch(
-                        index.Properties.Format(),
+                        index.DisplayName(),
                         index.DeclaringEntityType.DisplayName(),
-                        duplicateIndex.Properties.Format(),
+                        duplicateIndex.DisplayName(),
                         duplicateIndex.DeclaringEntityType.DisplayName(),
                         index.DeclaringEntityType.GetSchemaQualifiedTableName(),
                         index.GetDatabaseName(storeObject)));
@@ -89,9 +89,9 @@ public static bool AreCompatibleForSqlServer(
             {
                 throw new InvalidOperationException(
                     SqlServerStrings.DuplicateIndexFillFactorMismatch(
-                        index.Properties.Format(),
+                        index.DisplayName(),
                         index.DeclaringEntityType.DisplayName(),
-                        duplicateIndex.Properties.Format(),
+                        duplicateIndex.DisplayName(),
                         duplicateIndex.DeclaringEntityType.DisplayName(),
                         index.DeclaringEntityType.GetSchemaQualifiedTableName(),
                         index.GetDatabaseName(storeObject)));
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<DatabaseTable> 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<bool>("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..93d7dafa6f8 100644
--- a/src/EFCore/Metadata/Builders/IConventionIndexBuilder.cs
+++ b/src/EFCore/Metadata/Builders/IConventionIndexBuilder.cs
@@ -27,18 +27,30 @@ public interface IConventionIndexBuilder : IConventionAnnotatableBuilder
     /// </summary>
     /// <param name="unique">A value indicating whether the index is unique.</param>
     /// <param name="fromDataAnnotation">Indicates whether the configuration was specified using a data annotation.</param>
-    /// <returns>
-    ///     The same builder instance if the uniqueness was configured,
-    ///     <see langword="null" /> otherwise.
-    /// </returns>
+    /// <returns>The same builder instance if the uniqueness was configured, <see langword="null" /> otherwise.</returns>
     IConventionIndexBuilder? IsUnique(bool? unique, bool fromDataAnnotation = false);
 
     /// <summary>
-    ///     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.
     /// </summary>
     /// <param name="unique">A value indicating whether the index is unique.</param>
     /// <param name="fromDataAnnotation">Indicates whether the configuration was specified using a data annotation.</param>
     /// <returns><see langword="true" /> if the index uniqueness can be configured.</returns>
     bool CanSetIsUnique(bool? unique, bool fromDataAnnotation = false);
+
+    /// <summary>
+    ///     Configures the sort order(s) for the columns of this index (ascending or descending).
+    /// </summary>
+    /// <param name="descending">A set of values indicating whether each corresponding index column has descending sort order.</param>
+    /// <param name="fromDataAnnotation">Indicates whether the configuration was specified using a data annotation.</param>
+    /// <returns>The same builder instance if the uniqueness was configured, <see langword="null" /> otherwise.</returns>
+    IConventionIndexBuilder? IsDescending(IReadOnlyList<bool>? descending, bool fromDataAnnotation = false);
+
+    /// <summary>
+    ///     Returns a value indicating whether this index sort order can be configured from the current configuration source.
+    /// </summary>
+    /// <param name="descending">A set of values indicating whether each corresponding index column has descending sort order.</param>
+    /// <param name="fromDataAnnotation">Indicates whether the configuration was specified using a data annotation.</param>
+    /// <returns><see langword="true" /> if the index uniqueness can be configured.</returns>
+    bool CanSetIsDescending(IReadOnlyList<bool>? descending, bool fromDataAnnotation = false);
 }
diff --git a/src/EFCore/Metadata/Builders/IndexBuilder.cs b/src/EFCore/Metadata/Builders/IndexBuilder.cs
index 9f3f1e899cc..71779f59636 100644
--- a/src/EFCore/Metadata/Builders/IndexBuilder.cs
+++ b/src/EFCore/Metadata/Builders/IndexBuilder.cs
@@ -77,6 +77,18 @@ public virtual IndexBuilder IsUnique(bool unique = true)
         return this;
     }
 
+    /// <summary>
+    ///     Configures the sort order(s) for the columns of this index (ascending or descending).
+    /// </summary>
+    /// <param name="descending">A set of values indicating whether each corresponding index column has descending sort order.</param>
+    /// <returns>The same builder instance so that multiple configuration calls can be chained.</returns>
+    public virtual IndexBuilder IsDescending(params bool[] descending)
+    {
+        Builder.IsDescending(descending, ConfigurationSource.Explicit);
+
+        return this;
+    }
+
     #region Hidden System.Object members
 
     /// <summary>
diff --git a/src/EFCore/Metadata/Builders/IndexBuilder`.cs b/src/EFCore/Metadata/Builders/IndexBuilder`.cs
index 8bfc61e74b1..e8f36809e9e 100644
--- a/src/EFCore/Metadata/Builders/IndexBuilder`.cs
+++ b/src/EFCore/Metadata/Builders/IndexBuilder`.cs
@@ -49,4 +49,12 @@ public IndexBuilder(IMutableIndex index)
     /// <returns>The same builder instance so that multiple configuration calls can be chained.</returns>
     public new virtual IndexBuilder<T> IsUnique(bool unique = true)
         => (IndexBuilder<T>)base.IsUnique(unique);
+
+    /// <summary>
+    ///     Configures the sort order(s) for the columns of this index (ascending or descending).
+    /// </summary>
+    /// <param name="descending">A set of values indicating whether each corresponding index column has descending sort order.</param>
+    /// <returns>The same builder instance so that multiple configuration calls can be chained.</returns>
+    public new virtual IndexBuilder<T> IsDescending(params bool[] descending)
+        => (IndexBuilder<T>)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<IIndexUniquenessChangedConvention> IndexUniquenessChangedConventions { get; }
         = new List<IIndexUniquenessChangedConvention>();
 
+    /// <summary>
+    ///     Conventions to run when the sort order of an index is changed.
+    /// </summary>
+    public virtual IList<IIndexSortOrderChangedConvention> IndexSortOrderChangedConventions { get; }
+        = new List<IIndexSortOrderChangedConvention>();
+
     /// <summary>
     ///     Conventions to run when an annotation is changed on an index.
     /// </summary>
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;
+
+/// <summary>
+///     Represents an operation that should be performed when the sort order of an index is changed.
+/// </summary>
+/// <remarks>
+///     See <see href="https://aka.ms/efcore-docs-conventions">Model building conventions</see> for more information and examples.
+/// </remarks>
+public interface IIndexSortOrderChangedConvention : IConvention
+{
+    /// <summary>
+    ///     Called after the uniqueness for an index is changed.
+    /// </summary>
+    /// <param name="indexBuilder">The builder for the index.</param>
+    /// <param name="context">Additional information associated with convention execution.</param>
+    void ProcessIndexSortOrderChanged(
+        IConventionIndexBuilder indexBuilder,
+        IConventionContext<IReadOnlyList<bool>?> context);
+}
diff --git a/src/EFCore/Metadata/Conventions/IndexAttributeConvention.cs b/src/EFCore/Metadata/Conventions/IndexAttributeConvention.cs
index 0f8f1cef2f6..52831af95ca 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 = indexBuilder.IsUnique(indexAttribute.IsUnique, fromDataAnnotation: true);
+                }
+
+                if (indexBuilder is not null && indexAttribute.IsDescending is not null)
+                {
+                    indexBuilder = 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<bool>? 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<bool>? 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..68ec5094ed8 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<string> _stringConventionContext;
         private readonly ConventionContext<FieldInfo> _fieldInfoConventionContext;
         private readonly ConventionContext<bool?> _boolConventionContext;
+        private readonly ConventionContext<IReadOnlyList<bool>?> _boolListConventionContext;
 
         public ImmediateConventionScope(ConventionSet conventionSet, ConventionDispatcher dispatcher)
         {
@@ -54,6 +55,7 @@ public ImmediateConventionScope(ConventionSet conventionSet, ConventionDispatche
             _stringConventionContext = new ConventionContext<string>(dispatcher);
             _fieldInfoConventionContext = new ConventionContext<FieldInfo>(dispatcher);
             _boolConventionContext = new ConventionContext<bool?>(dispatcher);
+            _boolListConventionContext = new ConventionContext<IReadOnlyList<bool>?>(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<bool>? 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 (_boolListConventionContext.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);
 
+    /// <summary>
+    ///     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.
+    /// </summary>
+    public virtual IReadOnlyList<bool>? OnIndexSortOrderChanged(IConventionIndexBuilder indexBuilder)
+        => _scope.OnIndexSortOrderChanged(indexBuilder);
+
     /// <summary>
     ///     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..ccad73d3b7e 100644
--- a/src/EFCore/Metadata/IConventionIndex.cs
+++ b/src/EFCore/Metadata/IConventionIndex.cs
@@ -54,4 +54,18 @@ public interface IConventionIndex : IReadOnlyIndex, IConventionAnnotatable
     /// </summary>
     /// <returns>The configuration source for <see cref="IReadOnlyIndex.IsUnique" />.</returns>
     ConfigurationSource? GetIsUniqueConfigurationSource();
+
+    /// <summary>
+    ///     Sets the sort order(s) for this index (ascending or descending).
+    /// </summary>
+    /// <param name="descending">A set of values indicating whether each corresponding index column has descending sort order.</param>
+    /// <param name="fromDataAnnotation">Indicates whether the configuration was specified using a data annotation.</param>
+    /// <returns>The configured sort order(s).</returns>
+    IReadOnlyList<bool>? SetIsDescending(IReadOnlyList<bool>? descending, bool fromDataAnnotation = false);
+
+    /// <summary>
+    ///     Returns the configuration source for <see cref="IReadOnlyIndex.IsDescending" />.
+    /// </summary>
+    /// <returns>The configuration source for <see cref="IReadOnlyIndex.IsDescending" />.</returns>
+    ConfigurationSource? GetIsDescendingConfigurationSource();
 }
diff --git a/src/EFCore/Metadata/IMutableIndex.cs b/src/EFCore/Metadata/IMutableIndex.cs
index c1270b4995e..bd77f85679f 100644
--- a/src/EFCore/Metadata/IMutableIndex.cs
+++ b/src/EFCore/Metadata/IMutableIndex.cs
@@ -23,6 +23,11 @@ public interface IMutableIndex : IReadOnlyIndex, IMutableAnnotatable
     /// </summary>
     new bool IsUnique { get; set; }
 
+    /// <summary>
+    ///     A set of values indicating whether each corresponding index column has descending sort order.
+    /// </summary>
+    new IReadOnlyList<bool>? IsDescending { get; set; }
+
     /// <summary>
     ///     Gets the properties that this index is defined on.
     /// </summary>
diff --git a/src/EFCore/Metadata/IReadOnlyIndex.cs b/src/EFCore/Metadata/IReadOnlyIndex.cs
index 6bcb12dbecf..f7ab17a9a4a 100644
--- a/src/EFCore/Metadata/IReadOnlyIndex.cs
+++ b/src/EFCore/Metadata/IReadOnlyIndex.cs
@@ -28,6 +28,11 @@ public interface IReadOnlyIndex : IReadOnlyAnnotatable
     /// </summary>
     bool IsUnique { get; }
 
+    /// <summary>
+    ///     A set of values indicating whether each corresponding index column has descending sort order.
+    /// </summary>
+    IReadOnlyList<bool>? IsDescending { get; }
+
     /// <summary>
     ///     Gets the entity type the index is defined on. This may be different from the type that <see cref="Properties" />
     ///     are defined on when the index is defined a derived type in an inheritance hierarchy (since the properties
@@ -35,6 +40,15 @@ public interface IReadOnlyIndex : IReadOnlyAnnotatable
     /// </summary>
     IReadOnlyEntityType DeclaringEntityType { get; }
 
+    /// <summary>
+    ///     Gets the friendly display name for the given <see cref="IReadOnlyIndex" />, returning its <see cref="Name" /> if one is defined,
+    ///     or a string representation of its <see cref="Properties" /> if this is an unnamed index.
+    /// </summary>
+    /// <returns>The display name.</returns>
+    [DebuggerStepThrough]
+    string DisplayName()
+        => Name is null ? Properties.Format() : $"'{Name}'";
+
     /// <summary>
     ///     <para>
     ///         Creates a human-readable representation of the given metadata.
diff --git a/src/EFCore/Metadata/Internal/EntityType.cs b/src/EFCore/Metadata/Internal/EntityType.cs
index 00e68d5f620..e74bbe1fcd6 100644
--- a/src/EFCore/Metadata/Internal/EntityType.cs
+++ b/src/EFCore/Metadata/Internal/EntityType.cs
@@ -2249,7 +2249,7 @@ public virtual IEnumerable<Index> FindIndexesInHierarchy(string name)
             if (!_unnamedIndexes.Remove(index.Properties))
             {
                 throw new InvalidOperationException(
-                    CoreStrings.IndexWrongType(index.Properties.Format(), DisplayName(), index.DeclaringEntityType.DisplayName()));
+                    CoreStrings.IndexWrongType(index.DisplayName(), DisplayName(), index.DeclaringEntityType.DisplayName()));
             }
         }
         else
@@ -2618,7 +2618,7 @@ private void CheckPropertyNotInUse(Property property)
             throw new InvalidOperationException(
                 CoreStrings.PropertyInUseIndex(
                     property.Name, DisplayName(),
-                    containingIndex.Properties.Format(), containingIndex.DeclaringEntityType.DisplayName()));
+                    containingIndex.DisplayName(), containingIndex.DeclaringEntityType.DisplayName()));
         }
     }
 
diff --git a/src/EFCore/Metadata/Internal/Index.cs b/src/EFCore/Metadata/Internal/Index.cs
index f4a2bf54d69..43c887c43b4 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<bool>? _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;
 
     /// <summary>
     ///     This is an internal API that supports the Entity Framework Core infrastructure and not subject to
@@ -193,6 +196,70 @@ private static bool DefaultIsUnique
     private void UpdateIsUniqueConfigurationSource(ConfigurationSource configurationSource)
         => _isUniqueConfigurationSource = configurationSource.Max(_isUniqueConfigurationSource);
 
+    /// <summary>
+    ///     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.
+    /// </summary>
+    public virtual IReadOnlyList<bool>? IsDescending
+    {
+        get => _isDescending ?? DefaultIsDescending;
+        set => SetIsDescending(value, ConfigurationSource.Explicit);
+    }
+
+    /// <summary>
+    ///     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.
+    /// </summary>
+    public virtual IReadOnlyList<bool>? SetIsDescending(IReadOnlyList<bool>? descending, ConfigurationSource configurationSource)
+    {
+        EnsureMutable();
+
+        if (descending is not null && descending.Count != Properties.Count)
+        {
+            throw new ArgumentException(
+                CoreStrings.InvalidNumberOfIndexSortOrderValues(DisplayName(), descending.Count, Properties.Count), nameof(descending));
+        }
+
+        var oldIsDescending = IsDescending;
+        var isChanging =
+            (_isDescending is null && descending is not null && descending.Any(desc => desc))
+            || (descending is null && _isDescending is not null && _isDescending.Any(desc => desc))
+            || (descending is not null && oldIsDescending 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
+        = null;
+
+    /// <summary>
+    ///     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.
+    /// </summary>
+    public virtual ConfigurationSource? GetIsDescendingConfigurationSource()
+        => _isDescendingConfigurationSource;
+
+    private void UpdateIsDescendingConfigurationSource(ConfigurationSource configurationSource)
+        => _isDescendingConfigurationSource = configurationSource.Max(_isDescendingConfigurationSource);
+
     /// <summary>
     ///     Runs the conventions when an annotation was set or removed.
     /// </summary>
@@ -240,6 +307,16 @@ public virtual DebugView DebugView
             () => ((IIndex)this).ToDebugString(),
             () => ((IIndex)this).ToDebugString(MetadataDebugStringOptions.LongDefault));
 
+    /// <summary>
+    ///     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.
+    /// </summary>
+    [DebuggerStepThrough]
+    public virtual string DisplayName()
+        => Name is null ? Properties.Format() : $"'{Name}'";
+
     /// <summary>
     ///     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
@@ -369,4 +446,15 @@ IEntityType IIndex.DeclaringEntityType
     [DebuggerStepThrough]
     bool? IConventionIndex.SetIsUnique(bool? unique, bool fromDataAnnotation)
         => SetIsUnique(unique, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention);
+
+    /// <summary>
+    ///     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.
+    /// </summary>
+    [DebuggerStepThrough]
+    IReadOnlyList<bool>? IConventionIndex.SetIsDescending(IReadOnlyList<bool>? descending, bool fromDataAnnotation)
+        => SetIsDescending(descending, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention);
+
 }
diff --git a/src/EFCore/Metadata/Internal/InternalIndexBuilder.cs b/src/EFCore/Metadata/Internal/InternalIndexBuilder.cs
index 55ff70cd7ac..f55646fa897 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());
 
+    /// <summary>
+    ///     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.
+    /// </summary>
+    public virtual InternalIndexBuilder? IsDescending(IReadOnlyList<bool>? descending, ConfigurationSource configurationSource)
+    {
+        if (!CanSetIsDescending(descending, configurationSource))
+        {
+            return null;
+        }
+
+        Metadata.SetIsDescending(descending, configurationSource);
+        return this;
+    }
+
+    /// <summary>
+    ///     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.
+    /// </summary>
+    public virtual bool CanSetIsDescending(IReadOnlyList<bool>? descending, ConfigurationSource? configurationSource)
+        => descending is null && Metadata.IsDescending is null
+            || descending is not null && Metadata.IsDescending is not null && Metadata.IsDescending.SequenceEqual(descending)
+            || configurationSource.Overrides(Metadata.GetIsDescendingConfigurationSource());
+
     /// <summary>
     ///     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);
+
+    /// <summary>
+    ///     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.
+    /// </summary>
+    IConventionIndexBuilder? IConventionIndexBuilder.IsDescending(IReadOnlyList<bool>? descending, bool fromDataAnnotation)
+        => IsDescending(
+            descending,
+            fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention);
+
+    /// <summary>
+    ///     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.
+    /// </summary>
+    bool IConventionIndexBuilder.CanSetIsDescending(IReadOnlyList<bool>? 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..c5ecc2c41e3 100644
--- a/src/EFCore/Metadata/RuntimeIndex.cs
+++ b/src/EFCore/Metadata/RuntimeIndex.cs
@@ -55,6 +55,15 @@ public RuntimeIndex(
     /// </summary>
     public virtual RuntimeEntityType DeclaringEntityType { get; }
 
+    /// <summary>
+    ///     Always returns an empty array for <see cref="RuntimeIndex" />.
+    /// </summary>
+    IReadOnlyList<bool> IReadOnlyIndex.IsDescending
+    {
+        [DebuggerStepThrough]
+        get => throw new InvalidOperationException(CoreStrings.RuntimeModelMissingData);
+    }
+
     /// <summary>
     ///     Returns a string that represents the current object.
     /// </summary>
diff --git a/src/EFCore/Properties/CoreStrings.Designer.cs b/src/EFCore/Properties/CoreStrings.Designer.cs
index ecebccc29e4..9e1024ec553 100644
--- a/src/EFCore/Properties/CoreStrings.Designer.cs
+++ b/src/EFCore/Properties/CoreStrings.Designer.cs
@@ -1118,12 +1118,12 @@ public static string IndexPropertiesWrongEntity(object? indexProperties, object?
                 indexProperties, entityType);
 
         /// <summary>
-        ///     The index {indexProperties} cannot be removed from the entity type '{entityType}' because it is defined on the entity type '{otherEntityType}'.
+        ///     The index {index} cannot be removed from the entity type '{entityType}' because it is defined on the entity type '{otherEntityType}'.
         /// </summary>
-        public static string IndexWrongType(object? indexProperties, object? entityType, object? otherEntityType)
+        public static string IndexWrongType(object? index, object? entityType, object? otherEntityType)
             => string.Format(
-                GetString("IndexWrongType", nameof(indexProperties), nameof(entityType), nameof(otherEntityType)),
-                indexProperties, entityType, otherEntityType);
+                GetString("IndexWrongType", nameof(index), nameof(entityType), nameof(otherEntityType)),
+                index, entityType, otherEntityType);
 
         /// <summary>
         ///     The property '{property}' cannot be ignored on entity type '{entityType}' because it's declared on the base entity type '{baseEntityType}'. To exclude this property from your model, use the [NotMapped] attribute or 'Ignore' on the base type in 'OnModelCreating'.
@@ -1219,6 +1219,14 @@ public static string InvalidNavigationWithInverseProperty(object? property, obje
                 GetString("InvalidNavigationWithInverseProperty", "0_property", "1_entityType", nameof(referencedProperty), nameof(referencedEntityType)),
                 property, entityType, referencedProperty, referencedEntityType);
 
+        /// <summary>
+        ///     Invalid number of index sort order values provided for {indexProperties}: {numValues} values were provided, but the index has {numProperties} properties.
+        /// </summary>
+        public static string InvalidNumberOfIndexSortOrderValues(object? indexProperties, object? numValues, object? numProperties)
+            => string.Format(
+                GetString("InvalidNumberOfIndexSortOrderValues", nameof(indexProperties), nameof(numValues), nameof(numProperties)),
+                indexProperties, numValues, numProperties);
+
         /// <summary>
         ///     The specified poolSize must be greater than 0.
         /// </summary>
diff --git a/src/EFCore/Properties/CoreStrings.resx b/src/EFCore/Properties/CoreStrings.resx
index e2872bc118d..4578370aa47 100644
--- a/src/EFCore/Properties/CoreStrings.resx
+++ b/src/EFCore/Properties/CoreStrings.resx
@@ -536,7 +536,7 @@
     <value>The specified index properties {indexProperties} are not declared on the entity type '{entityType}'. Ensure that index properties are declared on the target entity type.</value>
   </data>
   <data name="IndexWrongType" xml:space="preserve">
-    <value>The index {indexProperties} cannot be removed from the entity type '{entityType}' because it is defined on the entity type '{otherEntityType}'.</value>
+    <value>The index {index} cannot be removed from the entity type '{entityType}' because it is defined on the entity type '{otherEntityType}'.</value>
   </data>
   <data name="InheritedPropertyCannotBeIgnored" xml:space="preserve">
     <value>The property '{property}' cannot be ignored on entity type '{entityType}' because it's declared on the base entity type '{baseEntityType}'. To exclude this property from your model, use the [NotMapped] attribute or 'Ignore' on the base type in 'OnModelCreating'.</value>
@@ -574,6 +574,9 @@
   <data name="InvalidNavigationWithInverseProperty" xml:space="preserve">
     <value>The [InverseProperty] attribute on property '{1_entityType}.{0_property}' is not valid. The property '{referencedProperty}' is not a valid navigation on the related type '{referencedEntityType}'. Ensure that the property exists and is a valid reference or collection navigation.</value>
   </data>
+  <data name="InvalidNumberOfIndexSortOrderValues" xml:space="preserve">
+    <value>Invalid number of index sort order values provided for {indexProperties}: {numValues} values were provided, but the index has {numProperties} properties.</value>
+  </data>
   <data name="InvalidPoolSize" xml:space="preserve">
     <value>The specified poolSize must be greater than 0.</value>
   </data>


Test patch:
diff --git a/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationOperationGeneratorTest.cs b/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationOperationGeneratorTest.cs
index 22778a022c3..9bae3036bc4 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.Null(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..b6e790757ae 100644
--- a/test/EFCore.Design.Tests/Migrations/ModelSnapshotSqlServerTest.cs
+++ b/test/EFCore.Design.Tests/Migrations/ModelSnapshotSqlServerTest.cs
@@ -164,6 +164,14 @@ private class EntityWithGenericProperty<TProperty>
         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,76 @@ 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<EntityWithThreeProperties>(
+                    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<int>(""Id"")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType(""int"");
+
+                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>(""Id""), 1L, 1);
+
+                    b.Property<int>(""X"")
+                        .HasColumnType(""int"");
+
+                    b.Property<int>(""Y"")
+                        .HasColumnType(""int"");
+
+                    b.Property<int>(""Z"")
+                        .HasColumnType(""int"");
+
+                    b.HasKey(""Id"");
+
+                    b.HasIndex(new[] { ""X"", ""Y"", ""Z"" }, ""IX_all_ascending"")
+                        .IsDescending(false, false, false);
+
+                    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.Null(emptyIndex.IsDescending);
+
+                var allAscendingIndex = Assert.Single(entityType.GetIndexes(), i => i.Name == "IX_all_ascending");
+                Assert.Equal(new[] { false, false, false}, 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..33ed76d34e6 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<int>("C");
                             x.HasKey("Id");
                             x.HasIndex(new[] { "A", "B" }, "IndexOnAAndB")
-                                .IsUnique();
+                                .IsUnique()
+                                .IsDescending(false, true);
                             x.HasIndex(new[] { "B", "C" }, "IndexOnBAndC")
                                 .HasFilter("Filter SQL")
                                 .HasAnnotation("AnnotationName", "AnnotationValue");
@@ -598,7 +599,8 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
             modelBuilder.Entity<EntityWithIndexes>(entity =>
             {
                 entity.HasIndex(e => new { e.A, e.B }, ""IndexOnAAndB"")
-                    .IsUnique();
+                    .IsUnique()
+                    .IsDescending(false, true);
 
                 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<int>("C");
                             x.HasKey("Id");
                             x.HasIndex(new[] { "A", "B" }, "IndexOnAAndB")
-                                .IsUnique();
+                                .IsUnique()
+                                .IsDescending(false, true);
                             x.HasIndex(new[] { "B", "C" }, "IndexOnBAndC")
                                 .HasFilter("Filter SQL")
                                 .HasAnnotation("AnnotationName", "AnnotationValue");
@@ -696,6 +699,107 @@ 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<int>("Id");
+                            x.Property<int>("X");
+                            x.Property<int>("Y");
+                            x.Property<int>("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<TestDbContext> options)
+            : base(options)
+        {
+        }
+
+        public virtual DbSet<EntityWithIndexes> 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<EntityWithIndexes>(entity =>
+            {
+                entity.HasIndex(e => new { e.X, e.Y, e.Z }, ""IX_all_ascending"")
+                    .IsDescending(false, false, false);
+
+                entity.HasIndex(e => new { e.X, e.Y, e.Z }, ""IX_all_descending"")
+                    .IsDescending(true, true, true);
+
+                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, false);
+
+                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.Null(emptyIndex.IsDescending);
+
+                    var allAscendingIndex = Assert.Single(entityType.GetIndexes(), i => i.Name == "IX_all_ascending");
+                    Assert.Equal(new[] { false, false, false }, 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 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..b14b813c5e0 100644
--- a/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpEntityTypeGeneratorTest.cs
+++ b/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpEntityTypeGeneratorTest.cs
@@ -247,7 +247,7 @@ public partial class Vista
             });
 
     [ConditionalFact]
-    public void IndexAttribute_is_generated_for_multiple_indexes_with_name_unique()
+    public void IndexAttribute_is_generated_for_multiple_indexes_with_name_unique_descending()
         => Test(
             modelBuilder => modelBuilder
                 .Entity(
@@ -260,7 +260,8 @@ public void IndexAttribute_is_generated_for_multiple_indexes_with_name_unique()
                         x.Property<int>("C");
                         x.HasKey("Id");
                         x.HasIndex(new[] { "A", "B" }, "IndexOnAAndB")
-                            .IsUnique();
+                            .IsUnique()
+                            .IsDescending(true, false);
                         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, false })]
     [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..6cf32d7cebf 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.Null(emptyIndex.IsDescending);
+
+        var allAscendingIndex = Assert.Single(entityType.GetIndexes(), i => i.Name == "IX_all_ascending");
+        Assert.Null(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..8e8604dd05f 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,88 @@ 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<int>("Id");
+                    e.Property<int>("X");
+                }),
+            builder => { },
+            builder => builder.Entity("People").HasIndex("X").IsDescending(true),
+            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<int>("Id");
+                    e.Property<int>("X");
+                    e.Property<int>("Y");
+                    e.Property<int>("Z");
+                }),
+            builder => { },
+            builder => builder.Entity("People")
+                .HasIndex("X", "Y", "Z")
+                .IsDescending(false, true, false),
+            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 Alter_index_make_unique()
+        => Test(
+            builder => builder.Entity(
+                "People", e =>
+                {
+                    e.Property<int>("Id");
+                    e.Property<int>("X");
+                }),
+            builder => builder.Entity("People").HasIndex("X"),
+            builder => builder.Entity("People").HasIndex("X").IsUnique(),
+            model =>
+            {
+                var table = Assert.Single(model.Tables);
+                var index = Assert.Single(table.Indexes);
+                Assert.True(index.IsUnique);
+            });
+
+    [ConditionalFact]
+    public virtual Task Alter_index_change_sort_order()
+        => Test(
+            builder => builder.Entity(
+                "People", e =>
+                {
+                    e.Property<int>("Id");
+                    e.Property<int>("X");
+                    e.Property<int>("Y");
+                    e.Property<int>("Z");
+                }),
+            builder => builder.Entity("People")
+                .HasIndex("X", "Y", "Z")
+                .IsDescending(true, false, true),
+            builder => builder.Entity("People")
+                .HasIndex("X", "Y", "Z")
+                .IsDescending(false, true, false),
+            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.Relational.Tests/Infrastructure/RelationalModelValidatorTest.cs b/test/EFCore.Relational.Tests/Infrastructure/RelationalModelValidatorTest.cs
index 754c5b88990..b50fdd57f5c 100644
--- a/test/EFCore.Relational.Tests/Infrastructure/RelationalModelValidatorTest.cs
+++ b/test/EFCore.Relational.Tests/Infrastructure/RelationalModelValidatorTest.cs
@@ -1308,6 +1308,24 @@ public virtual void Detects_duplicate_index_names_within_hierarchy_with_differen
             modelBuilder);
     }
 
+    [ConditionalFact]
+    public virtual void Detects_duplicate_index_names_within_hierarchy_with_different_sort_orders()
+    {
+        var modelBuilder = CreateConventionalModelBuilder();
+        modelBuilder.Entity<Animal>();
+        modelBuilder.Entity<Cat>().HasIndex(c => c.Name).HasDatabaseName("IX_Animal_Name")
+            .IsDescending(true);
+        modelBuilder.Entity<Dog>().HasIndex(d => d.Name).HasDatabaseName("IX_Animal_Name")
+            .IsDescending(false);
+
+        VerifyError(
+            RelationalStrings.DuplicateIndexSortOrdersMismatch(
+                "{'" + nameof(Dog.Name) + "'}", nameof(Dog),
+                "{'" + nameof(Cat.Name) + "'}", nameof(Cat),
+                nameof(Animal), "IX_Animal_Name"),
+            modelBuilder);
+    }
+
     [ConditionalFact]
     public virtual void Detects_duplicate_index_names_within_hierarchy_with_different_filters()
     {
diff --git a/test/EFCore.Relational.Tests/Migrations/Operations/CreateIndexOperationTest.cs b/test/EFCore.Relational.Tests/Migrations/Operations/CreateIndexOperationTest.cs
new file mode 100644
index 00000000000..c1ea868be05
--- /dev/null
+++ b/test/EFCore.Relational.Tests/Migrations/Operations/CreateIndexOperationTest.cs
@@ -0,0 +1,21 @@
+// 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.Migrations.Operations;
+
+public class CreateIndexOperationTest
+{
+    [Fact]
+    public void IsDescending_count_matches_column_count()
+    {
+        var operation = new CreateIndexOperation();
+
+        operation.IsDescending = new[] { true };
+        Assert.Throws<ArgumentException>(() => operation.Columns = new[] { "X", "Y" });
+
+        operation.IsDescending = null;
+
+        operation.Columns = new[] { "X", "Y" };
+        Assert.Throws<ArgumentException>(() => operation.IsDescending = new[] { true });
+    }
+}
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..cfbeec406c4 100644
--- a/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsSqlServerTest.cs
+++ b/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsSqlServerTest.cs
@@ -19,7 +19,7 @@ public MigrationsSqlServerTest(MigrationsSqlServerFixture fixture, ITestOutputHe
         : base(fixture)
     {
         Fixture.TestSqlLoggerFactory.Clear();
-        // Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper);
+        //Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper);
     }
 
     public override async Task Create_table()
@@ -1246,6 +1246,42 @@ 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 Alter_index_make_unique()
+    {
+        await base.Alter_index_make_unique();
+
+        AssertSql(
+            @"DROP INDEX [IX_People_X] ON [People];",
+            //
+            @"CREATE UNIQUE INDEX [IX_People_X] ON [People] ([X]);");
+    }
+
+    public override async Task Alter_index_change_sort_order()
+    {
+        await base.Alter_index_change_sort_order();
+
+        AssertSql(
+            @"DROP INDEX [IX_People_X_Y_Z] ON [People];",
+            //
+            @"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/ConventionDispatcherTest.cs b/test/EFCore.Tests/Metadata/Conventions/ConventionDispatcherTest.cs
index b25f94fd245..2e83815ef7f 100644
--- a/test/EFCore.Tests/Metadata/Conventions/ConventionDispatcherTest.cs
+++ b/test/EFCore.Tests/Metadata/Conventions/ConventionDispatcherTest.cs
@@ -2983,6 +2983,104 @@ public void ProcessIndexUniquenessChanged(
         }
     }
 
+#nullable enable
+    [InlineData(false, false)]
+    [InlineData(true, false)]
+    [InlineData(false, true)]
+    [InlineData(true, true)]
+    [ConditionalTheory]
+    public void OnIndexSortOrderChanged_calls_conventions_in_order(bool useBuilder, bool useScope)
+    {
+        var conventions = new ConventionSet();
+
+        var convention1 = new IndexSortOrderChangedConvention(terminate: false);
+        var convention2 = new IndexSortOrderChangedConvention(terminate: true);
+        var convention3 = new IndexSortOrderChangedConvention(terminate: false);
+        conventions.IndexSortOrderChangedConventions.Add(convention1);
+        conventions.IndexSortOrderChangedConventions.Add(convention2);
+        conventions.IndexSortOrderChangedConventions.Add(convention3);
+
+        var builder = new InternalModelBuilder(new Model(conventions));
+        var entityBuilder = builder.Entity(typeof(Order), ConfigurationSource.Convention)!;
+        var index = entityBuilder.HasIndex(new List<string> { "OrderId" }, ConfigurationSource.Convention)!.Metadata;
+
+        var scope = useScope ? builder.Metadata.ConventionDispatcher.DelayConventions() : null;
+
+        if (useBuilder)
+        {
+            index.Builder.IsDescending(new[] { true }, ConfigurationSource.Convention);
+        }
+        else
+        {
+            index.IsDescending = new[] { true };
+        }
+
+        if (useScope)
+        {
+            Assert.Empty(convention1.Calls);
+            Assert.Empty(convention2.Calls);
+            scope!.Dispose();
+        }
+
+        Assert.Equal(new[] { new[] { true } }, convention1.Calls);
+        Assert.Equal(new[] { new[] { true } }, convention2.Calls);
+        Assert.Empty(convention3.Calls);
+
+        if (useBuilder)
+        {
+            index.Builder.IsDescending(new[] { true }, ConfigurationSource.Convention);
+        }
+        else
+        {
+            index.IsDescending = new[] { true };
+        }
+
+        Assert.Equal(new[] { new[] { true } }, convention1.Calls);
+        Assert.Equal(new[] { new[] { true } }, convention2.Calls);
+        Assert.Empty(convention3.Calls);
+
+        if (useBuilder)
+        {
+            index.Builder.IsDescending(new[] { false }, ConfigurationSource.Convention);
+        }
+        else
+        {
+            index.IsDescending = new[] { false };
+        }
+
+        Assert.Equal(new[] { new[] { true }, new[] { false } }, convention1.Calls);
+        Assert.Equal(new[] { new[] { true }, new[] { false } }, convention2.Calls);
+        Assert.Empty(convention3.Calls);
+
+        Assert.Same(index, entityBuilder.Metadata.RemoveIndex(index.Properties));
+    }
+
+    private class IndexSortOrderChangedConvention : IIndexSortOrderChangedConvention
+    {
+        private readonly bool _terminate;
+        public readonly List<IReadOnlyList<bool>?> Calls = new();
+
+        public IndexSortOrderChangedConvention(bool terminate)
+        {
+            _terminate = terminate;
+        }
+
+        public void ProcessIndexSortOrderChanged(
+            IConventionIndexBuilder indexBuilder,
+            IConventionContext<IReadOnlyList<bool>?> context)
+        {
+            Assert.NotNull(indexBuilder.Metadata.Builder);
+
+            Calls.Add(indexBuilder.Metadata.IsDescending);
+
+            if (_terminate)
+            {
+                context.StopProcessing();
+            }
+        }
+    }
+#nullable restore
+
     [InlineData(false, false)]
     [InlineData(true, false)]
     [InlineData(false, true)]
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<string> { 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<EntityWithIndex>();
 
         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<ProviderConventionSetBuilderDependencies>();
 
-    [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<TEntity> HasAnnotation(string annotation, objec
         public override TestIndexBuilder<TEntity> IsUnique(bool isUnique = true)
             => new GenericTestIndexBuilder<TEntity>(IndexBuilder.IsUnique(isUnique));
 
+        public override TestIndexBuilder<TEntity> IsDescending(params bool[] isDescending)
+            => new GenericTestIndexBuilder<TEntity>(IndexBuilder.IsDescending(isDescending));
+
         IndexBuilder<TEntity> IInfrastructure<IndexBuilder<TEntity>>.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<TEntity> HasAnnotation(string annotation, objec
         public override TestIndexBuilder<TEntity> IsUnique(bool isUnique = true)
             => new NonGenericTestIndexBuilder<TEntity>(IndexBuilder.IsUnique(isUnique));
 
+        public override TestIndexBuilder<TEntity> IsDescending(params bool[] isDescending)
+            => new NonGenericTestIndexBuilder<TEntity>(IndexBuilder.IsDescending(isDescending));
+
         IndexBuilder IInfrastructure<IndexBuilder>.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<TEntity>
 
         public abstract TestIndexBuilder<TEntity> HasAnnotation(string annotation, object? value);
         public abstract TestIndexBuilder<TEntity> IsUnique(bool isUnique = true);
+        public abstract TestIndexBuilder<TEntity> IsDescending(params bool[] isDescending);
     }
 
     public abstract class TestPropertyBuilder<TProperty>
diff --git a/test/EFCore.Tests/ModelBuilding/NonRelationshipTestBase.cs b/test/EFCore.Tests/ModelBuilding/NonRelationshipTestBase.cs
index af02579ec2f..d69d92909eb 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(true);
 
             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]