diff --git a/src/EFCore/Metadata/Conventions/IndexAttributeConvention.cs b/src/EFCore/Metadata/Conventions/IndexAttributeConvention.cs index bbddf9209b7..d908053dc29 100644 --- a/src/EFCore/Metadata/Conventions/IndexAttributeConvention.cs +++ b/src/EFCore/Metadata/Conventions/IndexAttributeConvention.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Reflection; using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.Metadata.Builders; using Microsoft.EntityFrameworkCore.Metadata.Conventions.Infrastructure; @@ -16,7 +17,8 @@ namespace Microsoft.EntityFrameworkCore.Metadata.Conventions /// /// A convention that configures database indexes based on the . /// - public class IndexAttributeConvention : IModelFinalizingConvention + public class IndexAttributeConvention : IEntityTypeAddedConvention, + IEntityTypeBaseTypeChangedConvention, IPropertyAddedConvention, IModelFinalizingConvention { /// /// Creates a new instance of . @@ -33,9 +35,48 @@ public IndexAttributeConvention([NotNull] ProviderConventionSetBuilderDependenci protected virtual ProviderConventionSetBuilderDependencies Dependencies { get; } /// - public virtual void ProcessModelFinalizing(IConventionModelBuilder modelBuilder, IConventionContext context) + public virtual void ProcessEntityTypeAdded( + IConventionEntityTypeBuilder entityTypeBuilder, + IConventionContext context) { - foreach (var entityType in modelBuilder.Metadata.GetEntityTypes()) + CheckIndexAttributesAndEnsureIndex( + new[] { entityTypeBuilder.Metadata }, true); + } + + /// + public virtual void ProcessEntityTypeBaseTypeChanged( + IConventionEntityTypeBuilder entityTypeBuilder, + IConventionEntityType newBaseType, + IConventionEntityType oldBaseType, + IConventionContext context) + { + CheckIndexAttributesAndEnsureIndex( + entityTypeBuilder.Metadata.GetDerivedTypesInclusive(), true); + } + + /// + public virtual void ProcessPropertyAdded( + IConventionPropertyBuilder propertyBuilder, + IConventionContext context) + { + CheckIndexAttributesAndEnsureIndex( + propertyBuilder.Metadata.DeclaringEntityType.GetDerivedTypesInclusive(), true); + } + + /// + public virtual void ProcessModelFinalizing( + IConventionModelBuilder modelBuilder, + IConventionContext context) + { + CheckIndexAttributesAndEnsureIndex( + modelBuilder.Metadata.GetEntityTypes(), false); + } + + private void CheckIndexAttributesAndEnsureIndex( + IEnumerable entityTypes, + bool shouldEnsureIndexOrFailSilently) + { + foreach (var entityType in entityTypes) { if (entityType.ClrType != null) { @@ -48,6 +89,11 @@ public virtual void ProcessModelFinalizing(IConventionModelBuilder modelBuilder, { if (ignoredMembers.Contains(propertyName)) { + if (shouldEnsureIndexOrFailSilently) + { + return; + } + if (indexAttribute.Name == null) { throw new InvalidOperationException( @@ -70,6 +116,11 @@ public virtual void ProcessModelFinalizing(IConventionModelBuilder modelBuilder, var property = entityType.FindProperty(propertyName); if (property == null) { + if (shouldEnsureIndexOrFailSilently) + { + return; + } + if (indexAttribute.Name == null) { throw new InvalidOperationException( @@ -89,20 +140,26 @@ public virtual void ProcessModelFinalizing(IConventionModelBuilder modelBuilder, } } - indexProperties.Add(property); + if (shouldEnsureIndexOrFailSilently) + { + indexProperties.Add(property); + } } - var indexBuilder = indexAttribute.Name == null - ? entityType.Builder.HasIndex( - indexProperties, fromDataAnnotation: true) - : entityType.Builder.HasIndex( - indexProperties, indexAttribute.Name, fromDataAnnotation: true); - - if (indexBuilder != null) + if (shouldEnsureIndexOrFailSilently) { - if (indexAttribute.GetIsUnique().HasValue) + var indexBuilder = indexAttribute.Name == null + ? entityType.Builder.HasIndex( + indexProperties, fromDataAnnotation: true) + : entityType.Builder.HasIndex( + indexProperties, indexAttribute.Name, fromDataAnnotation: true); + + if (indexBuilder != null) { - indexBuilder.IsUnique(indexAttribute.GetIsUnique().Value, fromDataAnnotation: true); + if (indexAttribute.GetIsUnique().HasValue) + { + indexBuilder.IsUnique(indexAttribute.GetIsUnique().Value, fromDataAnnotation: true); + } } } } diff --git a/src/EFCore/Metadata/Conventions/Infrastructure/ProviderConventionSetBuilder.cs b/src/EFCore/Metadata/Conventions/Infrastructure/ProviderConventionSetBuilder.cs index e9fa5b444cb..24d6a63a122 100644 --- a/src/EFCore/Metadata/Conventions/Infrastructure/ProviderConventionSetBuilder.cs +++ b/src/EFCore/Metadata/Conventions/Infrastructure/ProviderConventionSetBuilder.cs @@ -61,6 +61,7 @@ public virtual ConventionSet CreateConventionSet() var inversePropertyAttributeConvention = new InversePropertyAttributeConvention(Dependencies); var relationshipDiscoveryConvention = new RelationshipDiscoveryConvention(Dependencies); var servicePropertyDiscoveryConvention = new ServicePropertyDiscoveryConvention(Dependencies); + var indexAttributeConvention = new IndexAttributeConvention(Dependencies); conventionSet.EntityTypeAddedConventions.Add(new NotMappedEntityTypeAttributeConvention(Dependencies)); conventionSet.EntityTypeAddedConventions.Add(new OwnedEntityTypeAttributeConvention(Dependencies)); @@ -70,6 +71,7 @@ public virtual ConventionSet CreateConventionSet() conventionSet.EntityTypeAddedConventions.Add(propertyDiscoveryConvention); conventionSet.EntityTypeAddedConventions.Add(servicePropertyDiscoveryConvention); conventionSet.EntityTypeAddedConventions.Add(keyDiscoveryConvention); + conventionSet.EntityTypeAddedConventions.Add(indexAttributeConvention); conventionSet.EntityTypeAddedConventions.Add(inversePropertyAttributeConvention); conventionSet.EntityTypeAddedConventions.Add(relationshipDiscoveryConvention); conventionSet.EntityTypeAddedConventions.Add(new DerivedTypeDiscoveryConvention(Dependencies)); @@ -87,6 +89,7 @@ public virtual ConventionSet CreateConventionSet() conventionSet.EntityTypeBaseTypeChangedConventions.Add(propertyDiscoveryConvention); conventionSet.EntityTypeBaseTypeChangedConventions.Add(servicePropertyDiscoveryConvention); conventionSet.EntityTypeBaseTypeChangedConventions.Add(keyDiscoveryConvention); + conventionSet.EntityTypeBaseTypeChangedConventions.Add(indexAttributeConvention); conventionSet.EntityTypeBaseTypeChangedConventions.Add(inversePropertyAttributeConvention); conventionSet.EntityTypeBaseTypeChangedConventions.Add(relationshipDiscoveryConvention); conventionSet.EntityTypeBaseTypeChangedConventions.Add(foreignKeyIndexConvention); @@ -123,6 +126,7 @@ public virtual ConventionSet CreateConventionSet() conventionSet.PropertyAddedConventions.Add(backingFieldAttributeConvention); conventionSet.PropertyAddedConventions.Add(keyAttributeConvention); conventionSet.PropertyAddedConventions.Add(keyDiscoveryConvention); + conventionSet.PropertyAddedConventions.Add(indexAttributeConvention); conventionSet.PropertyAddedConventions.Add(foreignKeyPropertyDiscoveryConvention); conventionSet.EntityTypePrimaryKeyChangedConventions.Add(foreignKeyPropertyDiscoveryConvention); @@ -170,6 +174,7 @@ public virtual ConventionSet CreateConventionSet() conventionSet.ModelFinalizingConventions.Add(new ModelCleanupConvention(Dependencies)); conventionSet.ModelFinalizingConventions.Add(keyAttributeConvention); + conventionSet.ModelFinalizingConventions.Add(indexAttributeConvention); conventionSet.ModelFinalizingConventions.Add(foreignKeyAttributeConvention); conventionSet.ModelFinalizingConventions.Add(new ChangeTrackingStrategyConvention(Dependencies)); conventionSet.ModelFinalizingConventions.Add(new ConstructorBindingConvention(Dependencies)); @@ -182,7 +187,6 @@ public virtual ConventionSet CreateConventionSet() conventionSet.ModelFinalizingConventions.Add(new QueryFilterDefiningQueryRewritingConvention(Dependencies)); conventionSet.ModelFinalizingConventions.Add(inversePropertyAttributeConvention); conventionSet.ModelFinalizingConventions.Add(backingFieldConvention); - conventionSet.ModelFinalizingConventions.Add(new IndexAttributeConvention(Dependencies)); conventionSet.ModelFinalizedConventions.Add(new ValidatingConvention(Dependencies)); diff --git a/test/EFCore.Design.Tests/Migrations/ModelSnapshotSqlServerTest.cs b/test/EFCore.Design.Tests/Migrations/ModelSnapshotSqlServerTest.cs index ead78b3fde7..17a4dc0031a 100644 --- a/test/EFCore.Design.Tests/Migrations/ModelSnapshotSqlServerTest.cs +++ b/test/EFCore.Design.Tests/Migrations/ModelSnapshotSqlServerTest.cs @@ -164,6 +164,30 @@ private class EntityWithGenericProperty public TProperty Property { get; set; } } + [Index(nameof(FirstName), nameof(LastName))] + private class EntityWithIndexAttribute + { + public int Id { get; set; } + public string FirstName { get; set; } + public string LastName { get; set; } + } + + [Index(nameof(FirstName), nameof(LastName), Name = "NamedIndex")] + private class EntityWithNamedIndexAttribute + { + public int Id { get; set; } + public string FirstName { get; set; } + public string LastName { get; set; } + } + + [Index(nameof(FirstName), nameof(LastName), IsUnique = true)] + private class EntityWithUniqueIndexAttribute + { + public int Id { get; set; } + public string FirstName { get; set; } + public string LastName { get; set; } + } + public class TestOwner { public int Id { get; set; } @@ -2363,7 +2387,7 @@ public virtual void Key_multiple_annotations_are_stored_in_snapshot() #endregion - #region HasIndex + #region Index [ConditionalFact] public virtual void Index_annotations_are_stored_in_snapshot() @@ -2581,6 +2605,146 @@ public virtual void Index_with_default_constraint_name_exceeding_max() model => Assert.Equal(128, model.GetEntityTypes().First().GetIndexes().First().GetDatabaseName().Length)); } + [ConditionalFact] + public virtual void IndexAttribute_causes_column_to_have_key_or_index_column_length() + { + Test( + builder => builder.Entity(), + AddBoilerPlate( + GetHeading() + + @" + modelBuilder.Entity(""Microsoft.EntityFrameworkCore.Migrations.ModelSnapshotSqlServerTest+EntityWithIndexAttribute"", b => + { + b.Property(""Id"") + .ValueGeneratedOnAdd() + .HasColumnType(""int"") + .HasAnnotation(""SqlServer:ValueGenerationStrategy"", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property(""FirstName"") + .HasColumnType(""nvarchar(450)""); + + b.Property(""LastName"") + .HasColumnType(""nvarchar(450)""); + + b.HasKey(""Id""); + + b.HasIndex(""FirstName"", ""LastName""); + + b.ToTable(""EntityWithIndexAttribute""); + });"), + model => + Assert.Collection( + model.GetEntityTypes().First().GetIndexes().First().Properties, + p0 => + { + Assert.Equal("FirstName", p0.Name); + Assert.Equal("nvarchar(450)", p0.GetColumnType()); + }, + p1 => + { + Assert.Equal("LastName", p1.Name); + Assert.Equal("nvarchar(450)", p1.GetColumnType()); + } + )); + } + + [ConditionalFact] + public virtual void IndexAttribute_name_is_stored_in_snapshot() + { + Test( + builder => builder.Entity(), + AddBoilerPlate( + GetHeading() + + @" + modelBuilder.Entity(""Microsoft.EntityFrameworkCore.Migrations.ModelSnapshotSqlServerTest+EntityWithNamedIndexAttribute"", b => + { + b.Property(""Id"") + .ValueGeneratedOnAdd() + .HasColumnType(""int"") + .HasAnnotation(""SqlServer:ValueGenerationStrategy"", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property(""FirstName"") + .HasColumnType(""nvarchar(450)""); + + b.Property(""LastName"") + .HasColumnType(""nvarchar(450)""); + + b.HasKey(""Id""); + + b.HasIndex(new[] { ""FirstName"", ""LastName"" }, ""NamedIndex""); + + b.ToTable(""EntityWithNamedIndexAttribute""); + });"), + model => + { + var index = model.GetEntityTypes().First().GetIndexes().First(); + Assert.Equal("NamedIndex", index.Name); + Assert.Collection( + index.Properties, + p0 => + { + Assert.Equal("FirstName", p0.Name); + Assert.Equal("nvarchar(450)", p0.GetColumnType()); + }, + p1 => + { + Assert.Equal("LastName", p1.Name); + Assert.Equal("nvarchar(450)", p1.GetColumnType()); + } + ); + }); + } + + + [ConditionalFact] + public virtual void IndexAttribute_IsUnique_is_stored_in_snapshot() + { + Test( + builder => builder.Entity(), + AddBoilerPlate( + GetHeading() + + @" + modelBuilder.Entity(""Microsoft.EntityFrameworkCore.Migrations.ModelSnapshotSqlServerTest+EntityWithUniqueIndexAttribute"", b => + { + b.Property(""Id"") + .ValueGeneratedOnAdd() + .HasColumnType(""int"") + .HasAnnotation(""SqlServer:ValueGenerationStrategy"", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property(""FirstName"") + .HasColumnType(""nvarchar(450)""); + + b.Property(""LastName"") + .HasColumnType(""nvarchar(450)""); + + b.HasKey(""Id""); + + b.HasIndex(""FirstName"", ""LastName"") + .IsUnique() + .HasFilter(""[FirstName] IS NOT NULL AND [LastName] IS NOT NULL""); + + b.ToTable(""EntityWithUniqueIndexAttribute""); + });"), + model => + { + var index = model.GetEntityTypes().First().GetIndexes().First(); + Assert.True(index.IsUnique); + Assert.Collection( + index.Properties, + p0 => + { + Assert.Equal("FirstName", p0.Name); + Assert.Equal("nvarchar(450)", p0.GetColumnType()); + }, + p1 => + { + Assert.Equal("LastName", p1.Name); + Assert.Equal("nvarchar(450)", p1.GetColumnType()); + } + ); + }); + } + #endregion #region ForeignKey diff --git a/test/EFCore.Relational.Tests/Metadata/RelationalIndexTest.cs b/test/EFCore.Relational.Tests/Metadata/RelationalIndexTest.cs new file mode 100644 index 00000000000..5adf646b1f2 --- /dev/null +++ b/test/EFCore.Relational.Tests/Metadata/RelationalIndexTest.cs @@ -0,0 +1,67 @@ +// 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.Linq; +using Microsoft.EntityFrameworkCore.Metadata.Internal; +using Microsoft.EntityFrameworkCore.TestUtilities; +using Xunit; + +// ReSharper disable InconsistentNaming + +namespace Microsoft.EntityFrameworkCore.Metadata +{ + public class RelationalIndexExtensionsTest + { + [ConditionalFact] + public void IndexAttribute_database_name_can_be_overriden_using_fluent_api() + { + var modelBuilder = CreateConventionModelBuilder(); + var entityBuilder = modelBuilder.Entity(); + + foreach (var entityType in modelBuilder.Model.GetEntityTypes()) + { + foreach (var index in entityType.GetDeclaredIndexes()) + { + index.SetDatabaseName("My" + index.Name); + } + } + + modelBuilder.Model.FinalizeModel(); + + var index0 = (Internal.Index)entityBuilder.Metadata.GetIndexes().First(); + Assert.Equal(ConfigurationSource.DataAnnotation, index0.GetConfigurationSource()); + Assert.Equal("IndexOnAAndB", index0.Name); + Assert.Equal("MyIndexOnAAndB", index0.GetDatabaseName()); + Assert.Equal(ConfigurationSource.Explicit, index0.GetDatabaseNameConfigurationSource()); + Assert.True(index0.IsUnique); + Assert.Equal(ConfigurationSource.DataAnnotation, index0.GetIsUniqueConfigurationSource()); + Assert.Collection(index0.Properties, + prop0 => Assert.Equal("A", prop0.Name), + prop1 => Assert.Equal("B", prop1.Name)); + + var index1 = (Internal.Index)entityBuilder.Metadata.GetIndexes().Skip(1).First(); + Assert.Equal(ConfigurationSource.DataAnnotation, index1.GetConfigurationSource()); + Assert.Equal("IndexOnBAndC", index1.Name); + Assert.Equal("MyIndexOnBAndC", index1.GetDatabaseName()); + Assert.Equal(ConfigurationSource.Explicit, index1.GetDatabaseNameConfigurationSource()); + Assert.False(index1.IsUnique); + Assert.Null(index1.GetIsUniqueConfigurationSource()); + Assert.Collection(index1.Properties, + prop0 => Assert.Equal("B", prop0.Name), + prop1 => Assert.Equal("C", prop1.Name)); + } + + protected virtual ModelBuilder CreateConventionModelBuilder() => RelationalTestHelpers.Instance.CreateConventionBuilder(); + + [Index(nameof(A), nameof(B), Name = "IndexOnAAndB", IsUnique = true)] + [Index(nameof(B), nameof(C), Name = "IndexOnBAndC")] + private class EntityWithIndexes + { + public int Id { get; set; } + public int A { get; set; } + public int B { get; set; } + public int C { get; set; } + } + } +} diff --git a/test/EFCore.Relational.Tests/Storage/RelationalTypeMapperTestBase.cs b/test/EFCore.Relational.Tests/Storage/RelationalTypeMapperTestBase.cs index b0021aea033..d8f8813fa7e 100644 --- a/test/EFCore.Relational.Tests/Storage/RelationalTypeMapperTestBase.cs +++ b/test/EFCore.Relational.Tests/Storage/RelationalTypeMapperTestBase.cs @@ -24,6 +24,7 @@ protected IMutableModel CreateModel() builder.Entity().Property(e => e.Relationship2Id).IsUnicode(); builder.Entity().Property(e => e.PrecisionOnly).HasPrecision(16); builder.Entity().Property(e => e.PrecisionAndScale).HasPrecision(18, 7); + builder.Entity(); return builder.Model; } @@ -85,5 +86,12 @@ protected class MyRelatedType4 public string Relationship2Id { get; set; } public MyRelatedType3 Relationship2 { get; set; } } + + [Index(nameof(Name))] + protected class MyTypeWithIndexAttribute + { + public int Id { get; set; } + public string Name { get; set; } + } } } diff --git a/test/EFCore.SqlServer.Tests/SqlServerTypeMapperTest.cs b/test/EFCore.SqlServer.Tests/SqlServerTypeMapperTest.cs index 44bf0c09103..849b825bd0f 100644 --- a/test/EFCore.SqlServer.Tests/SqlServerTypeMapperTest.cs +++ b/test/EFCore.SqlServer.Tests/SqlServerTypeMapperTest.cs @@ -384,6 +384,30 @@ public void Does_indexed_column_SQL_Server_string_mapping(bool? unicode, bool? f Assert.Equal(450, typeMapping.CreateParameter(new TestCommand(), "Name", "Value").Size); } + [ConditionalTheory] + [InlineData(true, false)] + [InlineData(null, false)] + [InlineData(true, null)] + [InlineData(null, null)] + public void Does_IndexAttribute_column_SQL_Server_string_mapping(bool? unicode, bool? fixedLength) + { + var model = CreateModel(); + var entityType = model.FindEntityType(typeof(MyTypeWithIndexAttribute)); + var property = entityType.FindProperty("Name"); + property.SetIsUnicode(unicode); + property.SetIsFixedLength(fixedLength); + model.FinalizeModel(); + + var typeMapping = CreateTypeMapper().GetMapping(property); + + Assert.Null(typeMapping.DbType); + Assert.Equal("nvarchar(450)", typeMapping.StoreType); + Assert.Equal(450, typeMapping.Size); + Assert.True(typeMapping.IsUnicode); + Assert.False(typeMapping.IsFixedLength); + Assert.Equal(450, typeMapping.CreateParameter(new TestCommand(), "Name", "Value").Size); + } + [ConditionalTheory] [InlineData(false)] [InlineData(null)] @@ -611,6 +635,28 @@ public void Does_indexed_column_SQL_Server_string_mapping_ansi(bool? fixedLength Assert.Equal(900, typeMapping.CreateParameter(new TestCommand(), "Name", "Value").Size); } + [ConditionalTheory] + [InlineData(false)] + [InlineData(null)] + public void Does_IndexAttribute_column_SQL_Server_string_mapping_ansi(bool? fixedLength) + { + var model = CreateModel(); + var entityType = model.FindEntityType(typeof(MyTypeWithIndexAttribute)); + var property = entityType.FindProperty("Name"); + property.SetIsUnicode(false); + property.SetIsFixedLength(fixedLength); + model.FinalizeModel(); + + var typeMapping = CreateTypeMapper().GetMapping(property); + + Assert.Equal(DbType.AnsiString, typeMapping.DbType); + Assert.Equal("varchar(900)", typeMapping.StoreType); + Assert.Equal(900, typeMapping.Size); + Assert.False(typeMapping.IsUnicode); + Assert.False(typeMapping.IsFixedLength); + Assert.Equal(900, typeMapping.CreateParameter(new TestCommand(), "Name", "Value").Size); + } + [ConditionalTheory] [InlineData(false)] [InlineData(null)] diff --git a/test/EFCore.Tests/Metadata/Conventions/IndexAttributeConventionTest.cs b/test/EFCore.Tests/Metadata/Conventions/IndexAttributeConventionTest.cs index 18031628ed6..c508c8a15dc 100644 --- a/test/EFCore.Tests/Metadata/Conventions/IndexAttributeConventionTest.cs +++ b/test/EFCore.Tests/Metadata/Conventions/IndexAttributeConventionTest.cs @@ -29,14 +29,17 @@ public void IndexAttribute_overrides_configuration_from_convention() var entityBuilder = modelBuilder.Entity(typeof(EntityWithIndex), ConfigurationSource.Convention); entityBuilder.Property("Id", ConfigurationSource.Convention); - var propA = entityBuilder.Property("A", ConfigurationSource.Convention); - var propB = entityBuilder.Property("B", ConfigurationSource.Convention); + var propABuilder = entityBuilder.Property("A", ConfigurationSource.Convention); + var propBBuilder = entityBuilder.Property("B", ConfigurationSource.Convention); entityBuilder.PrimaryKey(new List { "Id" }, ConfigurationSource.Convention); - var indexProperties = new List { propA.Metadata.Name, propB.Metadata.Name }; + var indexProperties = new List { propABuilder.Metadata.Name, propBBuilder.Metadata.Name }; var indexBuilder = entityBuilder.HasIndex(indexProperties, "IndexOnAAndB", ConfigurationSource.Convention); indexBuilder.IsUnique(false, ConfigurationSource.Convention); + RunConvention(entityBuilder); + RunConvention(propABuilder); + RunConvention(propBBuilder); RunConvention(modelBuilder); var index = entityBuilder.Metadata.GetIndexes().Single(); @@ -74,12 +77,11 @@ public void IndexAttribute_can_be_overriden_using_explicit_configuration() public void IndexAttribute_with_no_property_names_throws() { var modelBuilder = InMemoryTestHelpers.Instance.CreateConventionBuilder(); - modelBuilder.Entity(); Assert.Equal( AbstractionsStrings.CollectionArgumentIsEmpty("propertyNames"), Assert.Throws( - () => modelBuilder.Model.FinalizeModel()).Message); + () => modelBuilder.Entity()).Message); } [InlineData(typeof(EntityWithInvalidNullIndexProperty))] @@ -89,12 +91,11 @@ public void IndexAttribute_with_no_property_names_throws() public void IndexAttribute_properties_cannot_include_whitespace(Type entityTypeWithInvalidIndex) { var modelBuilder = InMemoryTestHelpers.Instance.CreateConventionBuilder(); - modelBuilder.Entity(entityTypeWithInvalidIndex); Assert.Equal( AbstractionsStrings.CollectionArgumentHasEmptyElements("propertyNames"), Assert.Throws( - () => modelBuilder.Model.FinalizeModel()).Message); + () => modelBuilder.Entity(entityTypeWithInvalidIndex)).Message); } [ConditionalFact] @@ -210,16 +211,85 @@ public virtual void IndexAttribute_with_name_and_non_existent_property_causes_er () => modelBuilder.Model.FinalizeModel()).Message); } + [ConditionalFact] + public void IndexAttribute_index_replicated_to_derived_type_when_base_type_changes() + { + var modelBuilder = InMemoryTestHelpers.Instance.CreateConventionBuilder(); + var baseEntityBuilder = modelBuilder.Entity(typeof(BaseEntityWithIndex)); + var derivedEntityBuilder = modelBuilder.Entity(); + + // Index is created on base type but not on derived type. + Assert.NotNull(derivedEntityBuilder.Metadata.BaseType); + Assert.Single(baseEntityBuilder.Metadata.GetDeclaredIndexes()); + Assert.Empty(derivedEntityBuilder.Metadata.GetDeclaredIndexes()); + + derivedEntityBuilder.HasBaseType((string)null); + + // Now the Index is replicated on the derived type. + Assert.Null(derivedEntityBuilder.Metadata.BaseType); + var index = (Metadata.Internal.Index) + Assert.Single(derivedEntityBuilder.Metadata.GetDeclaredIndexes()); + Assert.Equal(ConfigurationSource.DataAnnotation, index.GetConfigurationSource()); + Assert.Equal("IndexOnBaseGetsReplicatedToDerived", index.Name); + Assert.False(index.IsUnique); + Assert.Null(index.GetIsUniqueConfigurationSource()); + var indexProperty = Assert.Single(index.Properties); + Assert.Equal("B", indexProperty.Name); + + // Check there are no errors. + modelBuilder.Model.FinalizeModel(); + } + + [ConditionalFact] + public void IndexAttribute_index_is_created_when_missing_property_added() + { + var modelBuilder = InMemoryTestHelpers.Instance.CreateConventionBuilder(); + var entityBuilder = modelBuilder.Entity(typeof(EntityWithIndexOnShadowProperty)); + + Assert.Empty(entityBuilder.Metadata.GetDeclaredIndexes()); + + entityBuilder.Property("Y"); + + var index = (Metadata.Internal.Index) + Assert.Single(entityBuilder.Metadata.GetDeclaredIndexes()); + + Assert.Equal(ConfigurationSource.DataAnnotation, index.GetConfigurationSource()); + Assert.Equal("IndexOnShadowProperty", index.Name); + Assert.Collection(index.Properties, + prop0 => Assert.Equal("X", prop0.Name), + prop1 => Assert.Equal("Y", prop1.Name)); + + // Check there are no errors. + modelBuilder.Model.FinalizeModel(); + } + #endregion + private void RunConvention(InternalEntityTypeBuilder entityTypeBuilder) + { + var context = new ConventionContext( + entityTypeBuilder.Metadata.Model.ConventionDispatcher); + + CreateIndexAttributeConvention().ProcessEntityTypeAdded(entityTypeBuilder, context); + } + + private void RunConvention(InternalPropertyBuilder propertyBuilder) + { + var context = new ConventionContext( + propertyBuilder.Metadata.DeclaringEntityType.Model.ConventionDispatcher); + + CreateIndexAttributeConvention().ProcessPropertyAdded(propertyBuilder, context); + } + private void RunConvention(InternalModelBuilder modelBuilder) { var context = new ConventionContext(modelBuilder.Metadata.ConventionDispatcher); - new IndexAttributeConvention(CreateDependencies()) - .ProcessModelFinalizing(modelBuilder, context); + CreateIndexAttributeConvention().ProcessModelFinalizing(modelBuilder, context); } + private IndexAttributeConvention CreateIndexAttributeConvention() => new IndexAttributeConvention(CreateDependencies()); + private ProviderConventionSetBuilderDependencies CreateDependencies() => InMemoryTestHelpers.Instance.CreateContextServices().GetRequiredService(); @@ -321,5 +391,26 @@ private class EntityIndexWithNonExistentProperty public int A { get; set; } public int B { get; set; } } + + [Index(nameof(B), Name = "IndexOnBaseGetsReplicatedToDerived")] + private class BaseEntityWithIndex + { + public int Id { get; set; } + public int A { get; set; } + public int B { get; set; } + } + + private class DerivedEntity : BaseEntityWithIndex + { + public int C { get; set; } + public int D { get; set; } + } + + [Index(nameof(X), "Y", Name = "IndexOnShadowProperty")] + private class EntityWithIndexOnShadowProperty + { + public int Id { get; set; } + public int X { get; set; } + } } }