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;
///
/// Initializes a new instance of the class.
@@ -54,6 +55,24 @@ public bool IsUnique
set => _isUnique = value;
}
+ ///
+ /// A set of values indicating whether each corresponding index column has descending sort order.
+ ///
+ 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;
+ }
+ }
+
///
/// Checks whether has been explicitly set to a value.
///
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);
+ ///
+ /// Invalid number of index sort order values: {numValues} values were provided, but the index has {numProperties} properties.
+ ///
+ 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 @@
The collection argument '{argumentName}' must contain at least one element.
+
+ Invalid number of index sort order values: {numValues} values were provided, but the index has {numProperties} properties.
+
\ 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
///
bool IsUnique { get; }
+ ///
+ /// A set of values indicating whether each corresponding index column has descending sort order.
+ ///
+ IReadOnlyList? IsDescending { get; }
+
///
/// Gets the expression used as the index filter.
///
@@ -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
///
public virtual bool IsUnique { get; }
+ ///
+ public virtual IReadOnlyList? IsDescending
+ => MappedIndexes.First().IsDescending;
+
///
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 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 AlterTable(
/// The schema that contains the table, or to use the default schema.
/// Indicates whether or not the index enforces uniqueness.
/// The filter to apply to the index, or for no filter.
+ ///
+ /// A set of values indicating whether each corresponding index column has descending sort order.
+ /// If , all columns will have ascending order.
+ ///
/// A builder to allow annotations to be added to the operation.
public virtual OperationBuilder CreateIndex(
string name,
@@ -608,14 +612,16 @@ public virtual OperationBuilder CreateIndex(
string column,
string? schema = null,
bool unique = false,
- string? filter = null)
+ string? filter = null,
+ bool[]? descending = null)
=> CreateIndex(
name,
table,
new[] { Check.NotEmpty(column, nameof(column)) },
schema,
unique,
- filter);
+ filter,
+ descending);
///
/// Builds a to create a new composite (multi-column) index.
@@ -629,6 +635,10 @@ public virtual OperationBuilder CreateIndex(
/// The schema that contains the table, or to use the default schema.
/// Indicates whether or not the index enforces uniqueness.
/// The filter to apply to the index, or for no filter.
+ ///
+ /// A set of values indicating whether each corresponding index column has descending sort order.
+ /// If , all columns will have ascending order.
+ ///
/// A builder to allow annotations to be added to the operation.
public virtual OperationBuilder CreateIndex(
string name,
@@ -636,7 +646,8 @@ public virtual OperationBuilder CreateIndex(
string[] columns,
string? schema = null,
bool unique = false,
- string? filter = null)
+ string? filter = null,
+ bool[]? descending = null)
{
Check.NotEmpty(name, nameof(name));
Check.NotEmpty(table, nameof(table));
@@ -649,8 +660,10 @@ public virtual OperationBuilder CreateIndex(
Name = name,
Columns = columns,
IsUnique = unique,
+ IsDescending = descending,
Filter = filter
};
+
Operations.Add(operation);
return new OperationBuilder(operation);
diff --git a/src/EFCore.Relational/Migrations/MigrationsSqlGenerator.cs b/src/EFCore.Relational/Migrations/MigrationsSqlGenerator.cs
index f08c9c69118..5528830b693 100644
--- a/src/EFCore.Relational/Migrations/MigrationsSqlGenerator.cs
+++ b/src/EFCore.Relational/Migrations/MigrationsSqlGenerator.cs
@@ -428,9 +428,11 @@ protected virtual void Generate(
.Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Name))
.Append(" ON ")
.Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Table, operation.Schema))
- .Append(" (")
- .Append(ColumnList(operation.Columns))
- .Append(")");
+ .Append(" (");
+
+ GenerateIndexColumnList(operation, model, builder);
+
+ builder.Append(")");
IndexOptions(operation, model, builder);
@@ -1659,23 +1661,41 @@ protected virtual void CheckConstraint(
/// The operation.
/// The target model which may be if the operations exist without a model.
/// The command builder to use to add the SQL fragment.
- protected virtual void IndexTraits(
- MigrationOperation operation,
- IModel? model,
- MigrationCommandListBuilder builder)
+ protected virtual void IndexTraits(MigrationOperation operation, IModel? model, MigrationCommandListBuilder builder)
{
}
+ ///
+ /// Returns a SQL fragment for the column list of an index from a .
+ ///
+ /// The operation.
+ /// The target model which may be if the operations exist without a model.
+ /// The command builder to use to add the SQL fragment.
+ protected virtual void GenerateIndexColumnList(CreateIndexOperation operation, IModel? model, MigrationCommandListBuilder builder)
+ {
+ for (var i = 0; i < operation.Columns.Length; i++)
+ {
+ if (i > 0)
+ {
+ builder.Append(", ");
+ }
+
+ builder.Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Columns[i]));
+
+ if (operation.IsDescending is not null && i < operation.IsDescending.Length && operation.IsDescending[i])
+ {
+ builder.Append(" DESC");
+ }
+ }
+ }
+
///
/// Generates a SQL fragment for extras (filter, included columns, options) of an index from a .
///
/// The operation.
/// The target model which may be if the operations exist without a model.
/// The command builder to use to add the SQL fragment.
- protected virtual void IndexOptions(
- CreateIndexOperation operation,
- IModel? model,
- MigrationCommandListBuilder builder)
+ protected virtual void IndexOptions(CreateIndexOperation operation, IModel? model, MigrationCommandListBuilder builder)
{
if (!string.IsNullOrEmpty(operation.Filter))
{
diff --git a/src/EFCore.Relational/Migrations/Operations/CreateIndexOperation.cs b/src/EFCore.Relational/Migrations/Operations/CreateIndexOperation.cs
index 4b136221509..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
{
- ///
- /// Indicates whether or not the index should enforce uniqueness.
- ///
- public virtual bool IsUnique { get; set; }
+ private string[]? _columns;
+ private bool[]? _isDescending;
///
/// The name of the index.
@@ -35,7 +33,41 @@ public class CreateIndexOperation : MigrationOperation, ITableMigrationOperation
///
/// The ordered list of column names for the column that make up the index.
///
- 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;
+ }
+ }
+
+ ///
+ /// Indicates whether or not the index should enforce uniqueness.
+ ///
+ public virtual bool IsUnique { get; set; }
+
+ ///
+ /// A set of values indicating whether each corresponding index column has descending sort order.
+ ///
+ 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;
+ }
+ }
///
/// 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);
+ ///
+ /// {numSortOrderProperties} values were provided in CreateIndexOperations.IsDescending, but the operation has {numColumns} columns.
+ ///
+ public static string CreateIndexOperationWithInvalidSortOrder(object? numSortOrderProperties, object? numColumns)
+ => string.Format(
+ GetString("CreateIndexOperationWithInvalidSortOrder", nameof(numSortOrderProperties), nameof(numColumns)),
+ numSortOrderProperties, numColumns);
+
///
/// 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.
///
@@ -470,36 +478,44 @@ public static string DuplicateForeignKeyUniquenessMismatch(object? foreignKeyPro
foreignKeyProperties1, entityType1, foreignKeyProperties2, entityType2, table, foreignKeyName);
///
- /// 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}).
+ ///
+ 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);
+
+ ///
+ /// The indexes {index1} on '{entityType1}' and {index2} on '{entityType2}' are both mapped to '{table}.{indexName}', but with different filters ('{filter1}' and '{filter2}').
///
- 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);
///
- /// 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.
///
- 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);
///
- /// 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}').
///
- 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);
///
- /// 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.
///
- 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);
///
/// 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 @@
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}'.
+
+ {numSortOrderProperties} values were provided in CreateIndexOperations.IsDescending, but the operation has {numColumns} columns.
+
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.
@@ -290,16 +293,19 @@
The foreign keys {foreignKeyProperties1} on '{entityType1}' and {foreignKeyProperties2} on '{entityType2}' are both mapped to '{table}.{foreignKeyName}', but with different uniqueness configurations.
- 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}).
- 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 filters ('{filter1}' and '{filter2}').
+
+
+ The indexes {index1} on '{entityType1}' and {index2} on '{entityType2}' are both mapped to '{table}.{indexName}', but with different sort orders.
- 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}').
- 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.
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/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 Columns { get; } = new List();
///
- /// Indicates whether or not the index constrains uniqueness.
+ /// Indicates whether or not the index enforces uniqueness.
///
public virtual bool IsUnique { get; set; }
+ ///
+ /// A set of values indicating whether each corresponding index column has descending sort order.
+ ///
+ public virtual IList IsDescending { get; set; } = new List();
+
///
/// The filter expression, or if the index has no filter.
///
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 ta
[i].[filter_definition],
[i].[fill_factor],
COL_NAME([ic].[object_id], [ic].[column_id]) AS [column_name],
+ [ic].[is_descending_key],
[ic].[is_included_column]
FROM [sys].[indexes] AS [i]
JOIN [sys].[tables] AS [t] ON [i].[object_id] = [t].[object_id]
@@ -1137,6 +1138,8 @@ bool TryGetIndex(
return false;
}
+ index.IsDescending.Add(dataRecord.GetValueOrDefault("is_descending_key"));
+
index.Columns.Add(column);
}
diff --git a/src/EFCore.Sqlite.Core/Scaffolding/Internal/SqliteDatabaseModelFactory.cs b/src/EFCore.Sqlite.Core/Scaffolding/Internal/SqliteDatabaseModelFactory.cs
index fb527ae7c37..b2a50da6bbf 100644
--- a/src/EFCore.Sqlite.Core/Scaffolding/Internal/SqliteDatabaseModelFactory.cs
+++ b/src/EFCore.Sqlite.Core/Scaffolding/Internal/SqliteDatabaseModelFactory.cs
@@ -458,8 +458,9 @@ private void GetIndexes(DbConnection connection, DatabaseTable table)
using (var command2 = connection.CreateCommand())
{
command2.CommandText = new StringBuilder()
- .AppendLine("SELECT \"name\"")
- .AppendLine("FROM pragma_index_info(@index)")
+ .AppendLine("SELECT \"name\", \"desc\"")
+ .AppendLine("FROM pragma_index_xinfo(@index)")
+ .AppendLine("WHERE key = 1")
.AppendLine("ORDER BY \"seqno\";")
.ToString();
@@ -477,6 +478,7 @@ private void GetIndexes(DbConnection connection, DatabaseTable table)
Check.DebugAssert(column != null, "column is null.");
index.Columns.Add(column);
+ index.IsDescending.Add(reader2.GetBoolean(1));
}
}
diff --git a/src/EFCore/Metadata/Builders/IConventionIndexBuilder.cs b/src/EFCore/Metadata/Builders/IConventionIndexBuilder.cs
index be420be010c..93d7dafa6f8 100644
--- a/src/EFCore/Metadata/Builders/IConventionIndexBuilder.cs
+++ b/src/EFCore/Metadata/Builders/IConventionIndexBuilder.cs
@@ -27,18 +27,30 @@ public interface IConventionIndexBuilder : IConventionAnnotatableBuilder
///
/// A value indicating whether the index is unique.
/// Indicates whether the configuration was specified using a data annotation.
- ///
- /// The same builder instance if the uniqueness was configured,
- /// otherwise.
- ///
+ /// The same builder instance if the uniqueness was configured, otherwise.
IConventionIndexBuilder? IsUnique(bool? unique, bool fromDataAnnotation = false);
///
- /// Returns a value indicating whether this index uniqueness can be configured
- /// from the current configuration source.
+ /// Returns a value indicating whether this index uniqueness can be configured from the current configuration source.
///
/// A value indicating whether the index is unique.
/// Indicates whether the configuration was specified using a data annotation.
/// if the index uniqueness can be configured.
bool CanSetIsUnique(bool? unique, bool fromDataAnnotation = false);
+
+ ///
+ /// Configures the sort order(s) for the columns of this index (ascending or descending).
+ ///
+ /// A set of values indicating whether each corresponding index column has descending sort order.
+ /// Indicates whether the configuration was specified using a data annotation.
+ /// The same builder instance if the uniqueness was configured, otherwise.
+ IConventionIndexBuilder? IsDescending(IReadOnlyList? descending, bool fromDataAnnotation = false);
+
+ ///
+ /// Returns a value indicating whether this index sort order can be configured from the current configuration source.
+ ///
+ /// A set of values indicating whether each corresponding index column has descending sort order.
+ /// Indicates whether the configuration was specified using a data annotation.
+ /// if the index uniqueness can be configured.
+ bool CanSetIsDescending(IReadOnlyList? descending, bool fromDataAnnotation = false);
}
diff --git a/src/EFCore/Metadata/Builders/IndexBuilder.cs b/src/EFCore/Metadata/Builders/IndexBuilder.cs
index 9f3f1e899cc..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;
}
+ ///
+ /// Configures the sort order(s) for the columns of this index (ascending or descending).
+ ///
+ /// A set of values indicating whether each corresponding index column has descending sort order.
+ /// The same builder instance so that multiple configuration calls can be chained.
+ public virtual IndexBuilder IsDescending(params bool[] descending)
+ {
+ Builder.IsDescending(descending, ConfigurationSource.Explicit);
+
+ return this;
+ }
+
#region Hidden System.Object members
///
diff --git a/src/EFCore/Metadata/Builders/IndexBuilder`.cs b/src/EFCore/Metadata/Builders/IndexBuilder`.cs
index 8bfc61e74b1..e8f36809e9e 100644
--- a/src/EFCore/Metadata/Builders/IndexBuilder`.cs
+++ b/src/EFCore/Metadata/Builders/IndexBuilder`.cs
@@ -49,4 +49,12 @@ public IndexBuilder(IMutableIndex index)
/// The same builder instance so that multiple configuration calls can be chained.
public new virtual IndexBuilder IsUnique(bool unique = true)
=> (IndexBuilder)base.IsUnique(unique);
+
+ ///
+ /// Configures the sort order(s) for the columns of this index (ascending or descending).
+ ///
+ /// A set of values indicating whether each corresponding index column has descending sort order.
+ /// The same builder instance so that multiple configuration calls can be chained.
+ public new virtual IndexBuilder IsDescending(params bool[] descending)
+ => (IndexBuilder)base.IsDescending(descending);
}
diff --git a/src/EFCore/Metadata/Conventions/ConventionSet.cs b/src/EFCore/Metadata/Conventions/ConventionSet.cs
index 3d168974fb8..54cc3869801 100644
--- a/src/EFCore/Metadata/Conventions/ConventionSet.cs
+++ b/src/EFCore/Metadata/Conventions/ConventionSet.cs
@@ -207,6 +207,12 @@ public class ConventionSet
public virtual IList IndexUniquenessChangedConventions { get; }
= new List();
+ ///
+ /// Conventions to run when the sort order of an index is changed.
+ ///
+ public virtual IList IndexSortOrderChangedConventions { get; }
+ = new List();
+
///
/// Conventions to run when an annotation is changed on an index.
///
diff --git a/src/EFCore/Metadata/Conventions/IIndexSortOrderChangedConvention.cs b/src/EFCore/Metadata/Conventions/IIndexSortOrderChangedConvention.cs
new file mode 100644
index 00000000000..b537fc71c71
--- /dev/null
+++ b/src/EFCore/Metadata/Conventions/IIndexSortOrderChangedConvention.cs
@@ -0,0 +1,22 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace Microsoft.EntityFrameworkCore.Metadata.Conventions;
+
+///
+/// Represents an operation that should be performed when the sort order of an index is changed.
+///
+///
+/// See Model building conventions for more information and examples.
+///
+public interface IIndexSortOrderChangedConvention : IConvention
+{
+ ///
+ /// Called after the uniqueness for an index is changed.
+ ///
+ /// The builder for the index.
+ /// Additional information associated with convention execution.
+ void ProcessIndexSortOrderChanged(
+ IConventionIndexBuilder indexBuilder,
+ IConventionContext?> context);
+}
diff --git a/src/EFCore/Metadata/Conventions/IndexAttributeConvention.cs b/src/EFCore/Metadata/Conventions/IndexAttributeConvention.cs
index 0f8f1cef2f6..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? OnIndexSortOrderChanged(IConventionIndexBuilder indexBuilder);
+
public abstract IConventionKeyBuilder? OnKeyAdded(IConventionKeyBuilder keyBuilder);
public abstract IConventionAnnotation? OnKeyAnnotationChanged(
diff --git a/src/EFCore/Metadata/Conventions/Internal/ConventionDispatcher.DelayedConventionScope.cs b/src/EFCore/Metadata/Conventions/Internal/ConventionDispatcher.DelayedConventionScope.cs
index e7945a2f2f5..8e471dba230 100644
--- a/src/EFCore/Metadata/Conventions/Internal/ConventionDispatcher.DelayedConventionScope.cs
+++ b/src/EFCore/Metadata/Conventions/Internal/ConventionDispatcher.DelayedConventionScope.cs
@@ -183,6 +183,12 @@ public override IConventionIndex OnIndexRemoved(
return indexBuilder.Metadata.IsUnique;
}
+ public override IReadOnlyList? OnIndexSortOrderChanged(IConventionIndexBuilder indexBuilder)
+ {
+ Add(new OnIndexSortOrderChangedNode(indexBuilder));
+ return indexBuilder.Metadata.IsDescending;
+ }
+
public override IConventionAnnotation? OnIndexAnnotationChanged(
IConventionIndexBuilder indexBuilder,
string name,
@@ -901,6 +907,19 @@ public override void Run(ConventionDispatcher dispatcher)
=> dispatcher._immediateConventionScope.OnIndexUniquenessChanged(IndexBuilder);
}
+ private sealed class OnIndexSortOrderChangedNode : ConventionNode
+ {
+ public OnIndexSortOrderChangedNode(IConventionIndexBuilder indexBuilder)
+ {
+ IndexBuilder = indexBuilder;
+ }
+
+ public IConventionIndexBuilder IndexBuilder { get; }
+
+ public override void Run(ConventionDispatcher dispatcher)
+ => dispatcher._immediateConventionScope.OnIndexSortOrderChanged(IndexBuilder);
+ }
+
private sealed class OnIndexAnnotationChangedNode : ConventionNode
{
public OnIndexAnnotationChangedNode(
diff --git a/src/EFCore/Metadata/Conventions/Internal/ConventionDispatcher.ImmediateConventionScope.cs b/src/EFCore/Metadata/Conventions/Internal/ConventionDispatcher.ImmediateConventionScope.cs
index 846d88247b1..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 _stringConventionContext;
private readonly ConventionContext _fieldInfoConventionContext;
private readonly ConventionContext _boolConventionContext;
+ private readonly ConventionContext?> _boolListConventionContext;
public ImmediateConventionScope(ConventionSet conventionSet, ConventionDispatcher dispatcher)
{
@@ -54,6 +55,7 @@ public ImmediateConventionScope(ConventionSet conventionSet, ConventionDispatche
_stringConventionContext = new ConventionContext(dispatcher);
_fieldInfoConventionContext = new ConventionContext(dispatcher);
_boolConventionContext = new ConventionContext(dispatcher);
+ _boolListConventionContext = new ConventionContext?>(dispatcher);
}
public override void Run(ConventionDispatcher dispatcher)
@@ -969,6 +971,29 @@ public IConventionModelBuilder OnModelInitialized(IConventionModelBuilder modelB
return !indexBuilder.Metadata.IsInModel ? null : _boolConventionContext.Result;
}
+ public override IReadOnlyList? OnIndexSortOrderChanged(IConventionIndexBuilder indexBuilder)
+ {
+ using (_dispatcher.DelayConventions())
+ {
+ _boolListConventionContext.ResetState(indexBuilder.Metadata.IsDescending);
+ foreach (var indexConvention in _conventionSet.IndexSortOrderChangedConventions)
+ {
+ if (!indexBuilder.Metadata.IsInModel)
+ {
+ return null;
+ }
+
+ indexConvention.ProcessIndexSortOrderChanged(indexBuilder, _boolListConventionContext);
+ if (_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);
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ public virtual IReadOnlyList? OnIndexSortOrderChanged(IConventionIndexBuilder indexBuilder)
+ => _scope.OnIndexSortOrderChanged(indexBuilder);
+
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
diff --git a/src/EFCore/Metadata/IConventionIndex.cs b/src/EFCore/Metadata/IConventionIndex.cs
index ee2cfcdc9a2..ccad73d3b7e 100644
--- a/src/EFCore/Metadata/IConventionIndex.cs
+++ b/src/EFCore/Metadata/IConventionIndex.cs
@@ -54,4 +54,18 @@ public interface IConventionIndex : IReadOnlyIndex, IConventionAnnotatable
///
/// The configuration source for .
ConfigurationSource? GetIsUniqueConfigurationSource();
+
+ ///
+ /// Sets the sort order(s) for this index (ascending or descending).
+ ///
+ /// A set of values indicating whether each corresponding index column has descending sort order.
+ /// Indicates whether the configuration was specified using a data annotation.
+ /// The configured sort order(s).
+ IReadOnlyList? SetIsDescending(IReadOnlyList? descending, bool fromDataAnnotation = false);
+
+ ///
+ /// Returns the configuration source for .
+ ///
+ /// The configuration source for .
+ 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
///
new bool IsUnique { get; set; }
+ ///
+ /// A set of values indicating whether each corresponding index column has descending sort order.
+ ///
+ new IReadOnlyList? IsDescending { get; set; }
+
///
/// Gets the properties that this index is defined on.
///
diff --git a/src/EFCore/Metadata/IReadOnlyIndex.cs b/src/EFCore/Metadata/IReadOnlyIndex.cs
index 6bcb12dbecf..f7ab17a9a4a 100644
--- a/src/EFCore/Metadata/IReadOnlyIndex.cs
+++ b/src/EFCore/Metadata/IReadOnlyIndex.cs
@@ -28,6 +28,11 @@ public interface IReadOnlyIndex : IReadOnlyAnnotatable
///
bool IsUnique { get; }
+ ///
+ /// A set of values indicating whether each corresponding index column has descending sort order.
+ ///
+ IReadOnlyList? IsDescending { get; }
+
///
/// Gets the entity type the index is defined on. This may be different from the type that
/// are defined on when the index is defined a derived type in an inheritance hierarchy (since the properties
@@ -35,6 +40,15 @@ public interface IReadOnlyIndex : IReadOnlyAnnotatable
///
IReadOnlyEntityType DeclaringEntityType { get; }
+ ///
+ /// Gets the friendly display name for the given , returning its if one is defined,
+ /// or a string representation of its if this is an unnamed index.
+ ///
+ /// The display name.
+ [DebuggerStepThrough]
+ string DisplayName()
+ => Name is null ? Properties.Format() : $"'{Name}'";
+
///
///
/// 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 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? _isDescending;
+
private InternalIndexBuilder? _builder;
private ConfigurationSource _configurationSource;
private ConfigurationSource? _isUniqueConfigurationSource;
+ private ConfigurationSource? _isDescendingConfigurationSource;
// Warning: Never access these fields directly as access needs to be thread-safe
private object? _nullableValueFactory;
@@ -178,8 +181,8 @@ public virtual bool IsUnique
: oldIsUnique;
}
- private static bool DefaultIsUnique
- => false;
+ private static readonly bool DefaultIsUnique
+ = false;
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
@@ -193,6 +196,70 @@ private static bool DefaultIsUnique
private void UpdateIsUniqueConfigurationSource(ConfigurationSource configurationSource)
=> _isUniqueConfigurationSource = configurationSource.Max(_isUniqueConfigurationSource);
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ public virtual IReadOnlyList? IsDescending
+ {
+ get => _isDescending ?? DefaultIsDescending;
+ set => SetIsDescending(value, ConfigurationSource.Explicit);
+ }
+
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ public virtual IReadOnlyList? SetIsDescending(IReadOnlyList? descending, ConfigurationSource configurationSource)
+ {
+ EnsureMutable();
+
+ if (descending is not null && descending.Count != Properties.Count)
+ {
+ throw new ArgumentException(
+ CoreStrings.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;
+
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ public virtual ConfigurationSource? GetIsDescendingConfigurationSource()
+ => _isDescendingConfigurationSource;
+
+ private void UpdateIsDescendingConfigurationSource(ConfigurationSource configurationSource)
+ => _isDescendingConfigurationSource = configurationSource.Max(_isDescendingConfigurationSource);
+
///
/// Runs the conventions when an annotation was set or removed.
///
@@ -240,6 +307,16 @@ public virtual DebugView DebugView
() => ((IIndex)this).ToDebugString(),
() => ((IIndex)this).ToDebugString(MetadataDebugStringOptions.LongDefault));
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ [DebuggerStepThrough]
+ public virtual string DisplayName()
+ => Name is null ? Properties.Format() : $"'{Name}'";
+
///
/// 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);
+
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ [DebuggerStepThrough]
+ IReadOnlyList? IConventionIndex.SetIsDescending(IReadOnlyList? 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());
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ public virtual InternalIndexBuilder? IsDescending(IReadOnlyList? descending, ConfigurationSource configurationSource)
+ {
+ if (!CanSetIsDescending(descending, configurationSource))
+ {
+ return null;
+ }
+
+ Metadata.SetIsDescending(descending, configurationSource);
+ return this;
+ }
+
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ public virtual bool CanSetIsDescending(IReadOnlyList? descending, ConfigurationSource? configurationSource)
+ => descending is null && Metadata.IsDescending is null
+ || descending is not null && Metadata.IsDescending is not null && Metadata.IsDescending.SequenceEqual(descending)
+ || configurationSource.Overrides(Metadata.GetIsDescendingConfigurationSource());
+
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
@@ -107,4 +135,26 @@ bool IConventionIndexBuilder.CanSetIsUnique(bool? unique, bool fromDataAnnotatio
=> CanSetIsUnique(
unique,
fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention);
+
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ IConventionIndexBuilder? IConventionIndexBuilder.IsDescending(IReadOnlyList? descending, bool fromDataAnnotation)
+ => IsDescending(
+ descending,
+ fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention);
+
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ bool IConventionIndexBuilder.CanSetIsDescending(IReadOnlyList? descending, bool fromDataAnnotation)
+ => CanSetIsDescending(
+ descending,
+ fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention);
}
diff --git a/src/EFCore/Metadata/RuntimeIndex.cs b/src/EFCore/Metadata/RuntimeIndex.cs
index 109d734f507..c5ecc2c41e3 100644
--- a/src/EFCore/Metadata/RuntimeIndex.cs
+++ b/src/EFCore/Metadata/RuntimeIndex.cs
@@ -55,6 +55,15 @@ public RuntimeIndex(
///
public virtual RuntimeEntityType DeclaringEntityType { get; }
+ ///
+ /// Always returns an empty array for .
+ ///
+ IReadOnlyList IReadOnlyIndex.IsDescending
+ {
+ [DebuggerStepThrough]
+ get => throw new InvalidOperationException(CoreStrings.RuntimeModelMissingData);
+ }
+
///
/// Returns a string that represents the current object.
///
diff --git a/src/EFCore/Properties/CoreStrings.Designer.cs b/src/EFCore/Properties/CoreStrings.Designer.cs
index ecebccc29e4..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);
///
- /// 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}'.
///
- 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);
///
/// 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);
+ ///
+ /// Invalid number of index sort order values provided for {indexProperties}: {numValues} values were provided, but the index has {numProperties} properties.
+ ///
+ public static string InvalidNumberOfIndexSortOrderValues(object? indexProperties, object? numValues, object? numProperties)
+ => string.Format(
+ GetString("InvalidNumberOfIndexSortOrderValues", nameof(indexProperties), nameof(numValues), nameof(numProperties)),
+ indexProperties, numValues, numProperties);
+
///
/// The specified poolSize must be greater than 0.
///
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 @@
The specified index properties {indexProperties} are not declared on the entity type '{entityType}'. Ensure that index properties are declared on the target entity type.
- 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}'.
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'.
@@ -574,6 +574,9 @@
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.
+
+ Invalid number of index sort order values provided for {indexProperties}: {numValues} values were provided, but the index has {numProperties} properties.
+
The specified poolSize must be greater than 0.
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
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(
+ e =>
+ {
+ e.HasIndex(t => new { t.X, t.Y, t.Z }, "IX_empty");
+ e.HasIndex(t => new { t.X, t.Y, t.Z }, "IX_all_ascending")
+ .IsDescending(false, false, false);
+ e.HasIndex(t => new { t.X, t.Y, t.Z }, "IX_all_descending")
+ .IsDescending(true, true, true);
+ e.HasIndex(t => new { t.X, t.Y, t.Z }, "IX_mixed")
+ .IsDescending(false, true, false);
+ });
+ },
+ AddBoilerPlate(
+ GetHeading()
+ + @"
+ modelBuilder.Entity(""Microsoft.EntityFrameworkCore.Migrations.ModelSnapshotSqlServerTest+EntityWithThreeProperties"", b =>
+ {
+ b.Property(""Id"")
+ .ValueGeneratedOnAdd()
+ .HasColumnType(""int"");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property(""Id""), 1L, 1);
+
+ b.Property(""X"")
+ .HasColumnType(""int"");
+
+ b.Property(""Y"")
+ .HasColumnType(""int"");
+
+ b.Property(""Z"")
+ .HasColumnType(""int"");
+
+ b.HasKey(""Id"");
+
+ b.HasIndex(new[] { ""X"", ""Y"", ""Z"" }, ""IX_all_ascending"")
+ .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("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(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("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("Id");
+ x.Property("X");
+ x.Property("Y");
+ x.Property("Z");
+ x.HasKey("Id");
+ x.HasIndex(new[] { "X", "Y", "Z" }, "IX_empty");
+ x.HasIndex(new[] { "X", "Y", "Z" }, "IX_all_ascending")
+ .IsDescending(false, false, false);
+ x.HasIndex(new[] { "X", "Y", "Z" }, "IX_all_descending")
+ .IsDescending(true, true, true);
+ x.HasIndex(new[] { "X", "Y", "Z" }, "IX_mixed")
+ .IsDescending(false, true, false);
+ }),
+ new ModelCodeGenerationOptions { UseDataAnnotations = false },
+ code =>
+ {
+ AssertFileContents(
+ @"using System;
+using System.Collections.Generic;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata;
+
+namespace TestNamespace
+{
+ public partial class TestDbContext : DbContext
+ {
+ public TestDbContext()
+ {
+ }
+
+ public TestDbContext(DbContextOptions options)
+ : base(options)
+ {
+ }
+
+ public virtual DbSet EntityWithIndexes { get; set; }
+
+ protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
+ {
+ if (!optionsBuilder.IsConfigured)
+ {
+#warning "
+ + DesignStrings.SensitiveInformationWarning
+ + @"
+ optionsBuilder.UseSqlServer(""Initial Catalog=TestDatabase"");
+ }
+ }
+
+ protected override void OnModelCreating(ModelBuilder modelBuilder)
+ {
+ modelBuilder.Entity(entity =>
+ {
+ entity.HasIndex(e => new { e.X, e.Y, e.Z }, ""IX_all_ascending"")
+ .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("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("Id");
+ e.Property("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("Id");
+ e.Property("X");
+ e.Property("Y");
+ e.Property("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("Id");
+ e.Property("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("Id");
+ e.Property("X");
+ e.Property("Y");
+ e.Property("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();
+ modelBuilder.Entity().HasIndex(c => c.Name).HasDatabaseName("IX_Animal_Name")
+ .IsDescending(true);
+ modelBuilder.Entity().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(() => operation.Columns = new[] { "X", "Y" });
+
+ operation.IsDescending = null;
+
+ operation.Columns = new[] { "X", "Y" };
+ Assert.Throws(() => 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 { "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?> Calls = new();
+
+ public IndexSortOrderChangedConvention(bool terminate)
+ {
+ _terminate = terminate;
+ }
+
+ public void ProcessIndexSortOrderChanged(
+ IConventionIndexBuilder indexBuilder,
+ IConventionContext?> 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 { propABuilder.Metadata.Name, propBBuilder.Metadata.Name };
var indexBuilder = entityBuilder.HasIndex(indexProperties, "IndexOnAAndB", ConfigurationSource.Convention);
indexBuilder.IsUnique(false, ConfigurationSource.Convention);
+ indexBuilder.IsDescending(new[] { false, true }, ConfigurationSource.Convention);
RunConvention(entityBuilder);
RunConvention(modelBuilder);
@@ -35,8 +36,11 @@ public void IndexAttribute_overrides_configuration_from_convention()
var index = entityBuilder.Metadata.GetIndexes().Single();
Assert.Equal(ConfigurationSource.DataAnnotation, index.GetConfigurationSource());
Assert.Equal("IndexOnAAndB", index.Name);
+
Assert.True(index.IsUnique);
Assert.Equal(ConfigurationSource.DataAnnotation, index.GetIsUniqueConfigurationSource());
+ Assert.Equal(new[] { true, false }, index.IsDescending);
+ Assert.Equal(ConfigurationSource.DataAnnotation, index.GetIsDescendingConfigurationSource());
Assert.Collection(
index.Properties,
prop0 => Assert.Equal("A", prop0.Name),
@@ -50,7 +54,8 @@ public void IndexAttribute_can_be_overriden_using_explicit_configuration()
var entityBuilder = modelBuilder.Entity();
entityBuilder.HasIndex(new[] { "A", "B" }, "IndexOnAAndB")
- .IsUnique(false);
+ .IsUnique(false)
+ .IsDescending(false, true);
modelBuilder.Model.FinalizeModel();
@@ -59,6 +64,8 @@ public void IndexAttribute_can_be_overriden_using_explicit_configuration()
Assert.Equal("IndexOnAAndB", index.Name);
Assert.False(index.IsUnique);
Assert.Equal(ConfigurationSource.Explicit, index.GetIsUniqueConfigurationSource());
+ Assert.Equal(new[] { false, true }, index.IsDescending);
+ Assert.Equal(ConfigurationSource.Explicit, index.GetIsDescendingConfigurationSource());
Assert.Collection(
index.Properties,
prop0 => Assert.Equal("A", prop0.Name),
@@ -316,7 +323,7 @@ private IndexAttributeConvention CreateIndexAttributeConvention()
private ProviderConventionSetBuilderDependencies CreateDependencies()
=> InMemoryTestHelpers.Instance.CreateContextServices().GetRequiredService();
- [Index(nameof(A), nameof(B), Name = "IndexOnAAndB", IsUnique = true)]
+ [Index(nameof(A), nameof(B), Name = "IndexOnAAndB", IsUnique = true, IsDescending = new[] { true, false })]
private class EntityWithIndex
{
public int Id { get; set; }
diff --git a/test/EFCore.Tests/ModelBuilding/ModelBuilderGenericTest.cs b/test/EFCore.Tests/ModelBuilding/ModelBuilderGenericTest.cs
index f4e025e6f25..c7a2e0af068 100644
--- a/test/EFCore.Tests/ModelBuilding/ModelBuilderGenericTest.cs
+++ b/test/EFCore.Tests/ModelBuilding/ModelBuilderGenericTest.cs
@@ -581,6 +581,9 @@ public override TestIndexBuilder HasAnnotation(string annotation, objec
public override TestIndexBuilder IsUnique(bool isUnique = true)
=> new GenericTestIndexBuilder(IndexBuilder.IsUnique(isUnique));
+ public override TestIndexBuilder IsDescending(params bool[] isDescending)
+ => new GenericTestIndexBuilder(IndexBuilder.IsDescending(isDescending));
+
IndexBuilder IInfrastructure>.Instance
=> IndexBuilder;
}
diff --git a/test/EFCore.Tests/ModelBuilding/ModelBuilderNonGenericTest.cs b/test/EFCore.Tests/ModelBuilding/ModelBuilderNonGenericTest.cs
index a657349bf48..6904a9b0078 100644
--- a/test/EFCore.Tests/ModelBuilding/ModelBuilderNonGenericTest.cs
+++ b/test/EFCore.Tests/ModelBuilding/ModelBuilderNonGenericTest.cs
@@ -686,6 +686,9 @@ public override TestIndexBuilder HasAnnotation(string annotation, objec
public override TestIndexBuilder IsUnique(bool isUnique = true)
=> new NonGenericTestIndexBuilder(IndexBuilder.IsUnique(isUnique));
+ public override TestIndexBuilder IsDescending(params bool[] isDescending)
+ => new NonGenericTestIndexBuilder(IndexBuilder.IsDescending(isDescending));
+
IndexBuilder IInfrastructure.Instance
=> IndexBuilder;
}
diff --git a/test/EFCore.Tests/ModelBuilding/ModelBuilderTestBase.cs b/test/EFCore.Tests/ModelBuilding/ModelBuilderTestBase.cs
index c54b376fa60..368b3aaa84f 100644
--- a/test/EFCore.Tests/ModelBuilding/ModelBuilderTestBase.cs
+++ b/test/EFCore.Tests/ModelBuilding/ModelBuilderTestBase.cs
@@ -362,6 +362,7 @@ public abstract class TestIndexBuilder
public abstract TestIndexBuilder HasAnnotation(string annotation, object? value);
public abstract TestIndexBuilder IsUnique(bool isUnique = true);
+ public abstract TestIndexBuilder IsDescending(params bool[] isDescending);
}
public abstract class TestPropertyBuilder
diff --git a/test/EFCore.Tests/ModelBuilding/NonRelationshipTestBase.cs b/test/EFCore.Tests/ModelBuilding/NonRelationshipTestBase.cs
index af02579ec2f..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]