diff --git a/src/EFCore.SqlServer/Design/Internal/SqlServerAnnotationCodeGenerator.cs b/src/EFCore.SqlServer/Design/Internal/SqlServerAnnotationCodeGenerator.cs
index c795b7ecc4e..930e8a7643d 100644
--- a/src/EFCore.SqlServer/Design/Internal/SqlServerAnnotationCodeGenerator.cs
+++ b/src/EFCore.SqlServer/Design/Internal/SqlServerAnnotationCodeGenerator.cs
@@ -82,6 +82,11 @@ public override MethodCallCodeFragment GenerateFluentApi(IIndex index, IAnnotati
return new MethodCallCodeFragment(nameof(SqlServerIndexBuilderExtensions.IncludeProperties), annotation.Value);
}
+ if (annotation.Name == SqlServerAnnotationNames.FillFactor)
+ {
+ return new MethodCallCodeFragment(nameof(SqlServerIndexBuilderExtensions.HasFillFactor), annotation.Value);
+ }
+
return null;
}
}
diff --git a/src/EFCore.SqlServer/Extensions/SqlServerIndexBuilderExtensions.cs b/src/EFCore.SqlServer/Extensions/SqlServerIndexBuilderExtensions.cs
index 1968b83e5fc..ac986c35c13 100644
--- a/src/EFCore.SqlServer/Extensions/SqlServerIndexBuilderExtensions.cs
+++ b/src/EFCore.SqlServer/Extensions/SqlServerIndexBuilderExtensions.cs
@@ -245,5 +245,72 @@ public static bool CanSetIsCreatedOnline(
return indexBuilder.CanSetAnnotation(SqlServerAnnotationNames.CreatedOnline, createdOnline, fromDataAnnotation);
}
+
+ ///
+ /// Configures whether the index is created with fill factor option when targeting SQL Server.
+ ///
+ /// The builder for the index being configured.
+ /// A value indicating whether the index is created with fill factor option.
+ /// A builder to further configure the index.
+ public static IndexBuilder HasFillFactor([NotNull] this IndexBuilder indexBuilder, int fillFactor)
+ {
+ Check.NotNull(indexBuilder, nameof(indexBuilder));
+
+ indexBuilder.Metadata.SetFillFactor(fillFactor);
+
+ return indexBuilder;
+ }
+
+ ///
+ /// Configures whether the index is created with fill factor option when targeting SQL Server.
+ ///
+ /// The builder for the index being configured.
+ /// A value indicating whether the index is created with fill factor option.
+ /// A builder to further configure the index.
+ public static IndexBuilder HasFillFactor(
+ [NotNull] this IndexBuilder indexBuilder, int fillFactor)
+ => (IndexBuilder)HasFillFactor((IndexBuilder)indexBuilder, fillFactor);
+
+ ///
+ /// Configures whether the index is created with fill factor option when targeting SQL Server.
+ ///
+ /// The builder for the index being configured.
+ /// A value indicating whether the index is created with fill factor option.
+ /// Indicates whether the configuration was specified using a data annotation.
+ ///
+ /// The same builder instance if the configuration was applied,
+ /// null otherwise.
+ ///
+ public static IConventionIndexBuilder HasFillFactor(
+ [NotNull] this IConventionIndexBuilder indexBuilder,
+ int? fillFactor,
+ bool fromDataAnnotation = false)
+ {
+ if (indexBuilder.CanSetFillFactor(fillFactor, fromDataAnnotation))
+ {
+ indexBuilder.Metadata.SetFillFactor(fillFactor, fromDataAnnotation);
+
+ return indexBuilder;
+ }
+
+ return null;
+ }
+
+ ///
+ /// Returns a value indicating whether the index can be configured with fill factor option when targeting SQL Server.
+ ///
+ /// The builder for the index being configured.
+ /// A value indicating whether the index is created with fill factor option.
+ /// Indicates whether the configuration was specified using a data annotation.
+ /// true if the index can be configured with fill factor option when targeting SQL Server.
+ public static bool CanSetFillFactor(
+ [NotNull] this IConventionIndexBuilder indexBuilder,
+ int? fillFactor,
+ bool fromDataAnnotation = false)
+ {
+ Check.NotNull(indexBuilder, nameof(indexBuilder));
+
+ return indexBuilder.CanSetAnnotation(SqlServerAnnotationNames.FillFactor, fillFactor, fromDataAnnotation);
+ }
}
}
diff --git a/src/EFCore.SqlServer/Extensions/SqlServerIndexExtensions.cs b/src/EFCore.SqlServer/Extensions/SqlServerIndexExtensions.cs
index ba0f1223082..4366f718c47 100644
--- a/src/EFCore.SqlServer/Extensions/SqlServerIndexExtensions.cs
+++ b/src/EFCore.SqlServer/Extensions/SqlServerIndexExtensions.cs
@@ -1,6 +1,7 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+using System;
using System.Collections.Generic;
using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore.Metadata;
@@ -145,5 +146,63 @@ public static void SetIsCreatedOnline([NotNull] this IMutableIndex index, bool?
/// The for whether the index is online.
public static ConfigurationSource? GetIsCreatedOnlineConfigurationSource([NotNull] this IConventionIndex index)
=> index.FindAnnotation(SqlServerAnnotationNames.CreatedOnline)?.GetConfigurationSource();
+
+ ///
+ /// Returns a value indicating whether the index uses the fill factor.
+ ///
+ /// The index.
+ /// true if the index is online.
+ public static int? GetFillFactor([NotNull] this IIndex index)
+ => (int?)index[SqlServerAnnotationNames.FillFactor];
+
+ ///
+ /// Sets a value indicating whether the index uses the fill factor.
+ ///
+ /// The index.
+ /// The value to set.
+ public static void SetFillFactor([NotNull] this IMutableIndex index, int? fillFactor)
+ {
+ if (fillFactor != null && (fillFactor <= 0 || fillFactor > 100))
+ {
+ throw new ArgumentOutOfRangeException(nameof(fillFactor));
+ }
+
+ index.SetOrRemoveAnnotation(
+ SqlServerAnnotationNames.FillFactor,
+ fillFactor);
+ }
+
+ ///
+ /// Defines a value indicating whether the index uses the fill factor.
+ ///
+ /// The index.
+ /// The value to set.
+ /// Indicates whether the configuration was specified using a data annotation.
+ /// The configured value.
+ public static int? SetFillFactor(
+ [NotNull] this IConventionIndex index,
+ int? fillFactor,
+ bool fromDataAnnotation = false)
+ {
+ if (fillFactor != null && (fillFactor <= 0 || fillFactor > 100))
+ {
+ throw new ArgumentOutOfRangeException(nameof(fillFactor));
+ }
+
+ index.SetOrRemoveAnnotation(
+ SqlServerAnnotationNames.FillFactor,
+ fillFactor,
+ fromDataAnnotation);
+
+ return fillFactor;
+ }
+
+ ///
+ /// Returns the for whether the index uses the fill factor.
+ ///
+ /// The index.
+ /// The for whether the index uses the fill factor.
+ public static ConfigurationSource? GetFillFactorConfigurationSource([NotNull] this IConventionIndex index)
+ => index.FindAnnotation(SqlServerAnnotationNames.FillFactor)?.GetConfigurationSource();
}
}
diff --git a/src/EFCore.SqlServer/Metadata/Internal/SqlServerAnnotationNames.cs b/src/EFCore.SqlServer/Metadata/Internal/SqlServerAnnotationNames.cs
index ac73917367a..4aec92a48ba 100644
--- a/src/EFCore.SqlServer/Metadata/Internal/SqlServerAnnotationNames.cs
+++ b/src/EFCore.SqlServer/Metadata/Internal/SqlServerAnnotationNames.cs
@@ -130,5 +130,13 @@ public static class SqlServerAnnotationNames
/// doing so can result in application failures when updating to a new Entity Framework Core release.
///
public const string PerformanceLevelSql = Prefix + "PerformanceLevelSql";
+
+ ///
+ /// 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 const string FillFactor = Prefix + "FillFactor";
}
}
diff --git a/src/EFCore.SqlServer/Metadata/Internal/SqlServerAnnotationProvider.cs b/src/EFCore.SqlServer/Metadata/Internal/SqlServerAnnotationProvider.cs
index e2dc0b645ae..d4a15f1c5cf 100644
--- a/src/EFCore.SqlServer/Metadata/Internal/SqlServerAnnotationProvider.cs
+++ b/src/EFCore.SqlServer/Metadata/Internal/SqlServerAnnotationProvider.cs
@@ -162,6 +162,14 @@ public override IEnumerable For(ITableIndex index)
SqlServerAnnotationNames.CreatedOnline,
isOnline.Value);
}
+
+ var fillFactor = modelIndex.GetFillFactor();
+ if (fillFactor.HasValue)
+ {
+ yield return new Annotation(
+ SqlServerAnnotationNames.FillFactor,
+ fillFactor.Value);
+ }
}
///
diff --git a/src/EFCore.SqlServer/Metadata/Internal/SqlServerIndexExtensions.cs b/src/EFCore.SqlServer/Metadata/Internal/SqlServerIndexExtensions.cs
index c2a490a166c..dad29905804 100644
--- a/src/EFCore.SqlServer/Metadata/Internal/SqlServerIndexExtensions.cs
+++ b/src/EFCore.SqlServer/Metadata/Internal/SqlServerIndexExtensions.cs
@@ -80,6 +80,23 @@ public static bool AreCompatibleForSqlServer([NotNull] this IIndex index, [NotNu
return false;
}
+ if (index.GetFillFactor() != duplicateIndex.GetFillFactor())
+ {
+ if (shouldThrow)
+ {
+ throw new InvalidOperationException(
+ SqlServerStrings.DuplicateIndexFillFactorMismatch(
+ index.Properties.Format(),
+ index.DeclaringEntityType.DisplayName(),
+ duplicateIndex.Properties.Format(),
+ duplicateIndex.DeclaringEntityType.DisplayName(),
+ index.DeclaringEntityType.GetSchemaQualifiedTableName(),
+ index.GetName()));
+ }
+
+ return false;
+ }
+
return true;
}
diff --git a/src/EFCore.SqlServer/Migrations/SqlServerMigrationsSqlGenerator.cs b/src/EFCore.SqlServer/Migrations/SqlServerMigrationsSqlGenerator.cs
index aa96594a836..7f75332d437 100644
--- a/src/EFCore.SqlServer/Migrations/SqlServerMigrationsSqlGenerator.cs
+++ b/src/EFCore.SqlServer/Migrations/SqlServerMigrationsSqlGenerator.cs
@@ -1552,9 +1552,29 @@ protected override void IndexOptions(CreateIndexOperation operation, IModel mode
base.IndexOptions(operation, model, builder);
+ IndexWithOptions(operation, builder);
+ }
+
+ private void IndexWithOptions(CreateIndexOperation operation, MigrationCommandListBuilder builder)
+ {
+ var options = new List();
+
+ if (operation[SqlServerAnnotationNames.FillFactor] is int fillFactor)
+ {
+ options.Add("FILLFACTOR = " + fillFactor);
+ }
+
if (operation[SqlServerAnnotationNames.CreatedOnline] is bool isOnline && isOnline)
{
- builder.Append(" WITH (ONLINE = ON)");
+ options.Add("ONLINE = ON");
+ }
+
+ if (options.Count > 0)
+ {
+ builder
+ .Append(" WITH (")
+ .Append(string.Join(", ", options))
+ .Append(")");
}
}
diff --git a/src/EFCore.SqlServer/Properties/SqlServerStrings.Designer.cs b/src/EFCore.SqlServer/Properties/SqlServerStrings.Designer.cs
index 95237706db8..274f35ce5e6 100644
--- a/src/EFCore.SqlServer/Properties/SqlServerStrings.Designer.cs
+++ b/src/EFCore.SqlServer/Properties/SqlServerStrings.Designer.cs
@@ -223,6 +223,14 @@ public static string DuplicateIndexOnlineMismatch([CanBeNull] object index1, [Ca
GetString("DuplicateIndexOnlineMismatch", nameof(index1), nameof(entityType1), nameof(index2), nameof(entityType2), nameof(table), nameof(indexName)),
index1, entityType1, index2, entityType2, table, indexName);
+ ///
+ /// The indexes {index1} on '{entityType1}' and {index2} on '{entityType2}' are both mapped to '{table}.{indexName}' but with different fill factor configuration.
+ ///
+ public static string DuplicateIndexFillFactorMismatch([CanBeNull] object index1, [CanBeNull] object entityType1, [CanBeNull] object index2, [CanBeNull] object entityType2, [CanBeNull] object table, [CanBeNull] object indexName)
+ => string.Format(
+ GetString("DuplicateIndexFillFactorMismatch", nameof(index1), nameof(entityType1), nameof(index2), nameof(entityType2), nameof(table), nameof(indexName)),
+ index1, entityType1, index2, entityType2, table, indexName);
+
private static string GetString(string name, params string[] formatterNames)
{
var value = _resourceManager.GetString(name);
diff --git a/src/EFCore.SqlServer/Properties/SqlServerStrings.resx b/src/EFCore.SqlServer/Properties/SqlServerStrings.resx
index 821be229f8c..054b057c58f 100644
--- a/src/EFCore.SqlServer/Properties/SqlServerStrings.resx
+++ b/src/EFCore.SqlServer/Properties/SqlServerStrings.resx
@@ -266,4 +266,7 @@
The indexes {index1} on '{entityType1}' and {index2} on '{entityType2}' are both mapped to '{table}.{indexName}' but with different online configuration.
+
+ The indexes {index1} on '{entityType1}' and {index2} on '{entityType2}' are both mapped to '{table}.{indexName}' but with different fill factor configuration.
+
\ No newline at end of file
diff --git a/src/EFCore.SqlServer/Scaffolding/Internal/SqlServerDatabaseModelFactory.cs b/src/EFCore.SqlServer/Scaffolding/Internal/SqlServerDatabaseModelFactory.cs
index b81c671aabf..df7caeeb86d 100644
--- a/src/EFCore.SqlServer/Scaffolding/Internal/SqlServerDatabaseModelFactory.cs
+++ b/src/EFCore.SqlServer/Scaffolding/Internal/SqlServerDatabaseModelFactory.cs
@@ -843,6 +843,7 @@ private void GetIndexes(DbConnection connection, IReadOnlyList ta
[i].[is_unique],
[i].[has_filter],
[i].[filter_definition],
+ [i].[fill_factor],
COL_NAME([ic].[object_id], [ic].[column_id]) AS [column_name]
FROM [sys].[indexes] AS [i]
JOIN [sys].[tables] AS [t] ON [i].[object_id] = [t].[object_id]
@@ -965,7 +966,8 @@ FROM [sys].[indexes] i
TypeDesc: ddr.GetValueOrDefault("type_desc"),
IsUnique: ddr.GetValueOrDefault("is_unique"),
HasFilter: ddr.GetValueOrDefault("has_filter"),
- FilterDefinition: ddr.GetValueOrDefault("filter_definition")))
+ FilterDefinition: ddr.GetValueOrDefault("filter_definition"),
+ FillFactor: ddr.GetValueOrDefault("fill_factor")))
.ToArray();
foreach (var indexGroup in indexGroups)
@@ -983,6 +985,11 @@ FROM [sys].[indexes] i
index[SqlServerAnnotationNames.Clustered] = true;
}
+ if (indexGroup.Key.FillFactor > 0 && indexGroup.Key.FillFactor <= 100)
+ {
+ index[SqlServerAnnotationNames.FillFactor] = indexGroup.Key.FillFactor;
+ }
+
foreach (var dataRecord in indexGroup)
{
var columnName = dataRecord.GetValueOrDefault("column_name");
diff --git a/test/EFCore.SqlServer.FunctionalTests/MigrationsSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/MigrationsSqlServerTest.cs
index ceb2c345b8a..3ac2d0678ca 100644
--- a/test/EFCore.SqlServer.FunctionalTests/MigrationsSqlServerTest.cs
+++ b/test/EFCore.SqlServer.FunctionalTests/MigrationsSqlServerTest.cs
@@ -1200,6 +1200,96 @@ FROM [sys].[default_constraints] [d]
@"CREATE UNIQUE INDEX [IX_People_Name] ON [People] ([Name]) INCLUDE ([FirstName], [LastName]) WHERE [Name] IS NOT NULL WITH (ONLINE = ON);");
}
+ [ConditionalFact(Skip = "#19668, Online index operations can only be performed in Enterprise edition of SQL Server")]
+ [SqlServerCondition(SqlServerCondition.SupportsOnlineIndexes)]
+ public virtual async Task Create_index_unique_with_include_filter_online_and_fillfactor()
+ {
+ await Test(
+ builder => builder.Entity(
+ "People", e =>
+ {
+ e.Property("Id");
+ e.Property("FirstName");
+ e.Property("LastName");
+ e.Property("Name").IsRequired();
+ }),
+ builder => { },
+ builder => builder.Entity("People").HasIndex("Name")
+ .IsUnique()
+ .IncludeProperties("FirstName", "LastName")
+ .HasFilter("[Name] IS NOT NULL")
+ .IsCreatedOnline()
+ .HasFillFactor(90),
+ model =>
+ {
+ var table = Assert.Single(model.Tables);
+ var index = Assert.Single(table.Indexes);
+ Assert.True(index.IsUnique);
+ Assert.Equal("([Name] IS NOT NULL)", index.Filter);
+ // TODO: This is a scaffolding bug, #17083
+ Assert.Equal(3, index.Columns.Count);
+ Assert.Contains(table.Columns.Single(c => c.Name == "Name"), index.Columns);
+ Assert.Contains(table.Columns.Single(c => c.Name == "FirstName"), index.Columns);
+ Assert.Contains(table.Columns.Single(c => c.Name == "LastName"), index.Columns);
+ // TODO: Online index not scaffolded?
+ });
+
+ AssertSql(
+ @"DECLARE @var0 sysname;
+SELECT @var0 = [d].[name]
+FROM [sys].[default_constraints] [d]
+INNER JOIN [sys].[columns] [c] ON [d].[parent_column_id] = [c].[column_id] AND [d].[parent_object_id] = [c].[object_id]
+WHERE ([d].[parent_object_id] = OBJECT_ID(N'[People]') AND [c].[name] = N'Name');
+IF @var0 IS NOT NULL EXEC(N'ALTER TABLE [People] DROP CONSTRAINT [' + @var0 + '];');
+ALTER TABLE [People] ALTER COLUMN [Name] nvarchar(450) NOT NULL;",
+ //
+ @"CREATE UNIQUE INDEX [IX_People_Name] ON [People] ([Name]) INCLUDE ([FirstName], [LastName]) WHERE [Name] IS NOT NULL WITH (FILLFACTOR = 90, ONLINE = ON);");
+ }
+
+ [ConditionalFact]
+ public virtual async Task Create_index_unique_with_include_filter_and_fillfactor()
+ {
+ await Test(
+ builder => builder.Entity(
+ "People", e =>
+ {
+ e.Property("Id");
+ e.Property("FirstName");
+ e.Property("LastName");
+ e.Property("Name").IsRequired();
+ }),
+ builder => { },
+ builder => builder.Entity("People").HasIndex("Name")
+ .IsUnique()
+ .IncludeProperties("FirstName", "LastName")
+ .HasFilter("[Name] IS NOT NULL")
+ .HasFillFactor(90),
+ model =>
+ {
+ var table = Assert.Single(model.Tables);
+ var index = Assert.Single(table.Indexes);
+ Assert.True(index.IsUnique);
+ Assert.Equal("([Name] IS NOT NULL)", index.Filter);
+ // TODO: This is a scaffolding bug, #17083
+ Assert.Equal(3, index.Columns.Count);
+ Assert.Contains(table.Columns.Single(c => c.Name == "Name"), index.Columns);
+ Assert.Contains(table.Columns.Single(c => c.Name == "FirstName"), index.Columns);
+ Assert.Contains(table.Columns.Single(c => c.Name == "LastName"), index.Columns);
+ // TODO: Online index not scaffolded?
+ });
+
+ AssertSql(
+ @"DECLARE @var0 sysname;
+SELECT @var0 = [d].[name]
+FROM [sys].[default_constraints] [d]
+INNER JOIN [sys].[columns] [c] ON [d].[parent_column_id] = [c].[column_id] AND [d].[parent_object_id] = [c].[object_id]
+WHERE ([d].[parent_object_id] = OBJECT_ID(N'[People]') AND [c].[name] = N'Name');
+IF @var0 IS NOT NULL EXEC(N'ALTER TABLE [People] DROP CONSTRAINT [' + @var0 + '];');
+ALTER TABLE [People] ALTER COLUMN [Name] nvarchar(450) NOT NULL;",
+ //
+ @"CREATE UNIQUE INDEX [IX_People_Name] ON [People] ([Name]) INCLUDE ([FirstName], [LastName]) WHERE [Name] IS NOT NULL WITH (FILLFACTOR = 90);");
+ }
+
[ConditionalFact]
[SqlServerCondition(SqlServerCondition.SupportsMemoryOptimized)]
public virtual async Task Create_index_memoryOptimized_unique_nullable()
diff --git a/test/EFCore.SqlServer.Tests/Design/Internal/SqlServerAnnotationCodeGeneratorTest.cs b/test/EFCore.SqlServer.Tests/Design/Internal/SqlServerAnnotationCodeGeneratorTest.cs
index 84ca03e08f8..6b8edaaff4f 100644
--- a/test/EFCore.SqlServer.Tests/Design/Internal/SqlServerAnnotationCodeGeneratorTest.cs
+++ b/test/EFCore.SqlServer.Tests/Design/Internal/SqlServerAnnotationCodeGeneratorTest.cs
@@ -103,6 +103,29 @@ public void GenerateFluentApi_IIndex_works_when_nonclustered()
Assert.Equal(false, result.Arguments[0]);
}
+ [ConditionalFact]
+ public void GenerateFluentApi_IIndex_works_with_fillfactor()
+ {
+ var generator = new SqlServerAnnotationCodeGenerator(new AnnotationCodeGeneratorDependencies());
+ var modelBuilder = new ModelBuilder(SqlServerConventionSetBuilder.Build());
+ modelBuilder.Entity(
+ "Post",
+ x =>
+ {
+ x.Property("Id");
+ x.Property("Name");
+ x.HasIndex("Name").HasFillFactor(90);
+ });
+
+ var index = modelBuilder.Model.FindEntityType("Post").GetIndexes().Single();
+ var annotation = index.FindAnnotation(SqlServerAnnotationNames.FillFactor);
+ var result = generator.GenerateFluentApi(index, annotation);
+
+ Assert.Equal("HasFillFactor", result.Method);
+ Assert.Equal(1, result.Arguments.Count);
+ Assert.Equal(90, result.Arguments[0]);
+ }
+
[ConditionalFact]
public void GenerateFluentApi_IIndex_works_with_includes()
{
diff --git a/test/EFCore.SqlServer.Tests/Metadata/SqlServerBuilderExtensionsTest.cs b/test/EFCore.SqlServer.Tests/Metadata/SqlServerBuilderExtensionsTest.cs
index 28909fbaf1e..faec607c890 100644
--- a/test/EFCore.SqlServer.Tests/Metadata/SqlServerBuilderExtensionsTest.cs
+++ b/test/EFCore.SqlServer.Tests/Metadata/SqlServerBuilderExtensionsTest.cs
@@ -234,8 +234,8 @@ public void Can_set_index_online_non_generic()
var modelBuilder = CreateConventionModelBuilder();
modelBuilder
- .Entity()
- .HasIndex(e => e.Name)
+ .Entity(typeof(Customer))
+ .HasIndex("Name")
.IsCreatedOnline();
var index = modelBuilder.Model.FindEntityType(typeof(Customer)).GetIndexes().Single();
@@ -738,6 +738,53 @@ public void Can_write_index_filter_with_where_clauses_generic()
Assert.Equal("[Id] % 2 = 0", index.GetFilter());
}
+
+ [ConditionalFact]
+ public void Can_set_index_with_fillfactor()
+ {
+ var modelBuilder = CreateConventionModelBuilder();
+
+ modelBuilder
+ .Entity()
+ .HasIndex(e => e.Name)
+ .HasFillFactor(90);
+
+ var index = modelBuilder.Model.FindEntityType(typeof(Customer)).GetIndexes().Single();
+
+ Assert.Equal(90, index.GetFillFactor());
+ }
+
+ [ConditionalFact]
+ public void Can_set_index_with_fillfactor_non_generic()
+ {
+ var modelBuilder = CreateConventionModelBuilder();
+
+ modelBuilder
+ .Entity(typeof(Customer))
+ .HasIndex("Name")
+ .HasFillFactor(90);
+
+ var index = modelBuilder.Model.FindEntityType(typeof(Customer)).GetIndexes().Single();
+
+ Assert.Equal(90, index.GetFillFactor());
+ }
+
+ [ConditionalTheory]
+ [InlineData(0)]
+ [InlineData(101)]
+ public void Throws_if_attempt_to_set_fillfactor_with_argument_out_of_range(int fillFactor)
+ {
+ var modelBuilder = CreateConventionModelBuilder();
+
+ Assert.Throws(() =>
+ {
+ modelBuilder
+ .Entity(typeof(Customer))
+ .HasIndex("Name")
+ .HasFillFactor(fillFactor);
+ });
+ }
+
private void AssertIsGeneric(EntityTypeBuilder _)
{
}
diff --git a/test/EFCore.SqlServer.Tests/Migrations/SqlServerModelDifferTest.cs b/test/EFCore.SqlServer.Tests/Migrations/SqlServerModelDifferTest.cs
index 72d9c743079..9d89d8e180c 100644
--- a/test/EFCore.SqlServer.Tests/Migrations/SqlServerModelDifferTest.cs
+++ b/test/EFCore.SqlServer.Tests/Migrations/SqlServerModelDifferTest.cs
@@ -908,5 +908,151 @@ protected override MigrationsModelDiffer CreateModelDiffer(DbContextOptions opti
private bool? IsMemoryOptimized(Annotatable annotatable)
=> annotatable[SqlServerAnnotationNames.MemoryOptimized] as bool?;
+
+ [ConditionalFact]
+ public void Dont_rebuild_index_with_unchanged_fillfactor_option()
+ {
+ Execute(
+ source => source
+ .Entity(
+ "Address",
+ x =>
+ {
+ x.Property("Id");
+ x.Property("Zip");
+ x.Property("City");
+ x.HasIndex("Zip")
+ .HasFillFactor(90);
+ }),
+ target => target
+ .Entity(
+ "Address",
+ x =>
+ {
+ x.Property("Id");
+ x.Property("Zip");
+ x.Property("City");
+ x.HasIndex("Zip")
+ .HasFillFactor(90);
+ }),
+ operations => Assert.Equal(0, operations.Count));
+ }
+
+ [ConditionalFact]
+ public void Rebuild_index_when_adding_fillfactor_option()
+ {
+ Execute(
+ _ => { },
+ source => source
+ .Entity(
+ "Address",
+ x =>
+ {
+ x.Property("Id");
+ x.Property("Zip");
+ x.Property("City");
+ x.Property("Street");
+ x.HasIndex("Zip");
+ }),
+ target => target
+ .Entity(
+ "Address",
+ x =>
+ {
+ x.Property("Id");
+ x.Property("Zip");
+ x.Property("City");
+ x.Property("Street");
+ x.HasIndex("Zip")
+ .HasFillFactor(90);
+ }),
+ upOps =>
+ {
+ Assert.Equal(2, upOps.Count);
+
+ var operation1 = Assert.IsType(upOps[0]);
+ Assert.Equal("Address", operation1.Table);
+ Assert.Equal("IX_Address_Zip", operation1.Name);
+
+ Assert.Empty(operation1.GetAnnotations());
+
+ var operation2 = Assert.IsType(upOps[1]);
+ Assert.Equal("Address", operation1.Table);
+ Assert.Equal("IX_Address_Zip", operation1.Name);
+
+ var annotation = operation2.GetAnnotation(SqlServerAnnotationNames.FillFactor);
+ Assert.NotNull(annotation);
+
+ var annotationValue = Assert.IsType(annotation.Value);
+ Assert.Equal(90, annotationValue);
+ },
+ downOps =>
+ {
+ Assert.Equal(2, downOps.Count);
+
+ var operation1 = Assert.IsType(downOps[0]);
+ Assert.Equal("Address", operation1.Table);
+ Assert.Equal("IX_Address_Zip", operation1.Name);
+
+ Assert.Empty(operation1.GetAnnotations());
+
+ var operation2 = Assert.IsType(downOps[1]);
+ Assert.Equal("Address", operation1.Table);
+ Assert.Equal("IX_Address_Zip", operation1.Name);
+
+ Assert.Empty(operation2.GetAnnotations());
+ });
+ }
+
+ [ConditionalFact]
+ public void Rebuild_index_with_different_fillfactor_value()
+ {
+ Execute(
+ source => source
+ .Entity(
+ "Address",
+ x =>
+ {
+ x.Property("Id");
+ x.Property("Zip");
+ x.Property("City");
+ x.Property("Street");
+ x.HasIndex("Zip")
+ .HasFillFactor(50);
+ }),
+ target => target
+ .Entity(
+ "Address",
+ x =>
+ {
+ x.Property("Id");
+ x.Property("Zip");
+ x.Property("City");
+ x.Property("Street");
+ x.HasIndex("Zip")
+ .HasFillFactor(90);
+ }),
+ operations =>
+ {
+ Assert.Equal(2, operations.Count);
+
+ var operation1 = Assert.IsType(operations[0]);
+ Assert.Equal("Address", operation1.Table);
+ Assert.Equal("IX_Address_Zip", operation1.Name);
+
+ Assert.Empty(operation1.GetAnnotations());
+
+ var operation2 = Assert.IsType(operations[1]);
+ Assert.Equal("Address", operation1.Table);
+ Assert.Equal("IX_Address_Zip", operation1.Name);
+
+ var annotation = operation2.GetAnnotation(SqlServerAnnotationNames.FillFactor);
+ Assert.NotNull(annotation);
+
+ var annotationValue = Assert.IsType(annotation.Value);
+
+ Assert.Equal(90, annotationValue);
+ });
+ }
}
}