diff --git a/src/EFCore.Abstractions/IndexAttribute.cs b/src/EFCore.Abstractions/IndexAttribute.cs new file mode 100644 index 00000000000..c0e0a083940 --- /dev/null +++ b/src/EFCore.Abstractions/IndexAttribute.cs @@ -0,0 +1,58 @@ +// 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 System.Linq; +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Utilities; + +namespace Microsoft.EntityFrameworkCore +{ + /// + /// Specifies an index to be generated in the database. + /// + [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] + public sealed class IndexAttribute : Attribute + { + private static readonly bool DefaultIsUnique = false; + private bool? _isUnique; + + /// + /// Initializes a new instance of the class. + /// + /// The properties which constitute the index, in order (there must be at least one). + public IndexAttribute(params string[] propertyNames) + { + Check.NotEmpty(propertyNames, nameof(propertyNames)); + Check.HasNoEmptyElements(propertyNames, nameof(propertyNames)); + PropertyNames = propertyNames.ToList(); + } + + /// + /// The properties which constitute the index, in order. + /// + public List PropertyNames { get; } + + /// + /// The name of the index. + /// + public string Name { get; [param: NotNull] set; } + + + /// + /// Whether the index is unique. + /// + public bool IsUnique + { + get => _isUnique ?? DefaultIsUnique; + set => _isUnique = value; + } + + /// + /// Use this method if you want to know the uniqueness of + /// the index or if it was not specified. + /// + public bool? GetIsUnique() => _isUnique; + } +} diff --git a/src/EFCore.Abstractions/Properties/AbstractionsStrings.Designer.cs b/src/EFCore.Abstractions/Properties/AbstractionsStrings.Designer.cs index 7b47addd73d..f770c85bce8 100644 --- a/src/EFCore.Abstractions/Properties/AbstractionsStrings.Designer.cs +++ b/src/EFCore.Abstractions/Properties/AbstractionsStrings.Designer.cs @@ -37,6 +37,14 @@ public static string CollectionArgumentIsEmpty([CanBeNull] object argumentName) GetString("CollectionArgumentIsEmpty", nameof(argumentName)), argumentName); + /// + /// The collection argument '{argumentName}' must not contain any empty elements. + /// + public static string CollectionArgumentHasEmptyElements([CanBeNull] object argumentName) + => string.Format( + GetString("CollectionArgumentHasEmptyElements", nameof(argumentName)), + argumentName); + 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 f696e70e8e5..7d822b1b27f 100644 --- a/src/EFCore.Abstractions/Properties/AbstractionsStrings.resx +++ b/src/EFCore.Abstractions/Properties/AbstractionsStrings.resx @@ -123,4 +123,7 @@ The collection argument '{argumentName}' must contain at least one element. + + The collection argument '{argumentName}' must not contain any empty elements. + \ No newline at end of file diff --git a/src/EFCore.Design/Migrations/Design/CSharpSnapshotGenerator.cs b/src/EFCore.Design/Migrations/Design/CSharpSnapshotGenerator.cs index cb63fb5723d..1dd58982534 100644 --- a/src/EFCore.Design/Migrations/Design/CSharpSnapshotGenerator.cs +++ b/src/EFCore.Design/Migrations/Design/CSharpSnapshotGenerator.cs @@ -11,6 +11,7 @@ using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Metadata.Builders; using Microsoft.EntityFrameworkCore.Metadata.Internal; +using Microsoft.EntityFrameworkCore.Scaffolding.Internal; using Microsoft.EntityFrameworkCore.Storage; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using Microsoft.EntityFrameworkCore.Utilities; @@ -743,6 +744,8 @@ protected virtual void GenerateIndex( Check.NotNull(index, nameof(index)); Check.NotNull(stringBuilder, nameof(stringBuilder)); + // Note - method names below are meant to be hard-coded + // because old snapshot files will fail if they are changed stringBuilder .AppendLine() .Append(builderName) @@ -752,6 +755,15 @@ protected virtual void GenerateIndex( using (stringBuilder.Indent()) { + if (index.Name != null) + { + stringBuilder + .AppendLine() + .Append(".HasName(") + .Append(Code.Literal(index.Name)) + .Append(")"); + } + if (index.IsUnique) { stringBuilder @@ -777,10 +789,8 @@ protected virtual void GenerateIndexAnnotations( IgnoreAnnotations( annotations, - RelationalAnnotationNames.TableIndexMappings); + CSharpModelGenerator.IgnoredIndexAnnotations); - GenerateFluentApiForAnnotation( - ref annotations, RelationalAnnotationNames.Name, nameof(RelationalIndexBuilderExtensions.HasName), stringBuilder); GenerateFluentApiForAnnotation( ref annotations, RelationalAnnotationNames.Filter, nameof(RelationalIndexBuilderExtensions.HasFilter), stringBuilder); @@ -1213,6 +1223,27 @@ protected virtual void IgnoreAnnotations( } } + /// + /// Removes ignored annotations. + /// + /// The annotations to remove from. + /// The ignored annotation names. + protected virtual void IgnoreAnnotations( + [NotNull] IList annotations, [NotNull] IReadOnlyList annotationNames) + { + Check.NotNull(annotations, nameof(annotations)); + Check.NotNull(annotationNames, nameof(annotationNames)); + + foreach (var annotationName in annotationNames) + { + var annotation = annotations.FirstOrDefault(a => a.Name == annotationName); + if (annotation != null) + { + annotations.Remove(annotation); + } + } + } + /// /// Removes ignored annotations. /// diff --git a/src/EFCore.Design/Scaffolding/Internal/CSharpDbContextGenerator.cs b/src/EFCore.Design/Scaffolding/Internal/CSharpDbContextGenerator.cs index c370eacf0ae..941684b3aef 100644 --- a/src/EFCore.Design/Scaffolding/Internal/CSharpDbContextGenerator.cs +++ b/src/EFCore.Design/Scaffolding/Internal/CSharpDbContextGenerator.cs @@ -414,7 +414,15 @@ private void GenerateEntityType(IEntityType entityType, bool useDataAnnotations) foreach (var index in entityType.GetIndexes()) { - GenerateIndex(index); + // If there are annotations that cannot be represented + // using an IndexAttribute then use fluent API even + // if useDataAnnotations is true. + if (!useDataAnnotations + || index.GetAnnotations().Any( + a => !CSharpModelGenerator.IgnoredIndexAnnotations.Contains(a.Name))) + { + GenerateIndex(index); + } } foreach (var property in entityType.GetProperties()) @@ -582,14 +590,16 @@ private void GenerateIndex(IIndex index) var annotations = index.GetAnnotations().ToList(); - RemoveAnnotation(ref annotations, RelationalAnnotationNames.TableIndexMappings); + foreach (var annotation in CSharpModelGenerator.IgnoredIndexAnnotations) + { + RemoveAnnotation(ref annotations, annotation); + } - if (!string.IsNullOrEmpty((string)index[RelationalAnnotationNames.Name])) + if (index.Name != null) { lines.Add( - $".{nameof(RelationalIndexBuilderExtensions.HasName)}" + - $"({_code.Literal(index.GetName())})"); - RemoveAnnotation(ref annotations, RelationalAnnotationNames.Name); + $".{nameof(IndexBuilder.HasName)}" + + $"({_code.Literal(index.GetDatabaseName())})"); } if (index.IsUnique) diff --git a/src/EFCore.Design/Scaffolding/Internal/CSharpEntityTypeGenerator.cs b/src/EFCore.Design/Scaffolding/Internal/CSharpEntityTypeGenerator.cs index 1f2a1f89a46..d24f22b9099 100644 --- a/src/EFCore.Design/Scaffolding/Internal/CSharpEntityTypeGenerator.cs +++ b/src/EFCore.Design/Scaffolding/Internal/CSharpEntityTypeGenerator.cs @@ -136,6 +136,7 @@ protected virtual void GenerateEntityTypeDataAnnotations( GenerateKeylessAttribute(entityType); GenerateTableAttribute(entityType); + GenerateIndexAttributes(entityType); } private void GenerateKeylessAttribute(IEntityType entityType) @@ -170,6 +171,39 @@ private void GenerateTableAttribute(IEntityType entityType) } } + private void GenerateIndexAttributes(IEntityType entityType) + { + // Do not generate IndexAttributes for indexes which + // would be generated anyway by convention. + foreach (var index in entityType.GetIndexes().Where(i => + ConfigurationSource.Convention != ((IConventionIndex)i).GetConfigurationSource())) + { + // If there are annotations that cannot be represented + // using an IndexAttribute then use fluent API instead. + if (!index.GetAnnotations().Any( + a => !CSharpModelGenerator.IgnoredIndexAnnotations.Contains(a.Name))) + { + var indexAttribute = new AttributeWriter(nameof(IndexAttribute)); + foreach (var property in index.Properties) + { + indexAttribute.AddParameter($"nameof({property.Name})"); + } + + if (index.Name != null) + { + indexAttribute.AddParameter($"{nameof(IndexAttribute.Name)} = {_code.Literal(index.Name)}"); + } + + if (index.IsUnique) + { + indexAttribute.AddParameter($"{nameof(IndexAttribute.IsUnique)} = {_code.Literal(index.IsUnique)}"); + } + + _sb.AppendLine(indexAttribute.ToString()); + } + } + } + /// /// 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.Design/Scaffolding/Internal/CSharpModelGenerator.cs b/src/EFCore.Design/Scaffolding/Internal/CSharpModelGenerator.cs index d1e253a3fca..c3254f6bd21 100644 --- a/src/EFCore.Design/Scaffolding/Internal/CSharpModelGenerator.cs +++ b/src/EFCore.Design/Scaffolding/Internal/CSharpModelGenerator.cs @@ -2,6 +2,7 @@ // 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 System.IO; using JetBrains.Annotations; using Microsoft.EntityFrameworkCore.Diagnostics; @@ -128,5 +129,11 @@ public override ScaffoldedModel GenerateModel( return resultingFiles; } + + /// + /// The set of annotations ignored for the purposes of code generation for indexes. + /// + public static IReadOnlyList IgnoredIndexAnnotations + => new List { RelationalAnnotationNames.TableIndexMappings }; } } diff --git a/src/EFCore.Design/Scaffolding/Internal/RelationalScaffoldingModelFactory.cs b/src/EFCore.Design/Scaffolding/Internal/RelationalScaffoldingModelFactory.cs index 9c0c6363e51..bca1dbcc0ba 100644 --- a/src/EFCore.Design/Scaffolding/Internal/RelationalScaffoldingModelFactory.cs +++ b/src/EFCore.Design/Scaffolding/Internal/RelationalScaffoldingModelFactory.cs @@ -815,7 +815,7 @@ protected virtual IMutableForeignKey VisitForeignKey([NotNull] ModelBuilder mode _reporter.WriteWarning( DesignStrings.ForeignKeyPrincipalEndContainsNullableColumns( foreignKey.DisplayName(), - index.GetName(), + index.GetDatabaseName(), nullablePrincipalProperties.Select(tuple => tuple.column.DisplayName()).ToList() .Aggregate((a, b) => a + "," + b))); diff --git a/src/EFCore.Relational/Diagnostics/IndexEventData.cs b/src/EFCore.Relational/Diagnostics/IndexEventData.cs new file mode 100644 index 00000000000..8bd79f289ff --- /dev/null +++ b/src/EFCore.Relational/Diagnostics/IndexEventData.cs @@ -0,0 +1,54 @@ +// 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 System.Diagnostics; +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Metadata; + +namespace Microsoft.EntityFrameworkCore.Diagnostics +{ + /// + /// A event payload class for + /// the events involving an invalid index. + /// + public class IndexEventData : EventData + { + /// + /// Constructs the event payload for events involving an invalid index. + /// + /// The event definition. + /// A delegate that generates a log message for this event. + /// The entity type on which the index is defined. + /// The name of the index. + /// The names of the properties which define the index. + public IndexEventData( + [NotNull] EventDefinitionBase eventDefinition, + [NotNull] Func messageGenerator, + [NotNull] IEntityType entityType, + [CanBeNull] string indexName, + [NotNull] List indexPropertyNames) + : base(eventDefinition, messageGenerator) + { + EntityType = entityType; + Name = indexName; + PropertyNames = indexPropertyNames; + } + + /// + /// The entity type on which the index is defined. + /// + public virtual IEntityType EntityType { get; } + + /// + /// The name of the index. + /// + public virtual string Name { get; } + + /// + /// The list of properties which define the index. + /// + public virtual List PropertyNames { get; } + } +} diff --git a/src/EFCore.Relational/Diagnostics/IndexInvalidPropertiesEventData.cs b/src/EFCore.Relational/Diagnostics/IndexInvalidPropertiesEventData.cs new file mode 100644 index 00000000000..f55a60f5fc9 --- /dev/null +++ b/src/EFCore.Relational/Diagnostics/IndexInvalidPropertiesEventData.cs @@ -0,0 +1,86 @@ +// 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 System.Diagnostics; +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Metadata; + +namespace Microsoft.EntityFrameworkCore.Diagnostics +{ + /// + /// A event payload class for the + /// event. + /// + public class IndexInvalidPropertiesEventData : EventData + { + /// + /// Constructs the event payload for the event. + /// + /// The event definition. + /// A delegate that generates a log message for this event. + /// The entity type on which the index is defined. + /// The name of the index. + /// The names of the properties which define the index. + /// The name of the first property name which causes this event. + /// The tables mapped to the first property. + /// The name of the second property name which causes this event. + /// The tables mapped to the second property. + public IndexInvalidPropertiesEventData( + [NotNull] EventDefinitionBase eventDefinition, + [NotNull] Func messageGenerator, + [NotNull] IEntityType entityType, + [CanBeNull] string indexName, + [NotNull] List indexPropertyNames, + [NotNull] string property1Name, + [NotNull] List<(string Table, string Schema)> tablesMappedToProperty1, + [NotNull] string property2Name, + [NotNull] List<(string Table, string Schema)> tablesMappedToProperty2) + : base(eventDefinition, messageGenerator) + { + EntityType = entityType; + Name = indexName; + PropertyNames = indexPropertyNames; + Property1Name = property1Name; + TablesMappedToProperty1 = tablesMappedToProperty1; + Property2Name = property2Name; + TablesMappedToProperty2 = tablesMappedToProperty2; + } + + /// + /// The entity type on which the index is defined. + /// + public virtual IEntityType EntityType { get; } + + /// + /// The name of the index. + /// + public virtual string Name { get; } + + /// + /// The list of properties which define the index. + /// + public virtual List PropertyNames { get; } + + /// + /// The name of the first property. + /// + public virtual string Property1Name { get; } + + /// + /// The tables mapped to the first property. + /// + public virtual List<(string Table, string Schema)> TablesMappedToProperty1 { get; } + + /// + /// The name of the second property. + /// + public virtual string Property2Name { get; } + + /// + /// The tables mapped to the second property. + /// + public virtual List<(string Table, string Schema)> TablesMappedToProperty2 { get; } + } +} diff --git a/src/EFCore.Relational/Diagnostics/IndexInvalidPropertyEventData.cs b/src/EFCore.Relational/Diagnostics/IndexInvalidPropertyEventData.cs new file mode 100644 index 00000000000..cbaaf9d87b6 --- /dev/null +++ b/src/EFCore.Relational/Diagnostics/IndexInvalidPropertyEventData.cs @@ -0,0 +1,62 @@ +// 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 System.Diagnostics; +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Metadata; + +namespace Microsoft.EntityFrameworkCore.Diagnostics +{ + /// + /// A event payload class for + /// the events involving an invalid property name on an index. + /// + public class IndexInvalidPropertyEventData : EventData + { + /// + /// Constructs the event payload for indexes with a invalid property. + /// + /// The event definition. + /// A delegate that generates a log message for this event. + /// The entity type on which the index is defined. + /// The name of the index. + /// The names of the properties which define the index. + /// The property name which is invalid. + public IndexInvalidPropertyEventData( + [NotNull] EventDefinitionBase eventDefinition, + [NotNull] Func messageGenerator, + [NotNull] IEntityType entityType, + [CanBeNull] string indexName, + [NotNull] List indexPropertyNames, + [NotNull] string invalidPropertyName) + : base(eventDefinition, messageGenerator) + { + EntityType = entityType; + Name = indexName; + PropertyNames = indexPropertyNames; + InvalidPropertyName = invalidPropertyName; + } + + /// + /// The entity type on which the index is defined. + /// + public virtual IEntityType EntityType { get; } + + /// + /// The name of the index. + /// + public virtual string Name { get; } + + /// + /// The list of properties which define the index. + /// + public virtual List PropertyNames { get; } + + /// + /// The name of the invalid property. + /// + public virtual string InvalidPropertyName { get; } + } +} diff --git a/src/EFCore.Relational/Diagnostics/RelationalEventId.cs b/src/EFCore.Relational/Diagnostics/RelationalEventId.cs index 1e788ac7043..59d6799b24c 100644 --- a/src/EFCore.Relational/Diagnostics/RelationalEventId.cs +++ b/src/EFCore.Relational/Diagnostics/RelationalEventId.cs @@ -75,6 +75,9 @@ private enum Id // Model validation events ModelValidationKeyDefaultValueWarning = CoreEventId.RelationalBaseId + 600, BoolWithDefaultWarning, + AllIndexPropertiesNotToMappedToAnyTable, + IndexPropertiesBothMappedAndNotMappedToTable, + IndexPropertiesMappedToNonOverlappingTables, // Update events BatchReadyForExecution = CoreEventId.RelationalBaseId + 700, @@ -553,6 +556,45 @@ private enum Id /// public static readonly EventId BoolWithDefaultWarning = MakeValidationId(Id.BoolWithDefaultWarning); + /// + /// + /// An index specifies properties all of which are not mapped to a column in any table. + /// + /// + /// This event is in the category. + /// + /// + /// This event uses the payload when used with a . + /// + /// + public static readonly EventId AllIndexPropertiesNotToMappedToAnyTable = MakeValidationId(Id.AllIndexPropertiesNotToMappedToAnyTable); + + /// + /// + /// An index specifies properties some of which are mapped and some of which are not mapped to a column in a table. + /// + /// + /// This event is in the category. + /// + /// + /// This event uses the payload when used with a . + /// + /// + public static readonly EventId IndexPropertiesBothMappedAndNotMappedToTable = MakeValidationId(Id.IndexPropertiesBothMappedAndNotMappedToTable); + + /// + /// + /// An index specifies properties which map to columns on non-overlapping tables. + /// + /// + /// This event is in the category. + /// + /// + /// This event uses the payload when used with a . + /// + /// + public static readonly EventId IndexPropertiesMappedToNonOverlappingTables = MakeValidationId(Id.IndexPropertiesMappedToNonOverlappingTables); + private static readonly string _updatePrefix = DbLoggerCategory.Update.Name + "."; private static EventId MakeUpdateId(Id id) => new EventId((int)id, _updatePrefix + id); diff --git a/src/EFCore.Relational/Diagnostics/RelationalLoggerExtensions.cs b/src/EFCore.Relational/Diagnostics/RelationalLoggerExtensions.cs index 0cc06180ec0..79a53c893c0 100644 --- a/src/EFCore.Relational/Diagnostics/RelationalLoggerExtensions.cs +++ b/src/EFCore.Relational/Diagnostics/RelationalLoggerExtensions.cs @@ -7,6 +7,7 @@ using System.Data.Common; using System.Diagnostics; using System.Globalization; +using System.Linq; using System.Linq.Expressions; using System.Reflection; using System.Threading; @@ -14,12 +15,14 @@ using System.Transactions; using JetBrains.Annotations; using Microsoft.EntityFrameworkCore.Diagnostics.Internal; +using Microsoft.EntityFrameworkCore.Internal; using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Migrations.Internal; using Microsoft.EntityFrameworkCore.Storage; using Microsoft.EntityFrameworkCore.Storage.Internal; using Microsoft.EntityFrameworkCore.Update; +using Microsoft.EntityFrameworkCore.Utilities; using Microsoft.Extensions.Logging; using IsolationLevel = System.Data.IsolationLevel; @@ -3600,5 +3603,290 @@ private static string BatchSmallerThanMinBatchSize(EventDefinitionBase definitio var p = (MinBatchSizeEventData)payload; return d.GenerateMessage(p.CommandCount, p.MinBatchSize); } + + /// + /// Logs the event. + /// + /// The diagnostics logger to use. + /// The entity type on which the index is defined. + /// The index on the entity type. + public static void AllIndexPropertiesNotToMappedToAnyTable( + [NotNull] this IDiagnosticsLogger diagnostics, + [NotNull] IEntityType entityType, + [NotNull] IIndex index) + { + if (index.Name == null) + { + var definition = RelationalResources.LogUnnamedIndexAllPropertiesNotToMappedToAnyTable(diagnostics); + + if (diagnostics.ShouldLog(definition)) + { + definition.Log(diagnostics, + entityType.DisplayName(), + index.Properties.Format()); + } + + if (diagnostics.NeedsEventData(definition, out var diagnosticSourceEnabled, out var simpleLogEnabled)) + { + var eventData = new IndexEventData( + definition, + UnnamedIndexAllPropertiesNotToMappedToAnyTable, + entityType, + null, + index.Properties.Select(p => p.Name).ToList()); + + diagnostics.DispatchEventData(definition, eventData, diagnosticSourceEnabled, simpleLogEnabled); + } + } + else + { + var definition = RelationalResources.LogNamedIndexAllPropertiesNotToMappedToAnyTable(diagnostics); + + if (diagnostics.ShouldLog(definition)) + { + definition.Log(diagnostics, + index.Name, + entityType.DisplayName(), + index.Properties.Format()); + } + + if (diagnostics.NeedsEventData(definition, out var diagnosticSourceEnabled, out var simpleLogEnabled)) + { + var eventData = new IndexEventData( + definition, + NamedIndexAllPropertiesNotToMappedToAnyTable, + entityType, + index.Name, + index.Properties.Select(p => p.Name).ToList()); + + diagnostics.DispatchEventData(definition, eventData, diagnosticSourceEnabled, simpleLogEnabled); + } + } + } + + private static string UnnamedIndexAllPropertiesNotToMappedToAnyTable(EventDefinitionBase definition, EventData payload) + { + var d = (EventDefinition)definition; + var p = (IndexEventData)payload; + return d.GenerateMessage( + p.EntityType.DisplayName(), + p.PropertyNames.Format()); + } + + private static string NamedIndexAllPropertiesNotToMappedToAnyTable(EventDefinitionBase definition, EventData payload) + { + var d = (EventDefinition)definition; + var p = (IndexEventData)payload; + return d.GenerateMessage( + p.Name, + p.EntityType.DisplayName(), + p.PropertyNames.Format()); + } + + /// + /// Logs the event. + /// + /// The diagnostics logger to use. + /// The entity type on which the index is defined. + /// The index on the entity type. + /// The name of the property which is not mapped. + public static void IndexPropertiesBothMappedAndNotMappedToTable( + [NotNull] this IDiagnosticsLogger diagnostics, + [NotNull] IEntityType entityType, + [NotNull] IIndex index, + [NotNull] string unmappedPropertyName) + { + if (index.Name == null) + { + var definition = RelationalResources.LogUnnamedIndexPropertiesBothMappedAndNotMappedToTable(diagnostics); + + if (diagnostics.ShouldLog(definition)) + { + definition.Log(diagnostics, + entityType.DisplayName(), + index.Properties.Format(), + unmappedPropertyName); + } + + if (diagnostics.NeedsEventData(definition, out var diagnosticSourceEnabled, out var simpleLogEnabled)) + { + var eventData = new IndexInvalidPropertyEventData( + definition, + UnnamedIndexPropertiesBothMappedAndNotMappedToTable, + entityType, + null, + index.Properties.Select(p => p.Name).ToList(), + unmappedPropertyName); + + diagnostics.DispatchEventData(definition, eventData, diagnosticSourceEnabled, simpleLogEnabled); + } + } + else + { + var definition = RelationalResources.LogNamedIndexPropertiesBothMappedAndNotMappedToTable(diagnostics); + + if (diagnostics.ShouldLog(definition)) + { + definition.Log(diagnostics, + index.Name, + entityType.DisplayName(), + index.Properties.Format(), + unmappedPropertyName); + } + + if (diagnostics.NeedsEventData(definition, out var diagnosticSourceEnabled, out var simpleLogEnabled)) + { + var eventData = new IndexInvalidPropertyEventData( + definition, + NamedIndexPropertiesBothMappedAndNotMappedToTable, + entityType, + index.Name, + index.Properties.Select(p => p.Name).ToList(), + unmappedPropertyName); + + diagnostics.DispatchEventData(definition, eventData, diagnosticSourceEnabled, simpleLogEnabled); + } + } + } + + private static string UnnamedIndexPropertiesBothMappedAndNotMappedToTable(EventDefinitionBase definition, EventData payload) + { + var d = (EventDefinition)definition; + var p = (IndexInvalidPropertyEventData)payload; + return d.GenerateMessage( + p.EntityType.DisplayName(), + p.PropertyNames.Format(), + p.InvalidPropertyName); + } + + private static string NamedIndexPropertiesBothMappedAndNotMappedToTable(EventDefinitionBase definition, EventData payload) + { + var d = (EventDefinition)definition; + var p = (IndexInvalidPropertyEventData)payload; + return d.GenerateMessage( + p.Name, + p.EntityType.DisplayName(), + p.PropertyNames.Format(), + p.InvalidPropertyName); + } + + /// + /// Logs the event. + /// + /// The diagnostics logger to use. + /// The entity type on which the index is defined. + /// The index on the entity type. + /// The first property name which is invalid. + /// The tables mapped to the first property. + /// The second property name which is invalid. + /// The tables mapped to the second property. + public static void IndexPropertiesMappedToNonOverlappingTables( + [NotNull] this IDiagnosticsLogger diagnostics, + [NotNull] IEntityType entityType, + [NotNull] IIndex index, + [NotNull] string property1Name, + [NotNull] List<(string Table, string Schema)> tablesMappedToProperty1, + [NotNull] string property2Name, + [NotNull] List<(string Table, string Schema)> tablesMappedToProperty2) + { + if (index.Name == null) + { + var definition = RelationalResources.LogUnnamedIndexPropertiesMappedToNonOverlappingTables(diagnostics); + + if (diagnostics.ShouldLog(definition)) + { + definition.Log(diagnostics, + entityType.DisplayName(), + index.Properties.Format(), + property1Name, + tablesMappedToProperty1.FormatTables(), + property2Name, + tablesMappedToProperty2.FormatTables()); + } + + if (diagnostics.NeedsEventData(definition, out var diagnosticSourceEnabled, out var simpleLogEnabled)) + { + var eventData = new IndexInvalidPropertiesEventData( + definition, + UnnamedIndexPropertiesMappedToNonOverlappingTables, + entityType, + null, + index.Properties.Select(p => p.Name).ToList(), + property1Name, + tablesMappedToProperty1, + property2Name, + tablesMappedToProperty2); + + diagnostics.DispatchEventData(definition, eventData, diagnosticSourceEnabled, simpleLogEnabled); + } + } + else + { + var definition = RelationalResources.LogNamedIndexPropertiesMappedToNonOverlappingTables(diagnostics); + + if (diagnostics.ShouldLog(definition)) + { + definition.Log(diagnostics, + l => l.Log( + definition.Level, + definition.EventId, + definition.MessageFormat, + index.Name, + entityType.DisplayName(), + index.Properties.Format(), + property1Name, + tablesMappedToProperty1.FormatTables(), + property2Name, + tablesMappedToProperty2.FormatTables())); + } + + if (diagnostics.NeedsEventData(definition, out var diagnosticSourceEnabled, out var simpleLogEnabled)) + { + var eventData = new IndexInvalidPropertiesEventData( + definition, + NamedIndexPropertiesMappedToNonOverlappingTables, + entityType, + index.Name, + index.Properties.Select(p => p.Name).ToList(), + property1Name, + tablesMappedToProperty1, + property2Name, + tablesMappedToProperty2); + + diagnostics.DispatchEventData(definition, eventData, diagnosticSourceEnabled, simpleLogEnabled); + } + } + } + + private static string UnnamedIndexPropertiesMappedToNonOverlappingTables(EventDefinitionBase definition, EventData payload) + { + var d = (EventDefinition)definition; + var p = (IndexInvalidPropertiesEventData)payload; + return d.GenerateMessage( + p.EntityType.DisplayName(), + p.PropertyNames.Format(), + p.Property1Name, + p.TablesMappedToProperty1.FormatTables(), + p.Property2Name, + p.TablesMappedToProperty2.FormatTables()); + } + + private static string NamedIndexPropertiesMappedToNonOverlappingTables(EventDefinitionBase definition, EventData payload) + { + var d = (FallbackEventDefinition)definition; + var p = (IndexInvalidPropertiesEventData)payload; + return d.GenerateMessage( + l => l.Log( + d.Level, + d.EventId, + d.MessageFormat, + p.Name, + p.EntityType.DisplayName(), + p.PropertyNames.Format(), + p.Property1Name, + p.TablesMappedToProperty1.FormatTables(), + p.Property2Name, + p.TablesMappedToProperty2.FormatTables())); + } } } diff --git a/src/EFCore.Relational/Diagnostics/RelationalLoggingDefinitions.cs b/src/EFCore.Relational/Diagnostics/RelationalLoggingDefinitions.cs index e561e55352a..eb528cc2a3d 100644 --- a/src/EFCore.Relational/Diagnostics/RelationalLoggingDefinitions.cs +++ b/src/EFCore.Relational/Diagnostics/RelationalLoggingDefinitions.cs @@ -366,5 +366,59 @@ public abstract class RelationalLoggingDefinitions : LoggingDefinitions /// [EntityFrameworkInternal] public EventDefinitionBase LogValueConversionSqlLiteralWarning; + + /// + /// 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. + /// + [EntityFrameworkInternal] + public EventDefinitionBase LogNamedIndexAllPropertiesNotToMappedToAnyTable; + + /// + /// 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. + /// + [EntityFrameworkInternal] + public EventDefinitionBase LogUnnamedIndexAllPropertiesNotToMappedToAnyTable; + + /// + /// 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. + /// + [EntityFrameworkInternal] + public EventDefinitionBase LogNamedIndexPropertiesBothMappedAndNotMappedToTable; + + /// + /// 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. + /// + [EntityFrameworkInternal] + public EventDefinitionBase LogUnnamedIndexPropertiesBothMappedAndNotMappedToTable; + + /// + /// 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. + /// + [EntityFrameworkInternal] + public EventDefinitionBase LogNamedIndexPropertiesMappedToNonOverlappingTables; + + /// + /// 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. + /// + [EntityFrameworkInternal] + public EventDefinitionBase LogUnnamedIndexPropertiesMappedToNonOverlappingTables; } } diff --git a/src/EFCore.Relational/Extensions/Internal/TupleExtensions.cs b/src/EFCore.Relational/Extensions/Internal/TupleExtensions.cs new file mode 100644 index 00000000000..1f320c94255 --- /dev/null +++ b/src/EFCore.Relational/Extensions/Internal/TupleExtensions.cs @@ -0,0 +1,41 @@ +// 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.Collections.Generic; +using System.Linq; +using JetBrains.Annotations; + +// ReSharper disable once CheckNamespace +namespace Microsoft.EntityFrameworkCore.Internal +{ + /// + /// 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 static class TupleExtensions + { + /// + /// 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 static string FormatTables([NotNull] this IEnumerable<(string Table, string Schema)> tables) + => "{" + + string.Join(", ", tables.Select(FormatTable)) + + "}"; + + /// + /// 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 static string FormatTable(this (string Table, string Schema) table) + => "'" + + (table.Schema == null ? table.Table : table.Schema + "." + table.Table) + + "'"; + } +} diff --git a/src/EFCore.Relational/Extensions/RelationalIndexBuilderExtensions.cs b/src/EFCore.Relational/Extensions/RelationalIndexBuilderExtensions.cs index 84ac9207a3f..c9efc627405 100644 --- a/src/EFCore.Relational/Extensions/RelationalIndexBuilderExtensions.cs +++ b/src/EFCore.Relational/Extensions/RelationalIndexBuilderExtensions.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 JetBrains.Annotations; using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Metadata.Builders; @@ -20,15 +21,9 @@ public static class RelationalIndexBuilderExtensions /// The builder for the index being configured. /// The name of the index. /// A builder to further configure the index. + [Obsolete("Use IndexBuilder.HasName() instead.")] public static IndexBuilder HasName([NotNull] this IndexBuilder indexBuilder, [CanBeNull] string name) - { - Check.NotNull(indexBuilder, nameof(indexBuilder)); - Check.NullButNotEmpty(name, nameof(name)); - - indexBuilder.Metadata.SetName(name); - - return indexBuilder; - } + => indexBuilder.HasName(name); /// /// Configures the name of the index in the database when targeting a relational database. @@ -37,8 +32,9 @@ public static IndexBuilder HasName([NotNull] this IndexBuilder indexBuilder, [Ca /// The builder for the index being configured. /// The name of the index. /// A builder to further configure the index. + [Obsolete("Use IndexBuilder.HasName() instead.")] public static IndexBuilder HasName([NotNull] this IndexBuilder indexBuilder, [CanBeNull] string name) - => (IndexBuilder)HasName((IndexBuilder)indexBuilder, name); + => indexBuilder.HasName(name); /// /// Configures the name of the index in the database when targeting a relational database. @@ -50,17 +46,10 @@ public static IndexBuilder HasName([NotNull] this IndexBuilder /// The same builder instance if the configuration was applied, /// otherwise. /// + [Obsolete("Use IConventionIndexBuilder.HasName() instead.")] public static IConventionIndexBuilder HasName( [NotNull] this IConventionIndexBuilder indexBuilder, [CanBeNull] string name, bool fromDataAnnotation = false) - { - if (indexBuilder.CanSetName(name, fromDataAnnotation)) - { - indexBuilder.Metadata.SetName(name, fromDataAnnotation); - return indexBuilder; - } - - return null; - } + => indexBuilder.HasName(name, fromDataAnnotation); /// /// Returns a value indicating whether the given name can be set for the index. @@ -69,9 +58,10 @@ public static IConventionIndexBuilder HasName( /// The name of the index. /// Indicates whether the configuration was specified using a data annotation. /// if the given name can be set for the index. + [Obsolete("Use IConventionIndexBuilder.CanSetName() instead.")] public static bool CanSetName( [NotNull] this IConventionIndexBuilder indexBuilder, [CanBeNull] string name, bool fromDataAnnotation = false) - => indexBuilder.CanSetAnnotation(RelationalAnnotationNames.Name, name, fromDataAnnotation); + => indexBuilder.CanSetName(name, fromDataAnnotation); /// /// Configures the filter expression for the index. diff --git a/src/EFCore.Relational/Extensions/RelationalIndexExtensions.cs b/src/EFCore.Relational/Extensions/RelationalIndexExtensions.cs index 1717a837cde..5dfa848382e 100644 --- a/src/EFCore.Relational/Extensions/RelationalIndexExtensions.cs +++ b/src/EFCore.Relational/Extensions/RelationalIndexExtensions.cs @@ -1,13 +1,13 @@ // 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 System.Linq; using System.Text; using JetBrains.Annotations; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata; -using Microsoft.EntityFrameworkCore.Metadata.Internal; using Microsoft.EntityFrameworkCore.Utilities; // ReSharper disable once CheckNamespace @@ -23,9 +23,17 @@ public static class RelationalIndexExtensions /// /// The index. /// The name for this index. + public static string GetDatabaseName([NotNull] this IIndex index) + => index.Name ?? index.GetDefaultName(); + + /// + /// Returns the name for this index. + /// + /// The index. + /// The name for this index. + [Obsolete("Use GetDatabaseName() instead")] public static string GetName([NotNull] this IIndex index) - => (string)index[RelationalAnnotationNames.Name] - ?? index.GetDefaultName(); + => index.Name ?? index.GetDefaultName(); /// /// Returns the name for this index. @@ -38,8 +46,7 @@ public static string GetName( [NotNull] this IIndex index, [NotNull] string tableName, [CanBeNull] string schema) - => (string)index[RelationalAnnotationNames.Name] - ?? index.GetDefaultName(tableName, schema); + => index.Name ?? index.GetDefaultName(tableName, schema); /// /// Returns the default name that would be used for this index. @@ -111,10 +118,9 @@ public static string GetDefaultName( /// /// The index. /// The value to set. + [Obsolete("Use IMutableIndex.Name instead.")] public static void SetName([NotNull] this IMutableIndex index, [CanBeNull] string name) - => index.SetOrRemoveAnnotation( - RelationalAnnotationNames.Name, - Check.NullButNotEmpty(name, nameof(name))); + => index.Name = Check.NullButNotEmpty(name, nameof(name)); /// /// Sets the index name. @@ -123,23 +129,18 @@ public static void SetName([NotNull] this IMutableIndex index, [CanBeNull] strin /// The value to set. /// Indicates whether the configuration was specified using a data annotation. /// The configured value. + [Obsolete("Use IConventionIndex.SetName() instead.")] public static string SetName([NotNull] this IConventionIndex index, [CanBeNull] string name, bool fromDataAnnotation = false) - { - index.SetOrRemoveAnnotation( - RelationalAnnotationNames.Name, - Check.NullButNotEmpty(name, nameof(name)), - fromDataAnnotation); - - return name; - } + => index.SetName(Check.NullButNotEmpty(name, nameof(name)), fromDataAnnotation); /// /// Gets the for the index name. /// /// The index. /// The for the index name. + [Obsolete("Use IConventionIndex.GetNameConfigurationSource() instead.")] public static ConfigurationSource? GetNameConfigurationSource([NotNull] this IConventionIndex index) - => index.FindAnnotation(RelationalAnnotationNames.Name)?.GetConfigurationSource(); + => index.GetNameConfigurationSource(); /// /// Returns the index filter expression. diff --git a/src/EFCore.Relational/Infrastructure/RelationalModelValidator.cs b/src/EFCore.Relational/Infrastructure/RelationalModelValidator.cs index e7722ab0e77..e25de156ffa 100644 --- a/src/EFCore.Relational/Infrastructure/RelationalModelValidator.cs +++ b/src/EFCore.Relational/Infrastructure/RelationalModelValidator.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using JetBrains.Annotations; using Microsoft.EntityFrameworkCore; @@ -61,6 +62,7 @@ public override void Validate(IModel model, IDiagnosticsLogger @@ -911,5 +913,114 @@ protected virtual void ValidatePropertyOverrides( private static string Format(string tableName, string schema) => schema == null ? tableName : schema + "." + tableName; + + /// + /// Validates that the properties of any one index are + /// all mapped to columns on at least one common table. + /// + /// The model to validate. + /// The logger to use. + protected virtual void ValidateIndexProperties( + [NotNull] IModel model, [NotNull] IDiagnosticsLogger logger) + { + Check.NotNull(model, nameof(model)); + + foreach (var entityType in model.GetEntityTypes()) + { + foreach (var index in entityType.GetDeclaredIndexes() + .Where(i => ConfigurationSource.Convention != ((IConventionIndex)i).GetConfigurationSource())) + { + IProperty propertyNotMappedToAnyTable = null; + Tuple> firstPropertyTables = null; + Tuple> lastPropertyTables = null; + HashSet<(string Table, string Schema)> overlappingTables = null; + foreach (var property in index.Properties) + { + var tablesMappedToProperty = property.DeclaringEntityType.GetDerivedTypesInclusive() + .Select(t => (t.GetTableName(), t.GetSchema())).Distinct() + .Where(n => n.Item1 != null && property.GetColumnName(n.Item1, n.Item2) != null) + .ToList<(string Table, string Schema)>(); + if (tablesMappedToProperty.Count == 0) + { + propertyNotMappedToAnyTable = property; + overlappingTables = null; + + if (firstPropertyTables != null) + { + // Property is not mapped but we already found + // a property that is mapped. + break; + } + + continue; + } + + if (firstPropertyTables == null) + { + // store off which tables the first member maps to + firstPropertyTables = + new Tuple>(property.Name, tablesMappedToProperty); + } + else + { + // store off which tables the last member we encountered maps to + lastPropertyTables = + new Tuple>(property.Name, tablesMappedToProperty); + } + + if (propertyNotMappedToAnyTable != null) + { + // Property is mapped but we already found + // a property that is not mapped. + overlappingTables = null; + break; + } + + if (overlappingTables == null) + { + overlappingTables = new HashSet<(string Table, string Schema)>(tablesMappedToProperty); + } + else + { + overlappingTables.IntersectWith(tablesMappedToProperty); + if (overlappingTables.Count == 0) + { + break; + } + } + } + + if (overlappingTables == null) + { + if (firstPropertyTables == null) + { + logger.AllIndexPropertiesNotToMappedToAnyTable( + entityType, + index); + } + else + { + logger.IndexPropertiesBothMappedAndNotMappedToTable( + entityType, + index, + propertyNotMappedToAnyTable.Name); + } + } + else if (overlappingTables.Count == 0) + { + Debug.Assert(firstPropertyTables != null, nameof(firstPropertyTables)); + Debug.Assert(lastPropertyTables != null, nameof(lastPropertyTables)); + + logger.IndexPropertiesMappedToNonOverlappingTables( + entityType, + index, + firstPropertyTables.Item1, + firstPropertyTables.Item2, + lastPropertyTables.Item1, + lastPropertyTables.Item2); + } + } + } + } } } diff --git a/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs b/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs index 73663d16ec4..55ecbf8ed08 100644 --- a/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs +++ b/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs @@ -1802,5 +1802,146 @@ public static EventDefinition LogMigrationAttributeMissingWarning([NotNu return (EventDefinition)definition; } + + /// + /// The index named '{indexName}' on the entity type '{entityType}' specifies properties {indexPropertiesList}. None of these properties are mapped to a column in any table. This index will not be created in the database. + /// + public static EventDefinition LogNamedIndexAllPropertiesNotToMappedToAnyTable([NotNull] IDiagnosticsLogger logger) + { + var definition = ((RelationalLoggingDefinitions)logger.Definitions).LogNamedIndexAllPropertiesNotToMappedToAnyTable; + if (definition == null) + { + definition = LazyInitializer.EnsureInitialized( + ref ((RelationalLoggingDefinitions)logger.Definitions).LogNamedIndexAllPropertiesNotToMappedToAnyTable, + () => new EventDefinition( + logger.Options, + RelationalEventId.AllIndexPropertiesNotToMappedToAnyTable, + LogLevel.Information, + "RelationalEventId.AllIndexPropertiesNotToMappedToAnyTable", + level => LoggerMessage.Define( + level, + RelationalEventId.AllIndexPropertiesNotToMappedToAnyTable, + _resourceManager.GetString("LogNamedIndexAllPropertiesNotToMappedToAnyTable")))); + } + + return (EventDefinition)definition; + } + + /// + /// The unnamed index on the entity type '{entityType}' specifies properties {indexPropertiesList}. None of these properties are mapped to a column in any table. This index will not be created in the database. + /// + public static EventDefinition LogUnnamedIndexAllPropertiesNotToMappedToAnyTable([NotNull] IDiagnosticsLogger logger) + { + var definition = ((RelationalLoggingDefinitions)logger.Definitions).LogUnnamedIndexAllPropertiesNotToMappedToAnyTable; + if (definition == null) + { + definition = LazyInitializer.EnsureInitialized( + ref ((RelationalLoggingDefinitions)logger.Definitions).LogUnnamedIndexAllPropertiesNotToMappedToAnyTable, + () => new EventDefinition( + logger.Options, + RelationalEventId.AllIndexPropertiesNotToMappedToAnyTable, + LogLevel.Information, + "RelationalEventId.AllIndexPropertiesNotToMappedToAnyTable", + level => LoggerMessage.Define( + level, + RelationalEventId.AllIndexPropertiesNotToMappedToAnyTable, + _resourceManager.GetString("LogUnnamedIndexAllPropertiesNotToMappedToAnyTable")))); + } + + return (EventDefinition)definition; + } + + /// + /// The index named '{indexName}' on the entity type '{entityType}' specifies properties {indexPropertiesList}. Some properties are mapped to a column in a table, but the property '{propertyName}' is not. All of the properties should be mapped for the index to be created in the database. + /// + public static EventDefinition LogNamedIndexPropertiesBothMappedAndNotMappedToTable([NotNull] IDiagnosticsLogger logger) + { + var definition = ((RelationalLoggingDefinitions)logger.Definitions).LogNamedIndexPropertiesBothMappedAndNotMappedToTable; + if (definition == null) + { + definition = LazyInitializer.EnsureInitialized( + ref ((RelationalLoggingDefinitions)logger.Definitions).LogNamedIndexPropertiesBothMappedAndNotMappedToTable, + () => new EventDefinition( + logger.Options, + RelationalEventId.IndexPropertiesBothMappedAndNotMappedToTable, + LogLevel.Error, + "RelationalEventId.IndexPropertiesBothMappedAndNotMappedToTable", + level => LoggerMessage.Define( + level, + RelationalEventId.IndexPropertiesBothMappedAndNotMappedToTable, + _resourceManager.GetString("LogNamedIndexPropertiesBothMappedAndNotMappedToTable")))); + } + + return (EventDefinition)definition; + } + + /// + /// The unnamed index on the entity type '{entityType}' specifies properties {indexPropertiesList}. Some properties are mapped to a column in a table, but the property '{propertyName}' is not. All of the properties should be mapped for the index to be created in the database. + /// + public static EventDefinition LogUnnamedIndexPropertiesBothMappedAndNotMappedToTable([NotNull] IDiagnosticsLogger logger) + { + var definition = ((RelationalLoggingDefinitions)logger.Definitions).LogUnnamedIndexPropertiesBothMappedAndNotMappedToTable; + if (definition == null) + { + definition = LazyInitializer.EnsureInitialized( + ref ((RelationalLoggingDefinitions)logger.Definitions).LogUnnamedIndexPropertiesBothMappedAndNotMappedToTable, + () => new EventDefinition( + logger.Options, + RelationalEventId.IndexPropertiesBothMappedAndNotMappedToTable, + LogLevel.Error, + "RelationalEventId.IndexPropertiesBothMappedAndNotMappedToTable", + level => LoggerMessage.Define( + level, + RelationalEventId.IndexPropertiesBothMappedAndNotMappedToTable, + _resourceManager.GetString("LogUnnamedIndexPropertiesBothMappedAndNotMappedToTable")))); + } + + return (EventDefinition)definition; + } + + /// + /// The index named '{indexName}' on the entity type '{entityType}' specifies properties {indexPropertiesList}. The property '{propertyName1}' is mapped to table(s) {tableList1}, whereas the property '{propertyName2}' is mapped to table(s) {tableList2}. All index properties must map to at least one common table. + /// + public static FallbackEventDefinition LogNamedIndexPropertiesMappedToNonOverlappingTables([NotNull] IDiagnosticsLogger logger) + { + var definition = ((RelationalLoggingDefinitions)logger.Definitions).LogNamedIndexPropertiesMappedToNonOverlappingTables; + if (definition == null) + { + definition = LazyInitializer.EnsureInitialized( + ref ((RelationalLoggingDefinitions)logger.Definitions).LogNamedIndexPropertiesMappedToNonOverlappingTables, + () => new FallbackEventDefinition( + logger.Options, + RelationalEventId.IndexPropertiesMappedToNonOverlappingTables, + LogLevel.Error, + "RelationalEventId.IndexPropertiesMappedToNonOverlappingTables", + _resourceManager.GetString("LogNamedIndexPropertiesMappedToNonOverlappingTables"))); + } + + return (FallbackEventDefinition)definition; + } + + /// + /// The unnamed index on the entity type '{entityType}' specifies properties {indexPropertiesList}. The property '{propertyName1}' is mapped to table(s) {tableList1}, whereas the property '{propertyName2}' is mapped to table(s) {tableList2}. All index properties must map to at least one common table. + /// + public static EventDefinition LogUnnamedIndexPropertiesMappedToNonOverlappingTables([NotNull] IDiagnosticsLogger logger) + { + var definition = ((RelationalLoggingDefinitions)logger.Definitions).LogUnnamedIndexPropertiesMappedToNonOverlappingTables; + if (definition == null) + { + definition = LazyInitializer.EnsureInitialized( + ref ((RelationalLoggingDefinitions)logger.Definitions).LogUnnamedIndexPropertiesMappedToNonOverlappingTables, + () => new EventDefinition( + logger.Options, + RelationalEventId.IndexPropertiesMappedToNonOverlappingTables, + LogLevel.Error, + "RelationalEventId.IndexPropertiesMappedToNonOverlappingTables", + level => LoggerMessage.Define( + level, + RelationalEventId.IndexPropertiesMappedToNonOverlappingTables, + _resourceManager.GetString("LogUnnamedIndexPropertiesMappedToNonOverlappingTables")))); + } + + return (EventDefinition)definition; + } } } diff --git a/src/EFCore.Relational/Properties/RelationalStrings.resx b/src/EFCore.Relational/Properties/RelationalStrings.resx index 794ce2f3bfa..b59a504cf75 100644 --- a/src/EFCore.Relational/Properties/RelationalStrings.resx +++ b/src/EFCore.Relational/Properties/RelationalStrings.resx @@ -606,4 +606,28 @@ The property '{property}' on entity type '{entityType}' is not mapped to the table '{table}'. + + The index named '{indexName}' on the entity type '{entityType}' specifies properties {indexPropertiesList}. None of these properties are mapped to a column in any table. This index will not be created in the database. + Information RelationalEventId.AllIndexPropertiesNotToMappedToAnyTable string string string + + + The unnamed index on the entity type '{entityType}' specifies properties {indexPropertiesList}. None of these properties are mapped to a column in any table. This index will not be created in the database. + Information RelationalEventId.AllIndexPropertiesNotToMappedToAnyTable string string + + + The index named '{indexName}' on the entity type '{entityType}' specifies properties {indexPropertiesList}. Some properties are mapped to a column in a table, but the property '{propertyName}' is not. All of the properties should be mapped for the index to be created in the database. + Error RelationalEventId.IndexPropertiesBothMappedAndNotMappedToTable string string string string + + + The unnamed index on the entity type '{entityType}' specifies properties {indexPropertiesList}. Some properties are mapped to a column in a table, but the property '{propertyName}' is not. All of the properties should be mapped for the index to be created in the database. + Error RelationalEventId.IndexPropertiesBothMappedAndNotMappedToTable string string string + + + The index named '{indexName}' on the entity type '{entityType}' specifies properties {indexPropertiesList}. The property '{propertyName1}' is mapped to table(s) {tableList1}, whereas the property '{propertyName2}' is mapped to table(s) {tableList2}. All index properties must map to at least one common table. + Error RelationalEventId.IndexPropertiesMappedToNonOverlappingTables string string string string string string string + + + The unnamed index on the entity type '{entityType}' specifies properties {indexPropertiesList}. The property '{propertyName1}' is mapped to table(s) {tableList1}, whereas the property '{propertyName2}' is mapped to table(s) {tableList2}. All index properties must map to at least one common table. + Error RelationalEventId.IndexPropertiesMappedToNonOverlappingTables string string string string string string + \ No newline at end of file diff --git a/src/EFCore/Metadata/Builders/IConventionIndexBuilder.cs b/src/EFCore/Metadata/Builders/IConventionIndexBuilder.cs index c3ef86de595..9dce6b218d6 100644 --- a/src/EFCore/Metadata/Builders/IConventionIndexBuilder.cs +++ b/src/EFCore/Metadata/Builders/IConventionIndexBuilder.cs @@ -1,6 +1,8 @@ // 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 JetBrains.Annotations; + namespace Microsoft.EntityFrameworkCore.Metadata.Builders { /// @@ -32,11 +34,32 @@ public interface IConventionIndexBuilder : IConventionAnnotatableBuilder /// /// Returns a value indicating whether this index uniqueness can be configured - /// from the current configuration source + /// 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 name of this index. + /// + /// The name of the index which can be + /// to indicate that a unique name should be generated. + /// Indicates whether the configuration was specified using a data annotation. + /// + /// The same builder instance if the name is unchanged, + /// otherwise. + /// + IConventionIndexBuilder HasName([CanBeNull] string name, bool fromDataAnnotation = false); + + /// + /// Returns a value indicating whether the name can be configured + /// from the current configuration source. + /// + /// The name of the index. + /// Indicates whether the configuration was specified using a data annotation. + /// if the name can be configured. + bool CanSetName([CanBeNull] string name, bool fromDataAnnotation = false); } } diff --git a/src/EFCore/Metadata/Builders/IndexBuilder.cs b/src/EFCore/Metadata/Builders/IndexBuilder.cs index ab92e068f62..6b0eca95631 100644 --- a/src/EFCore/Metadata/Builders/IndexBuilder.cs +++ b/src/EFCore/Metadata/Builders/IndexBuilder.cs @@ -74,6 +74,21 @@ public virtual IndexBuilder IsUnique(bool unique = true) return this; } + /// + /// Configures the name of this index. + /// + /// + /// The name of this index which can be + /// to indicate that a unique name should be generated. + /// + /// The same builder instance so that multiple configuration calls can be chained. + public virtual IndexBuilder HasName([CanBeNull] string name) + { + Builder.HasName(name, 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 d964c742a0c..cc5b1fe290c 100644 --- a/src/EFCore/Metadata/Builders/IndexBuilder`.cs +++ b/src/EFCore/Metadata/Builders/IndexBuilder`.cs @@ -48,5 +48,16 @@ public IndexBuilder([NotNull] 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 name of this index. + /// + /// + /// The name of this index which can be + /// to indicate that a unique name should be generated. + /// + /// The same builder instance so that multiple configuration calls can be chained. + public new virtual IndexBuilder HasName([CanBeNull] string name) + => (IndexBuilder)base.HasName(name); } } diff --git a/src/EFCore/Metadata/Conventions/ConventionSet.cs b/src/EFCore/Metadata/Conventions/ConventionSet.cs index 5969000e213..4ef12321184 100644 --- a/src/EFCore/Metadata/Conventions/ConventionSet.cs +++ b/src/EFCore/Metadata/Conventions/ConventionSet.cs @@ -199,6 +199,12 @@ public class ConventionSet public virtual IList IndexUniquenessChangedConventions { get; } = new List(); + /// + /// Conventions to run when the name of an index is changed. + /// + public virtual IList IndexNameChangedConventions { get; } + = new List(); + /// /// Conventions to run when an annotation is changed on an index. /// diff --git a/src/EFCore/Metadata/Conventions/IIndexNameChangedConvention.cs b/src/EFCore/Metadata/Conventions/IIndexNameChangedConvention.cs new file mode 100644 index 00000000000..3a90e68ffd3 --- /dev/null +++ b/src/EFCore/Metadata/Conventions/IIndexNameChangedConvention.cs @@ -0,0 +1,23 @@ +// 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 JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Microsoft.EntityFrameworkCore.Metadata.Conventions +{ + /// + /// Represents an operation that should be performed when the name of an index is changed. + /// + public interface IIndexNameChangedConvention : IConvention + { + /// + /// Called after the name of an index is changed. + /// + /// The builder for the index. + /// Additional information associated with convention execution. + void ProcessIndexNameChanged( + [NotNull] IConventionIndexBuilder indexBuilder, + [NotNull] IConventionContext context); + } +} diff --git a/src/EFCore/Metadata/Conventions/IndexAttributeConvention.cs b/src/EFCore/Metadata/Conventions/IndexAttributeConvention.cs new file mode 100644 index 00000000000..8e25c1b804c --- /dev/null +++ b/src/EFCore/Metadata/Conventions/IndexAttributeConvention.cs @@ -0,0 +1,91 @@ +// 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 System.Linq; +using System.Reflection; +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Diagnostics; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Microsoft.EntityFrameworkCore.Metadata.Conventions.Infrastructure; +using Microsoft.EntityFrameworkCore.Utilities; + +namespace Microsoft.EntityFrameworkCore.Metadata.Conventions +{ + /// + /// A convention that configures database indexes based on the . + /// + public class IndexAttributeConvention : IModelFinalizingConvention + { + /// + /// Creates a new instance of . + /// + /// Parameter object containing dependencies for this convention. + public IndexAttributeConvention([NotNull] ProviderConventionSetBuilderDependencies dependencies) + { + Dependencies = dependencies; + } + + /// + /// Parameter object containing service dependencies. + /// + protected virtual ProviderConventionSetBuilderDependencies Dependencies { get; } + + /// + public virtual void ProcessModelFinalizing(IConventionModelBuilder modelBuilder, IConventionContext context) + { + foreach (var entityType in modelBuilder.Metadata.GetEntityTypes()) + { + if (entityType.ClrType != null) + { + var ignoredMembers = entityType.GetIgnoredMembers(); + foreach (var indexAttribute in + entityType.ClrType.GetCustomAttributes(true)) + { + var indexProperties = new List(); + foreach (var propertyName in indexAttribute.PropertyNames) + { + if (ignoredMembers.Contains(propertyName)) + { + throw new InvalidOperationException( + CoreStrings.IndexDefinedOnIgnoredProperty( + indexAttribute.Name, + entityType.DisplayName(), + indexAttribute.PropertyNames.Format(), + propertyName)); + } + + var property = entityType.FindProperty(propertyName); + if (property == null) + { + throw new InvalidOperationException( + CoreStrings.IndexDefinedOnNonExistentProperty( + indexAttribute.Name, + entityType.DisplayName(), + indexAttribute.PropertyNames.Format(), + propertyName)); + } + + indexProperties.Add(property); + } + + var indexBuilder = entityType.Builder.HasIndex(indexProperties, fromDataAnnotation: true); + if (indexBuilder != null) + { + if (indexAttribute.Name != null) + { + indexBuilder.HasName(indexAttribute.Name, 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 b069f775d6e..f1353edf8fd 100644 --- a/src/EFCore/Metadata/Conventions/Infrastructure/ProviderConventionSetBuilder.cs +++ b/src/EFCore/Metadata/Conventions/Infrastructure/ProviderConventionSetBuilder.cs @@ -181,6 +181,7 @@ 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/src/EFCore/Metadata/Conventions/Internal/ConventionDispatcher.ConventionScope.cs b/src/EFCore/Metadata/Conventions/Internal/ConventionDispatcher.ConventionScope.cs index f3f53c9bfff..e8d36daab53 100644 --- a/src/EFCore/Metadata/Conventions/Internal/ConventionDispatcher.ConventionScope.cs +++ b/src/EFCore/Metadata/Conventions/Internal/ConventionDispatcher.ConventionScope.cs @@ -116,6 +116,7 @@ public abstract IConventionIndex OnIndexRemoved( [NotNull] IConventionEntityTypeBuilder entityTypeBuilder, [NotNull] IConventionIndex index); public abstract bool? OnIndexUniquenessChanged([NotNull] IConventionIndexBuilder indexBuilder); + public abstract string OnIndexNameChanged([NotNull] IConventionIndexBuilder indexBuilder); public abstract IConventionKeyBuilder OnKeyAdded([NotNull] 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 2ccc197bae9..6e0523d0b50 100644 --- a/src/EFCore/Metadata/Conventions/Internal/ConventionDispatcher.DelayedConventionScope.cs +++ b/src/EFCore/Metadata/Conventions/Internal/ConventionDispatcher.DelayedConventionScope.cs @@ -182,6 +182,12 @@ public override IConventionIndex OnIndexRemoved( return indexBuilder.Metadata.IsUnique; } + public override string OnIndexNameChanged(IConventionIndexBuilder indexBuilder) + { + Add(new OnIndexNameChangedNode(indexBuilder)); + return indexBuilder.Metadata.Name; + } + public override IConventionAnnotation OnIndexAnnotationChanged( IConventionIndexBuilder indexBuilder, string name, @@ -861,6 +867,19 @@ public override void Run(ConventionDispatcher dispatcher) => dispatcher._immediateConventionScope.OnIndexUniquenessChanged(IndexBuilder); } + private sealed class OnIndexNameChangedNode : ConventionNode + { + public OnIndexNameChangedNode(IConventionIndexBuilder indexBuilder) + { + IndexBuilder = indexBuilder; + } + + public IConventionIndexBuilder IndexBuilder { get; } + + public override void Run(ConventionDispatcher dispatcher) + => dispatcher._immediateConventionScope.OnIndexNameChanged(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 be8a37b18c8..bfd892d3ef4 100644 --- a/src/EFCore/Metadata/Conventions/Internal/ConventionDispatcher.ImmediateConventionScope.cs +++ b/src/EFCore/Metadata/Conventions/Internal/ConventionDispatcher.ImmediateConventionScope.cs @@ -1042,6 +1042,34 @@ public override IConventionIndex OnIndexRemoved(IConventionEntityTypeBuilder ent return _boolConventionContext.Result; } + public override string OnIndexNameChanged(IConventionIndexBuilder indexBuilder) + { + using (_dispatcher.DelayConventions()) + { + _stringConventionContext.ResetState(indexBuilder.Metadata.Name); + foreach (var indexConvention in _conventionSet.IndexNameChangedConventions) + { + if (indexBuilder.Metadata.Builder == null) + { + return null; + } + + indexConvention.ProcessIndexNameChanged(indexBuilder, _stringConventionContext); + if (_stringConventionContext.ShouldStopProcessing()) + { + return _stringConventionContext.Result; + } + } + } + + if (indexBuilder.Metadata.Builder == null) + { + return null; + } + + return _stringConventionContext.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 d0a198fb4d7..85f00a2a098 100644 --- a/src/EFCore/Metadata/Conventions/Internal/ConventionDispatcher.cs +++ b/src/EFCore/Metadata/Conventions/Internal/ConventionDispatcher.cs @@ -473,6 +473,15 @@ public virtual IConventionIndex OnIndexRemoved( public virtual bool? OnIndexUniquenessChanged([NotNull] 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 string OnIndexNameChanged([NotNull] IConventionIndexBuilder indexBuilder) + => _scope.OnIndexNameChanged(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 fae466eb227..3b50dedbb85 100644 --- a/src/EFCore/Metadata/IConventionIndex.cs +++ b/src/EFCore/Metadata/IConventionIndex.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System.Collections.Generic; +using JetBrains.Annotations; using Microsoft.EntityFrameworkCore.Metadata.Builders; namespace Microsoft.EntityFrameworkCore.Metadata @@ -53,5 +54,20 @@ public interface IConventionIndex : IIndex, IConventionAnnotatable /// /// The configuration source for . ConfigurationSource? GetIsUniqueConfigurationSource(); + + /// + /// Sets the name of the index which can be + /// to indicate that a unique name should be generated. + /// + /// The name of the index. + /// Indicates whether the configuration was specified using a data annotation. + /// The configured name. + string SetName([CanBeNull] string name, bool fromDataAnnotation = false); + + /// + /// Returns the configuration source for . + /// + /// The configuration source for . + ConfigurationSource? GetNameConfigurationSource(); } } diff --git a/src/EFCore/Metadata/IIndex.cs b/src/EFCore/Metadata/IIndex.cs index fd411aa59de..cf2bce5f05f 100644 --- a/src/EFCore/Metadata/IIndex.cs +++ b/src/EFCore/Metadata/IIndex.cs @@ -16,6 +16,11 @@ public interface IIndex : IAnnotatable /// IReadOnlyList Properties { get; } + /// + /// Gets the name of this index. + /// + string Name { get; } + /// /// Gets a value indicating whether the values assigned to the indexed properties are unique. /// diff --git a/src/EFCore/Metadata/IMutableIndex.cs b/src/EFCore/Metadata/IMutableIndex.cs index 9bf805b39a0..cbd341409e9 100644 --- a/src/EFCore/Metadata/IMutableIndex.cs +++ b/src/EFCore/Metadata/IMutableIndex.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System.Collections.Generic; +using JetBrains.Annotations; namespace Microsoft.EntityFrameworkCore.Metadata { @@ -21,6 +22,12 @@ public interface IMutableIndex : IIndex, IMutableAnnotatable /// new bool IsUnique { get; set; } + /// + /// Gets or sets the name of the index which can be + /// to indicate that a unique name should be generated. + /// + new string Name { get; [param: CanBeNull] set; } + /// /// Gets the properties that this index is defined on. /// diff --git a/src/EFCore/Metadata/Internal/Index.cs b/src/EFCore/Metadata/Internal/Index.cs index cb15b1fea98..a6e0cb4f459 100644 --- a/src/EFCore/Metadata/Internal/Index.cs +++ b/src/EFCore/Metadata/Internal/Index.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 System.Diagnostics; using JetBrains.Annotations; @@ -22,9 +23,11 @@ namespace Microsoft.EntityFrameworkCore.Metadata.Internal public class Index : ConventionAnnotatable, IMutableIndex, IConventionIndex { private bool? _isUnique; + private string _name; private ConfigurationSource _configurationSource; private ConfigurationSource? _isUniqueConfigurationSource; + private ConfigurationSource? _nameConfigurationSource; // Warning: Never access these fields directly as access needs to be thread-safe private object _nullableValueFactory; @@ -138,6 +141,44 @@ public virtual bool IsUnique private static bool DefaultIsUnique => false; + /// + /// 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 string Name + { + get => _name; + set => SetName(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 string SetName([CanBeNull] string name, ConfigurationSource configurationSource) + { + var oldName = Name; + var isChanging = !string.Equals(oldName, name, StringComparison.Ordinal); + _name = name; + + if (name == null) + { + _nameConfigurationSource = null; + } + else + { + UpdateNameConfigurationSource(configurationSource); + } + + return isChanging + ? DeclaringEntityType.Model.ConventionDispatcher.OnIndexNameChanged(Builder) + : oldName; + } + /// /// 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 @@ -149,6 +190,17 @@ public virtual bool IsUnique 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 ConfigurationSource? GetNameConfigurationSource() => _nameConfigurationSource; + + private void UpdateNameConfigurationSource(ConfigurationSource configurationSource) + => _nameConfigurationSource = configurationSource.Max(_nameConfigurationSource); + /// /// Runs the conventions when an annotation was set or removed. /// @@ -286,5 +338,15 @@ IConventionEntityType IConventionIndex.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] + string IConventionIndex.SetName(string name, bool fromDataAnnotation) + => SetName(name, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); } } diff --git a/src/EFCore/Metadata/Internal/InternalIndexBuilder.cs b/src/EFCore/Metadata/Internal/InternalIndexBuilder.cs index 310741cf321..06226f844ba 100644 --- a/src/EFCore/Metadata/Internal/InternalIndexBuilder.cs +++ b/src/EFCore/Metadata/Internal/InternalIndexBuilder.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 JetBrains.Annotations; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata.Builders; @@ -53,6 +54,33 @@ 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 HasName([CanBeNull] string name, ConfigurationSource configurationSource) + { + if (!CanSetName(name, configurationSource)) + { + return null; + } + + Metadata.SetName(name, 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 CanSetName([CanBeNull] string name, ConfigurationSource? configurationSource) + => string.Equals(Metadata.Name, name, StringComparison.Ordinal) + || configurationSource.Overrides(Metadata.GetNameConfigurationSource()); + /// /// 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 @@ -108,5 +136,27 @@ 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.HasName(string name, bool fromDataAnnotation) + => HasName( + name, + 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.CanSetName(string name, bool fromDataAnnotation) + => CanSetName( + name, + fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); } } diff --git a/src/EFCore/Properties/CoreStrings.Designer.cs b/src/EFCore/Properties/CoreStrings.Designer.cs index a45d1ed215b..db5e055a713 100644 --- a/src/EFCore/Properties/CoreStrings.Designer.cs +++ b/src/EFCore/Properties/CoreStrings.Designer.cs @@ -2658,6 +2658,22 @@ public static string MissingInverseManyToManyNavigation([CanBeNull] object princ public static string QueryContextAlreadyInitializedStateManager => GetString("QueryContextAlreadyInitializedStateManager"); + /// + /// The index named '{indexName}' on the entity type '{entityType}' with properties {indexPropertyList} is invalid. The property '{propertyName}' has been marked NotMapped or Ignore(). An index cannot use such properties. + /// + public static string IndexDefinedOnIgnoredProperty([CanBeNull] object indexName, [CanBeNull] object entityType, [CanBeNull] object indexPropertyList, [CanBeNull] object propertyName) + => string.Format( + GetString("IndexDefinedOnIgnoredProperty", nameof(indexName), nameof(entityType), nameof(indexPropertyList), nameof(propertyName)), + indexName, entityType, indexPropertyList, propertyName); + + /// + /// An index named '{indexName}' on the entity type '{entityType}' specifies properties {indexPropertyList}. But no property with name '{propertyName}' exists on that entity type or any of its base types. + /// + public static string IndexDefinedOnNonExistentProperty([CanBeNull] object indexName, [CanBeNull] object entityType, [CanBeNull] object indexPropertyList, [CanBeNull] object propertyName) + => string.Format( + GetString("IndexDefinedOnNonExistentProperty", nameof(indexName), nameof(entityType), nameof(indexPropertyList), nameof(propertyName)), + indexName, entityType, indexPropertyList, propertyName); + private static string GetString(string name, params string[] formatterNames) { var value = _resourceManager.GetString(name); diff --git a/src/EFCore/Properties/CoreStrings.resx b/src/EFCore/Properties/CoreStrings.resx index 9610f9b190d..8e5f53b893e 100644 --- a/src/EFCore/Properties/CoreStrings.resx +++ b/src/EFCore/Properties/CoreStrings.resx @@ -1403,4 +1403,10 @@ InitializeStateManager method has been called multiple times on current query context. This method is intended to be called only once before query enumeration starts. + + The index named '{indexName}' on the entity type '{entityType}' with properties {indexPropertyList} is invalid. The property '{propertyName}' has been marked NotMapped or Ignore(). An index cannot use such properties. + + + An index named '{indexName}' on the entity type '{entityType}' specifies properties {indexPropertyList}. But no property with name '{propertyName}' exists on that entity type or any of its base types. + \ No newline at end of file diff --git a/src/Shared/Check.cs b/src/Shared/Check.cs index c974f3e3915..962ebca2d74 100644 --- a/src/Shared/Check.cs +++ b/src/Shared/Check.cs @@ -94,6 +94,21 @@ public static IReadOnlyList HasNoNulls(IReadOnlyList value, [InvokerPar return value; } + + public static IReadOnlyList HasNoEmptyElements(IReadOnlyList value, [InvokerParameterName][NotNull] string parameterName) + { + NotNull(value, parameterName); + + if (value.Any(s => string.IsNullOrWhiteSpace(s))) + { + NotEmpty(parameterName, nameof(parameterName)); + + throw new ArgumentException(AbstractionsStrings.CollectionArgumentHasEmptyElements(parameterName)); + } + + return value; + } + [Conditional("DEBUG")] public static void DebugAssert([System.Diagnostics.CodeAnalysis.DoesNotReturnIf(false)] bool condition, string message) { diff --git a/src/Shared/EnumerableExtensions.cs b/src/Shared/EnumerableExtensions.cs index 54beb347d9d..9f08ef12189 100644 --- a/src/Shared/EnumerableExtensions.cs +++ b/src/Shared/EnumerableExtensions.cs @@ -137,5 +137,12 @@ public static async Task> ToListAsync( public static List ToList(this IEnumerable source) => source.OfType().ToList(); + + public static string Format([NotNull] this IEnumerable strings) + => "{" + + string.Join( + ", ", + strings.Select(s => "'" + s + "'")) + + "}"; } } diff --git a/test/EFCore.Design.Tests/Migrations/ModelSnapshotSqlServerTest.cs b/test/EFCore.Design.Tests/Migrations/ModelSnapshotSqlServerTest.cs index 193a67755b4..c2a815b404f 100644 --- a/test/EFCore.Design.Tests/Migrations/ModelSnapshotSqlServerTest.cs +++ b/test/EFCore.Design.Tests/Migrations/ModelSnapshotSqlServerTest.cs @@ -2414,7 +2414,7 @@ public virtual void Index_name_annotation_is_stored_in_snapshot_as_fluent_api() b.ToTable(""EntityWithTwoProperties""); });"), - o => Assert.Equal("IndexName", o.GetEntityTypes().First().GetIndexes().First()["Relational:Name"])); + o => Assert.Equal("IndexName", o.GetEntityTypes().First().GetIndexes().First().Name)); } [ConditionalFact] @@ -2486,9 +2486,9 @@ public virtual void Index_multiple_annotations_are_stored_in_snapshot() o => { var index = o.GetEntityTypes().First().GetIndexes().First(); - Assert.Equal(3, index.GetAnnotations().Count()); + Assert.Equal(2, index.GetAnnotations().Count()); Assert.Equal("AnnotationValue", index["AnnotationName"]); - Assert.Equal("IndexName", index["Relational:Name"]); + Assert.Equal("IndexName", index.Name); }); } @@ -2526,7 +2526,7 @@ public virtual void Index_with_default_constraint_name_exceeding_max() b.ToTable(""EntityWithStringProperty""); });"), - model => Assert.Equal(128, model.GetEntityTypes().First().GetIndexes().First().GetName().Length)); + model => Assert.Equal(128, model.GetEntityTypes().First().GetIndexes().First().GetDatabaseName().Length)); } #endregion @@ -2876,7 +2876,7 @@ public virtual void ForeignKey_name_preserved_when_generic() var originalIndex = originalChild.FindIndex(originalChild.FindProperty("Property")); var index = child.FindIndex(child.FindProperty("Property")); - Assert.Equal(originalIndex.GetName(), index.GetName()); + Assert.Equal(originalIndex.GetDatabaseName(), index.GetDatabaseName()); }); } diff --git a/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpDbContextGeneratorTest.cs b/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpDbContextGeneratorTest.cs index a842c2b0981..2c843ba4c6e 100644 --- a/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpDbContextGeneratorTest.cs +++ b/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpDbContextGeneratorTest.cs @@ -2,6 +2,7 @@ // 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.Design; using Microsoft.EntityFrameworkCore.Internal; using Microsoft.EntityFrameworkCore.Metadata; @@ -347,6 +348,175 @@ public void Collation_works() }); } + + [ConditionalFact] + public void Entity_with_indexes_and_use_data_annotations_false_always_generates_fluent_API() + { + Test( + modelBuilder => modelBuilder + .Entity( + "EntityWithIndexes", + x => + { + x.Property("Id"); + x.Property("A"); + x.Property("B"); + x.Property("C"); + x.HasKey("Id"); + x.HasIndex("A", "B") + .HasName("IndexOnAAndB") + .IsUnique(); + x.HasIndex("B", "C") + .HasName("IndexOnBAndC") + .HasFilter("Filter SQL") + .HasAnnotation("AnnotationName", "AnnotationValue"); + }), + new ModelCodeGenerationOptions { UseDataAnnotations = false }, + code => + { + Assert.Equal( + @"using System; +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(x => new { x.A, x.B }) + .HasName(""IndexOnAAndB"") + .IsUnique(); + + entity.HasIndex(x => new { x.B, x.C }) + .HasName(""IndexOnBAndC"") + .HasFilter(""Filter SQL"") + .HasAnnotation(""AnnotationName"", ""AnnotationValue""); + + entity.Property(e => e.Id).HasAnnotation(""SqlServer:ValueGenerationStrategy"", SqlServerValueGenerationStrategy.IdentityColumn); + }); + + OnModelCreatingPartial(modelBuilder); + } + + partial void OnModelCreatingPartial(ModelBuilder modelBuilder); + } +} +", + code.ContextFile.Code, + ignoreLineEndingDifferences: true); + }, + model => + Assert.Equal(2, model.FindEntityType("TestNamespace.EntityWithIndexes").GetIndexes().Count())); + } + + [ConditionalFact] + public void Entity_with_indexes_and_use_data_annotations_true_generates_fluent_API_only_for_indexes_with_annotations() + { + Test( + modelBuilder => modelBuilder + .Entity( + "EntityWithIndexes", + x => + { + x.Property("Id"); + x.Property("A"); + x.Property("B"); + x.Property("C"); + x.HasKey("Id"); + x.HasIndex("A", "B") + .HasName("IndexOnAAndB") + .IsUnique(); + x.HasIndex("B", "C") + .HasName("IndexOnBAndC") + .HasFilter("Filter SQL") + .HasAnnotation("AnnotationName", "AnnotationValue"); + }), + new ModelCodeGenerationOptions { UseDataAnnotations = true }, + code => + { + Assert.Equal( + @"using System; +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(x => new { x.B, x.C }) + .HasName(""IndexOnBAndC"") + .HasFilter(""Filter SQL"") + .HasAnnotation(""AnnotationName"", ""AnnotationValue""); + + entity.Property(e => e.Id).HasAnnotation(""SqlServer:ValueGenerationStrategy"", SqlServerValueGenerationStrategy.IdentityColumn); + }); + + OnModelCreatingPartial(modelBuilder); + } + + partial void OnModelCreatingPartial(ModelBuilder modelBuilder); + } +} +", + code.ContextFile.Code, + ignoreLineEndingDifferences: true); + }, + model => + Assert.Equal(2, model.FindEntityType("TestNamespace.EntityWithIndexes").GetIndexes().Count())); + } + private class TestCodeGeneratorPlugin : ProviderCodeGeneratorPlugin { public override MethodCallCodeFragment GenerateProviderOptions() diff --git a/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpEntityTypeGeneratorTest.cs b/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpEntityTypeGeneratorTest.cs index 4ca21b197b3..6d3d9432aad 100644 --- a/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpEntityTypeGeneratorTest.cs +++ b/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpEntityTypeGeneratorTest.cs @@ -409,5 +409,113 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) Assert.Null(entityType.FindPrimaryKey()); }); } + + [ConditionalFact] + public void Entity_with_multiple_indexes_generates_multiple_IndexAttributes() + { + Test( + modelBuilder => modelBuilder + .Entity( + "EntityWithIndexes", + x => + { + x.Property("Id"); + x.Property("A"); + x.Property("B"); + x.Property("C"); + x.HasKey("Id"); + x.HasIndex("A", "B") + .HasName("IndexOnAAndB") + .IsUnique(); + x.HasIndex("B", "C") + .HasName("IndexOnBAndC"); + }), + new ModelCodeGenerationOptions { UseDataAnnotations = true }, + code => + { + var entityFile = code.AdditionalFiles.First(f => f.Path == "EntityWithIndexes.cs"); + Assert.Equal( + @"using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using Microsoft.EntityFrameworkCore; + +namespace TestNamespace +{ + [Index(nameof(A), nameof(B), Name = ""IndexOnAAndB"", IsUnique = true)] + [Index(nameof(B), nameof(C), Name = ""IndexOnBAndC"")] + public partial class EntityWithIndexes + { + [Key] + public int Id { get; set; } + public int A { get; set; } + public int B { get; set; } + public int C { get; set; } + } +} +", + entityFile.Code, ignoreLineEndingDifferences: true); + }, + model => + { + var entityType = model.FindEntityType("TestNamespace.EntityWithIndexes"); + var indexes = entityType.GetIndexes(); + Assert.Equal(2, indexes.Count()); + Assert.Equal("IndexOnAAndB", indexes.First().Name); + Assert.Equal("IndexOnBAndC", indexes.Skip(1).First().Name); + }); + } + + [ConditionalFact] + public void Entity_with_indexes_generates_IndexAttribute_only_for_indexes_without_annotations() + { + Test( + modelBuilder => modelBuilder + .Entity( + "EntityWithIndexes", + x => + { + x.Property("Id"); + x.Property("A"); + x.Property("B"); + x.Property("C"); + x.HasKey("Id"); + x.HasIndex("A", "B") + .HasName("IndexOnAAndB") + .IsUnique(); + x.HasIndex("B", "C") + .HasName("IndexOnBAndC") + .HasFilter("Filter SQL"); + }), + new ModelCodeGenerationOptions { UseDataAnnotations = true }, + code => + { + var entityFile = code.AdditionalFiles.First(f => f.Path == "EntityWithIndexes.cs"); + Assert.Equal( + @"using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using Microsoft.EntityFrameworkCore; + +namespace TestNamespace +{ + [Index(nameof(A), nameof(B), Name = ""IndexOnAAndB"", IsUnique = true)] + public partial class EntityWithIndexes + { + [Key] + public int Id { get; set; } + public int A { get; set; } + public int B { get; set; } + public int C { get; set; } + } +} +", + entityFile.Code, ignoreLineEndingDifferences: true); + }, + model => + Assert.Equal(2, model.FindEntityType("TestNamespace.EntityWithIndexes").GetIndexes().Count())); + } } } diff --git a/test/EFCore.Design.Tests/Scaffolding/Internal/RelationalScaffoldingModelFactoryTest.cs b/test/EFCore.Design.Tests/Scaffolding/Internal/RelationalScaffoldingModelFactoryTest.cs index 710be4a42a3..88699a5055a 100644 --- a/test/EFCore.Design.Tests/Scaffolding/Internal/RelationalScaffoldingModelFactoryTest.cs +++ b/test/EFCore.Design.Tests/Scaffolding/Internal/RelationalScaffoldingModelFactoryTest.cs @@ -464,7 +464,7 @@ public void Unique_constraint() var index = entityType.GetIndexes().Single(); Assert.True(index.IsUnique); - Assert.Equal("MyUniqueConstraint", index.GetName()); + Assert.Equal("MyUniqueConstraint", index.GetDatabaseName()); Assert.Same(entityType.FindProperty("MyColumn"), index.Properties.Single()); } @@ -530,7 +530,7 @@ public void Indexes_and_alternate_keys() indexColumn1 => { Assert.False(indexColumn1.IsUnique); - Assert.Equal("IDX_C1", indexColumn1.GetName()); + Assert.Equal("IDX_C1", indexColumn1.GetDatabaseName()); Assert.Same(entityType.FindProperty("C1"), indexColumn1.Properties.Single()); }, uniqueColumn2 => diff --git a/test/EFCore.Relational.Tests/Infrastructure/RelationalModelValidatorTest.cs b/test/EFCore.Relational.Tests/Infrastructure/RelationalModelValidatorTest.cs index 66a30a5cc3b..1541d52ed5f 100644 --- a/test/EFCore.Relational.Tests/Infrastructure/RelationalModelValidatorTest.cs +++ b/test/EFCore.Relational.Tests/Infrastructure/RelationalModelValidatorTest.cs @@ -692,7 +692,7 @@ public virtual void Detects_duplicate_foreignKey_names_within_hierarchy_with_dif var index1 = fk1.DeclaringEntityType.GetDeclaredIndexes().Single(); var index2 = fk2.DeclaringEntityType.GetDeclaredIndexes().Single(); Assert.NotSame(index1, index2); - Assert.NotEqual(index1.GetName(), index2.GetName()); + Assert.NotEqual(index1.GetDatabaseName(), index2.GetDatabaseName()); } [ConditionalFact] @@ -732,7 +732,7 @@ public virtual void Passes_for_incompatible_foreignKeys_within_hierarchy() var index1 = fk1.DeclaringEntityType.GetDeclaredIndexes().Single(); var index2 = fk2.DeclaringEntityType.GetDeclaredIndexes().Single(); Assert.NotSame(index1, index2); - Assert.Equal(index1.GetName(), index2.GetName()); + Assert.Equal(index1.GetDatabaseName(), index2.GetDatabaseName()); } [ConditionalFact] @@ -753,7 +753,7 @@ public virtual void Passes_for_incompatible_foreignKeys_within_hierarchy_when_on var index1 = fk1.DeclaringEntityType.GetDeclaredIndexes().Single(); var index2 = fk2.DeclaringEntityType.GetDeclaredIndexes().Single(); Assert.NotSame(index1, index2); - Assert.Equal(index1.GetName(), index2.GetName()); + Assert.Equal(index1.GetDatabaseName(), index2.GetDatabaseName()); } [ConditionalFact] @@ -799,7 +799,7 @@ public virtual void Passes_for_compatible_duplicate_foreignKey_names_within_hier var index1 = fk1.DeclaringEntityType.GetDeclaredIndexes().Single(); var index2 = fk2.DeclaringEntityType.GetDeclaredIndexes().Single(); Assert.NotSame(index1, index2); - Assert.Equal(index1.GetName(), index2.GetName()); + Assert.Equal(index1.GetDatabaseName(), index2.GetDatabaseName()); } [ConditionalFact] @@ -847,7 +847,7 @@ public virtual void Passes_for_compatible_duplicate_foreignKey_names_within_hier var index1 = fk1.DeclaringEntityType.GetDeclaredIndexes().Single(); var index2 = fk2.DeclaringEntityType.GetDeclaredIndexes().Single(); Assert.NotSame(index1, index2); - Assert.Equal(index1.GetName(), index2.GetName()); + Assert.Equal(index1.GetDatabaseName(), index2.GetDatabaseName()); } [ConditionalFact] @@ -946,8 +946,8 @@ public virtual void Passes_for_incompatible_indexes_within_hierarchy_when_one_na Validate(modelBuilder.Model); - Assert.Equal("IX_Animal_Name", index1.GetName()); - Assert.Equal("IX_Animal_Name1", index2.GetName()); + Assert.Equal("IX_Animal_Name", index1.GetDatabaseName()); + Assert.Equal("IX_Animal_Name1", index2.GetDatabaseName()); } [ConditionalFact] @@ -973,7 +973,7 @@ public virtual void Passes_for_compatible_duplicate_index_names_within_hierarchy Validate(modelBuilder.Model); Assert.NotSame(index1, index2); - Assert.Equal(index1.GetName(), index2.GetName()); + Assert.Equal(index1.GetDatabaseName(), index2.GetDatabaseName()); } [ConditionalFact] @@ -1248,6 +1248,162 @@ var methodInfo modelBuilder.Model); } + [ConditionalFact] + public void Passes_for_unnamed_index_with_all_properties_not_mapped_to_any_table() + { + var modelBuilder = CreateConventionalModelBuilder(); + + modelBuilder.Entity().ToTable(null); + modelBuilder.Entity().HasIndex(nameof(Animal.Id), nameof(Animal.Name)); + + var definition = RelationalResources + .LogUnnamedIndexAllPropertiesNotToMappedToAnyTable( + new TestLogger()); + VerifyWarning( + definition.GenerateMessage( + nameof(Animal), + "{'Id', 'Name'}"), + modelBuilder.Model, + LogLevel.Information); + } + + [ConditionalFact] + public void Passes_for_named_index_with_all_properties_not_mapped_to_any_table() + { + var modelBuilder = CreateConventionalModelBuilder(); + + modelBuilder.Entity().ToTable(null); + modelBuilder.Entity() + .HasIndex(nameof(Animal.Id), nameof(Animal.Name)) + .HasName("IX_AllPropertiesNotMapped"); + + var definition = RelationalResources + .LogNamedIndexAllPropertiesNotToMappedToAnyTable( + new TestLogger()); + VerifyWarning( + definition.GenerateMessage( + "IX_AllPropertiesNotMapped", + nameof(Animal), + "{'Id', 'Name'}"), + modelBuilder.Model, + LogLevel.Information); + } + + [ConditionalFact] + public void Detects_mix_of_index_property_mapped_and_not_mapped_to_any_table_unmapped_first() + { + var modelBuilder = CreateConventionalModelBuilder(); + + modelBuilder.Entity().ToTable(null); + modelBuilder.Entity().ToTable("Cats"); + modelBuilder.Entity().HasIndex(nameof(Animal.Name), nameof(Cat.Identity)); + + var definition = RelationalResources + .LogUnnamedIndexPropertiesBothMappedAndNotMappedToTable( + new TestLogger()); + VerifyWarning( + definition.GenerateMessage( + nameof(Cat), + "{'Name', 'Identity'}", + "Name"), + modelBuilder.Model, + LogLevel.Error); + } + + [ConditionalFact] + public void Detects_mix_of_index_property_mapped_and_not_mapped_to_any_table_mapped_first() + { + var modelBuilder = CreateConventionalModelBuilder(); + + modelBuilder.Entity().ToTable(null); + modelBuilder.Entity().ToTable("Cats"); + modelBuilder.Entity() + .HasIndex(nameof(Cat.Identity), nameof(Animal.Name)) + .HasName("IX_MixOfMappedAndUnmappedProperties"); + + var definition = RelationalResources + .LogNamedIndexPropertiesBothMappedAndNotMappedToTable( + new TestLogger()); + VerifyWarning( + definition.GenerateMessage( + "IX_MixOfMappedAndUnmappedProperties", + nameof(Cat), + "{'Identity', 'Name'}", + "Name"), + modelBuilder.Model, + LogLevel.Error); + } + + [ConditionalFact] + public void Passes_for_index_properties_mapped_to_same_table_in_TPT_hierarchy() + { + var modelBuilder = CreateConventionalModelBuilder(); + + modelBuilder.Entity().ToTable("Animals"); + modelBuilder.Entity().ToTable("Cats"); + modelBuilder.Entity().HasIndex(nameof(Animal.Id), nameof(Cat.Identity)); + + Validate(modelBuilder.Model); + + Assert.Empty(LoggerFactory.Log + .Where(l => l.Level != LogLevel.Trace && l.Level != LogLevel.Debug)); + } + + [ConditionalFact] + public void Detects_unnamed_index_properties_mapped_to_different_tables_in_TPT_hierarchy() + { + var modelBuilder = CreateConventionalModelBuilder(); + + modelBuilder.Entity().ToTable("Animals"); + modelBuilder.Entity().ToTable("Cats"); + modelBuilder.Entity().HasIndex(nameof(Animal.Name), nameof(Cat.Identity)); + + var definition = RelationalResources + .LogUnnamedIndexPropertiesMappedToNonOverlappingTables( + new TestLogger()); + VerifyWarning( + definition.GenerateMessage( + nameof(Cat), + "{'Name', 'Identity'}", + nameof(Animal.Name), + "{'Animals'}", + nameof(Cat.Identity), + "{'Cats'}"), + modelBuilder.Model, + LogLevel.Error); + } + + [ConditionalFact] + public void Detects_named_index_properties_mapped_to_different_tables_in_TPT_hierarchy() + { + var modelBuilder = CreateConventionalModelBuilder(); + + modelBuilder.Entity().ToTable("Animals"); + modelBuilder.Entity().ToTable("Cats"); + modelBuilder.Entity() + .HasIndex(nameof(Animal.Name), nameof(Cat.Identity)) + .HasName("IX_MappedToDifferentTables"); + + var definition = RelationalResources + .LogNamedIndexPropertiesMappedToNonOverlappingTables( + new TestLogger()); + VerifyWarning( + definition.GenerateMessage( + l => l.Log( + definition.Level, + definition.EventId, + definition.MessageFormat, + "IX_MappedToDifferentTables", + nameof(Cat), + "{'Name', 'Identity'}", + nameof(Animal.Name), + "{'Animals'}", + nameof(Cat.Identity), + "{'Cats'}")), + modelBuilder.Model, + LogLevel.Error); + } + private static void GenerateMapping(IMutableProperty property) => property[CoreAnnotationNames.TypeMapping] = new TestRelationalTypeMappingSource( diff --git a/test/EFCore.Relational.Tests/Metadata/RelationalBuilderExtensionsTest.cs b/test/EFCore.Relational.Tests/Metadata/RelationalBuilderExtensionsTest.cs index b642aec703c..de5bd1d3ad8 100644 --- a/test/EFCore.Relational.Tests/Metadata/RelationalBuilderExtensionsTest.cs +++ b/test/EFCore.Relational.Tests/Metadata/RelationalBuilderExtensionsTest.cs @@ -405,14 +405,14 @@ public void Default_index_name_is_based_on_index_column_names() var index = modelBuilder.Model.FindEntityType(typeof(Customer)).GetIndexes().Single(); - Assert.Equal("IX_Customer_Id", index.GetName()); + Assert.Equal("IX_Customer_Id", index.GetDatabaseName()); modelBuilder .Entity() .Property(e => e.Id) .HasColumnName("Eendax"); - Assert.Equal("IX_Customer_Eendax", index.GetName()); + Assert.Equal("IX_Customer_Eendax", index.GetDatabaseName()); } [ConditionalFact] @@ -427,7 +427,7 @@ public void Can_set_index_name() var index = modelBuilder.Model.FindEntityType(typeof(Customer)).GetIndexes().Single(); - Assert.Equal("Eeeendeeex", index.GetName()); + Assert.Equal("Eeeendeeex", index.GetDatabaseName()); } [ConditionalFact] diff --git a/test/EFCore.Relational.Tests/Metadata/RelationalMetadataBuilderExtensionsTest.cs b/test/EFCore.Relational.Tests/Metadata/RelationalMetadataBuilderExtensionsTest.cs index b63cd45ce98..541da0212a8 100644 --- a/test/EFCore.Relational.Tests/Metadata/RelationalMetadataBuilderExtensionsTest.cs +++ b/test/EFCore.Relational.Tests/Metadata/RelationalMetadataBuilderExtensionsTest.cs @@ -147,6 +147,7 @@ public void Can_access_index() entityTypeBuilder.Property(typeof(int), "Id", ConfigurationSource.Convention); var indexBuilder = entityTypeBuilder.HasIndex(new[] { "Id" }, ConfigurationSource.Convention); +#pragma warning disable CS0618 // Type or member is obsolete Assert.NotNull(indexBuilder.HasName("Splew")); Assert.Equal("Splew", indexBuilder.Metadata.GetName()); @@ -161,6 +162,7 @@ public void Can_access_index() Assert.NotNull(indexBuilder.HasName("Splod")); Assert.Equal("Splod", indexBuilder.Metadata.GetName()); +#pragma warning restore CS0618 // Type or member is obsolete Assert.NotNull(indexBuilder.HasFilter("Splew")); Assert.Equal("Splew", indexBuilder.Metadata.GetFilter()); diff --git a/test/EFCore.Relational.Tests/Metadata/RelationalMetadataExtensionsTest.cs b/test/EFCore.Relational.Tests/Metadata/RelationalMetadataExtensionsTest.cs index 7ef93e4acb4..76d4fe3a172 100644 --- a/test/EFCore.Relational.Tests/Metadata/RelationalMetadataExtensionsTest.cs +++ b/test/EFCore.Relational.Tests/Metadata/RelationalMetadataExtensionsTest.cs @@ -346,6 +346,7 @@ public void Can_get_and_set_index_name() .HasIndex(e => e.Id) .Metadata; +#pragma warning disable CS0618 // Type or member is obsolete Assert.Equal("IX_Customer_Id", index.GetName()); index.SetName("MyIndex"); @@ -355,6 +356,7 @@ public void Can_get_and_set_index_name() index.SetName(null); Assert.Equal("IX_Customer_Id", index.GetName()); +#pragma warning restore CS0618 // Type or member is obsolete } [ConditionalFact] diff --git a/test/EFCore.Relational.Tests/RelationalEventIdTest.cs b/test/EFCore.Relational.Tests/RelationalEventIdTest.cs index b690f3c57af..40cc715b1f7 100644 --- a/test/EFCore.Relational.Tests/RelationalEventIdTest.cs +++ b/test/EFCore.Relational.Tests/RelationalEventIdTest.cs @@ -40,6 +40,7 @@ public void Every_eventId_has_a_logger_method_and_logs_when_level_enabled() var property = new Property( "A", typeof(int), null, null, entityType, ConfigurationSource.Convention, ConfigurationSource.Convention); var contextServices = RelationalTestHelpers.Instance.CreateContextServices(model.FinalizeModel()); + var index = new Metadata.Internal.Index(new List { property }, entityType, ConfigurationSource.Convention); var fakeFactories = new Dictionary> { @@ -65,7 +66,9 @@ public void Every_eventId_has_a_logger_method_and_logs_when_level_enabled() { typeof(IMigrationsAssembly), () => new FakeMigrationsAssembly() }, { typeof(MethodCallExpression), () => Expression.Call(constantExpression, typeof(object).GetMethod("ToString")) }, { typeof(Expression), () => constantExpression }, + { typeof(IEntityType), () => entityType }, { typeof(IProperty), () => property }, + { typeof(IIndex), () => index }, { typeof(TypeInfo), () => typeof(object).GetTypeInfo() }, { typeof(Type), () => typeof(object) }, { typeof(ValueConverter), () => new BoolToZeroOneConverter() }, diff --git a/test/EFCore.SqlServer.FunctionalTests/UpdatesSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/UpdatesSqlServerTest.cs index 7232e64683b..1c491026abc 100644 --- a/test/EFCore.SqlServer.FunctionalTests/UpdatesSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/UpdatesSqlServerTest.cs @@ -68,7 +68,7 @@ public override void Identifiers_are_generated_correctly() entityType.GetForeignKeys().Single().GetConstraintName()); Assert.Equal( "IX_LoginEntityTypeWithAnExtremelyLongAndOverlyConvolutedNameThatIsUsedToVerifyThatTheStoreIdentifierGenerationLengthLimitIsWork~", - entityType.GetIndexes().Single().GetName()); + entityType.GetIndexes().Single().GetDatabaseName()); var entityType2 = context.Model.FindEntityType( typeof( @@ -89,7 +89,7 @@ public override void Identifiers_are_generated_correctly() entityType2.GetProperties().ElementAt(2).GetColumnName()); Assert.Equal( "IX_LoginEntityTypeWithAnExtremelyLongAndOverlyConvolutedNameThatIsUsedToVerifyThatTheStoreIdentifierGenerationLengthLimitIsWor~1", - entityType2.GetIndexes().Single().GetName()); + entityType2.GetIndexes().Single().GetDatabaseName()); } private void AssertSql(params string[] expected) diff --git a/test/EFCore.SqlServer.Tests/Infrastructure/SqlServerModelValidatorTest.cs b/test/EFCore.SqlServer.Tests/Infrastructure/SqlServerModelValidatorTest.cs index a06df8f44ea..c68e77fbcf8 100644 --- a/test/EFCore.SqlServer.Tests/Infrastructure/SqlServerModelValidatorTest.cs +++ b/test/EFCore.SqlServer.Tests/Infrastructure/SqlServerModelValidatorTest.cs @@ -231,8 +231,8 @@ public virtual void Passes_for_compatible_duplicate_convention_indexes_for_forei var model = Validate(modelBuilder.Model); - Assert.Equal("IX_Animal_Name", model.FindEntityType(typeof(Cat)).GetDeclaredIndexes().Single().GetName()); - Assert.Equal("IX_Animal_Name", model.FindEntityType(typeof(Dog)).GetDeclaredIndexes().Single().GetName()); + Assert.Equal("IX_Animal_Name", model.FindEntityType(typeof(Cat)).GetDeclaredIndexes().Single().GetDatabaseName()); + Assert.Equal("IX_Animal_Name", model.FindEntityType(typeof(Dog)).GetDeclaredIndexes().Single().GetDatabaseName()); } [ConditionalFact] diff --git a/test/EFCore.Sqlite.FunctionalTests/UpdatesSqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/UpdatesSqliteTest.cs index f277e129cbf..f6dc0554e9f 100644 --- a/test/EFCore.Sqlite.FunctionalTests/UpdatesSqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/UpdatesSqliteTest.cs @@ -32,7 +32,7 @@ public override void Identifiers_are_generated_correctly() entityType.GetForeignKeys().Single().GetConstraintName()); Assert.Equal( "IX_LoginEntityTypeWithAnExtremelyLongAndOverlyConvolutedNameThatIsUsedToVerifyThatTheStoreIdentifierGenerationLengthLimitIsWorkingCorrectly_ProfileId_ProfileId1_ProfileId3_ProfileId4_ProfileId5_ProfileId6_ProfileId7_ProfileId8_ProfileId9_ProfileId10_ProfileId11_ProfileId12_ProfileId13_ProfileId14_ExtraProperty", - entityType.GetIndexes().Single().GetName()); + entityType.GetIndexes().Single().GetDatabaseName()); } } } diff --git a/test/EFCore.Tests/Infrastructure/EventIdTestBase.cs b/test/EFCore.Tests/Infrastructure/EventIdTestBase.cs index 67d19bf267f..ab2fa5578d6 100644 --- a/test/EFCore.Tests/Infrastructure/EventIdTestBase.cs +++ b/test/EFCore.Tests/Infrastructure/EventIdTestBase.cs @@ -91,7 +91,7 @@ public void TestEventLogging( } catch (Exception) { - Assert.True(false, "Need to add factory for type " + type.DisplayName()); + Assert.True(false, "Need to add fake test factory for type " + type.DisplayName() + " in class " + eventIdType.Name + "Test"); } } } diff --git a/test/EFCore.Tests/Metadata/Conventions/ConventionDispatcherTest.cs b/test/EFCore.Tests/Metadata/Conventions/ConventionDispatcherTest.cs index 11110e12509..441dd6f6a71 100644 --- a/test/EFCore.Tests/Metadata/Conventions/ConventionDispatcherTest.cs +++ b/test/EFCore.Tests/Metadata/Conventions/ConventionDispatcherTest.cs @@ -2812,6 +2812,102 @@ public void ProcessIndexUniquenessChanged( } } + [InlineData(false, false)] + [InlineData(true, false)] + [InlineData(false, true)] + [InlineData(true, true)] + [ConditionalTheory] + public void OnIndexNameChanged_calls_conventions_in_order(bool useBuilder, bool useScope) + { + var conventions = new ConventionSet(); + + var convention1 = new IndexNameChangedConvention(terminate: false); + var convention2 = new IndexNameChangedConvention(terminate: true); + var convention3 = new IndexNameChangedConvention(terminate: false); + conventions.IndexNameChangedConventions.Add(convention1); + conventions.IndexNameChangedConventions.Add(convention2); + conventions.IndexNameChangedConventions.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.HasName("OriginalIndexName", ConfigurationSource.Convention); + } + else + { + index.Name = "OriginalIndexName"; + } + + if (useScope) + { + Assert.Empty(convention1.Calls); + Assert.Empty(convention2.Calls); + scope.Dispose(); + } + + Assert.Equal(new[] { "OriginalIndexName" }, convention1.Calls); + Assert.Equal(new[] { "OriginalIndexName" }, convention2.Calls); + Assert.Empty(convention3.Calls); + + if (useBuilder) + { + index.Builder.HasName("OriginalIndexName", ConfigurationSource.Convention); + } + else + { + index.Name = "OriginalIndexName"; + } + + Assert.Equal(new[] { "OriginalIndexName" }, convention1.Calls); + Assert.Equal(new[] { "OriginalIndexName" }, convention2.Calls); + Assert.Empty(convention3.Calls); + + if (useBuilder) + { + index.Builder.HasName("UpdatedIndexName", ConfigurationSource.Convention); + } + else + { + index.Name = "UpdatedIndexName"; + } + + Assert.Equal(new[] { "OriginalIndexName", "UpdatedIndexName" }, convention1.Calls); + Assert.Equal(new[] { "OriginalIndexName", "UpdatedIndexName" }, convention2.Calls); + Assert.Empty(convention3.Calls); + + Assert.Same(index, entityBuilder.Metadata.RemoveIndex(index.Properties)); + } + + private class IndexNameChangedConvention : IIndexNameChangedConvention + { + private readonly bool _terminate; + public readonly List Calls = new List(); + + public IndexNameChangedConvention(bool terminate) + { + _terminate = terminate; + } + + public void ProcessIndexNameChanged( + IConventionIndexBuilder indexBuilder, IConventionContext context) + { + Assert.NotNull(indexBuilder.Metadata.Builder); + + Calls.Add(indexBuilder.Metadata.Name); + + if (_terminate) + { + context.StopProcessing(); + } + } + } + [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 new file mode 100644 index 00000000000..fac541df31d --- /dev/null +++ b/test/EFCore.Tests/Metadata/Conventions/IndexAttributeConventionTest.cs @@ -0,0 +1,285 @@ +// 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 System.ComponentModel.DataAnnotations.Schema; +using System.Linq; +using Microsoft.EntityFrameworkCore.Diagnostics; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Microsoft.EntityFrameworkCore.Metadata.Conventions.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata.Conventions.Internal; +using Microsoft.EntityFrameworkCore.Metadata.Internal; +using Microsoft.EntityFrameworkCore.TestUtilities; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +// ReSharper disable UnusedMember.Local +// ReSharper disable InconsistentNaming +namespace Microsoft.EntityFrameworkCore.Metadata.Conventions +{ + public class IndexAttributeConventionTest + { + #region IndexAttribute + + [ConditionalFact] + public void IndexAttribute_overrides_configuration_from_convention() + { + var modelBuilder = new InternalModelBuilder(new Model()); + + 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); + entityBuilder.PrimaryKey(new List { "Id" }, ConfigurationSource.Convention); + + var indexProperties = new List { propA.Metadata.Name, propB.Metadata.Name }; + var indexBuilder = entityBuilder.HasIndex(indexProperties, ConfigurationSource.Convention); + indexBuilder.HasName("ConventionalIndexName", ConfigurationSource.Convention); + indexBuilder.IsUnique(false, ConfigurationSource.Convention); + + RunConvention(modelBuilder); + + var index = entityBuilder.Metadata.GetIndexes().Single(); + Assert.Equal(ConfigurationSource.DataAnnotation, index.GetConfigurationSource()); + Assert.Equal("IndexOnAAndB", index.Name); + Assert.Equal(ConfigurationSource.DataAnnotation, index.GetNameConfigurationSource()); + Assert.True(index.IsUnique); + Assert.Equal(ConfigurationSource.DataAnnotation, index.GetIsUniqueConfigurationSource()); + Assert.Collection(index.Properties, + prop0 => Assert.Equal("A", prop0.Name), + prop1 => Assert.Equal("B", prop1.Name)); + } + + [ConditionalFact] + public void IndexAttribute_can_be_overriden_using_explicit_configuration() + { + var modelBuilder = InMemoryTestHelpers.Instance.CreateConventionBuilder(); + var entityBuilder = modelBuilder.Entity(); + + entityBuilder.HasIndex("A", "B") + .HasName("OverridenIndexName") + .IsUnique(false); + + modelBuilder.Model.FinalizeModel(); + + var index = (Metadata.Internal.Index)entityBuilder.Metadata.GetIndexes().Single(); + Assert.Equal(ConfigurationSource.Explicit, index.GetConfigurationSource()); + Assert.Equal("OverridenIndexName", index.Name); + Assert.Equal(ConfigurationSource.Explicit, index.GetNameConfigurationSource()); + Assert.False(index.IsUnique); + Assert.Equal(ConfigurationSource.Explicit, index.GetIsUniqueConfigurationSource()); + Assert.Collection(index.Properties, + prop0 => Assert.Equal("A", prop0.Name), + prop1 => Assert.Equal("B", prop1.Name)); + } + + [ConditionalFact] + 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); + } + + [InlineData(typeof(EntityWithInvalidNullIndexProperty))] + [InlineData(typeof(EntityWithInvalidEmptyIndexProperty))] + [InlineData(typeof(EntityWithInvalidWhiteSpaceIndexProperty))] + [ConditionalTheory] + 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); + } + + [ConditionalFact] + public void IndexAttribute_can_be_applied_more_than_once_per_entity_type() + { + var modelBuilder = InMemoryTestHelpers.Instance.CreateConventionBuilder(); + var entityBuilder = modelBuilder.Entity(); + modelBuilder.Model.FinalizeModel(); + + var indexes = entityBuilder.Metadata.GetIndexes(); + Assert.Equal(2, indexes.Count()); + + var index0 = (Metadata.Internal.Index)indexes.First(); + Assert.Equal(ConfigurationSource.DataAnnotation, index0.GetConfigurationSource()); + Assert.Equal("IndexOnAAndB", index0.Name); + Assert.Equal(ConfigurationSource.DataAnnotation, index0.GetNameConfigurationSource()); + 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 = (Metadata.Internal.Index)indexes.Skip(1).First(); + Assert.Equal(ConfigurationSource.DataAnnotation, index1.GetConfigurationSource()); + Assert.Equal("IndexOnBAndC", index1.Name); + Assert.Equal(ConfigurationSource.DataAnnotation, index1.GetNameConfigurationSource()); + Assert.False(index1.IsUnique); + Assert.Equal(ConfigurationSource.DataAnnotation, index1.GetIsUniqueConfigurationSource()); + Assert.Collection(index1.Properties, + prop0 => Assert.Equal("B", prop0.Name), + prop1 => Assert.Equal("C", prop1.Name)); + } + + [ConditionalFact] + public void IndexAttribute_can_be_inherited_from_base_entity_type() + { + var modelBuilder = InMemoryTestHelpers.Instance.CreateConventionBuilder(); + var entityBuilder = modelBuilder.Entity(); + modelBuilder.Model.FinalizeModel(); + + // assert that the base type is not part of the model + Assert.Empty(modelBuilder.Model.GetEntityTypes() + .Where(e => e.ClrType == typeof(BaseUnmappedEntityWithIndex))); + + // assert that we see the index anyway + var index = (Metadata.Internal.Index)entityBuilder.Metadata.GetIndexes().Single(); + Assert.Equal(ConfigurationSource.DataAnnotation, index.GetConfigurationSource()); + Assert.Equal("IndexOnAAndB", index.Name); + Assert.Equal(ConfigurationSource.DataAnnotation, index.GetNameConfigurationSource()); + Assert.True(index.IsUnique); + Assert.Equal(ConfigurationSource.DataAnnotation, index.GetIsUniqueConfigurationSource()); + Assert.Collection(index.Properties, + prop0 => Assert.Equal("A", prop0.Name), + prop1 => Assert.Equal("B", prop1.Name)); + } + + [ConditionalFact] + public virtual void IndexAttribute_with_an_ignored_property_causes_error() + { + var modelBuilder = InMemoryTestHelpers.Instance.CreateConventionBuilder(); + var entity = modelBuilder.Entity(); + + Assert.Equal( + CoreStrings.IndexDefinedOnIgnoredProperty( + "", + nameof(EntityWithIgnoredProperty), + "{'A', 'B'}", + "B"), + Assert.Throws( + () => modelBuilder.Model.FinalizeModel()).Message); + } + + [ConditionalFact] + public virtual void IndexAttribute_with_a_non_existent_property_causes_error() + { + var modelBuilder = InMemoryTestHelpers.Instance.CreateConventionBuilder(); + var entity = modelBuilder.Entity(); + + Assert.Equal( + CoreStrings.IndexDefinedOnNonExistentProperty( + "IndexOnAAndNonExistentProperty", + nameof(EntityWithNonExistentProperty), + "{'A', 'DoesNotExist'}", + "DoesNotExist"), + Assert.Throws( + () => modelBuilder.Model.FinalizeModel()).Message); + } + + #endregion + + private void RunConvention(InternalModelBuilder modelBuilder) + { + var context = new ConventionContext(modelBuilder.Metadata.ConventionDispatcher); + + new IndexAttributeConvention(CreateDependencies()) + .ProcessModelFinalizing(modelBuilder, context); + } + + private ProviderConventionSetBuilderDependencies CreateDependencies() + => InMemoryTestHelpers.Instance.CreateContextServices().GetRequiredService(); + + [Index(nameof(A), nameof(B), Name = "IndexOnAAndB", IsUnique = true)] + private class EntityWithIndex + { + public int Id { get; set; } + public int A { get; set; } + public int B { get; set; } + } + + [Index(nameof(A), nameof(B), Name = "IndexOnAAndB", IsUnique = true)] + [Index(nameof(B), nameof(C), Name = "IndexOnBAndC", IsUnique = false)] + private class EntityWithTwoIndexes + { + public int Id { get; set; } + public int A { get; set; } + public int B { get; set; } + public int C { get; set; } + } + + [Index(nameof(A), nameof(B), Name = "IndexOnAAndB", IsUnique = true)] + [NotMapped] + private class BaseUnmappedEntityWithIndex + { + public int Id { get; set; } + public int A { get; set; } + public int B { get; set; } + } + + private class EntityWithIndexFromBaseType : BaseUnmappedEntityWithIndex + { + public int C { get; set; } + public int D { get; set; } + } + + [Index] + private class EntityWithInvalidEmptyIndex + { + public int Id { get; set; } + public int A { get; set; } + public int B { get; set; } + } + + [Index(nameof(A), null, Name = "IndexOnAAndNull")] + private class EntityWithInvalidNullIndexProperty + { + public int Id { get; set; } + public int A { get; set; } + public int B { get; set; } + } + + [Index(nameof(A), "", Name = "IndexOnAAndEmpty")] + private class EntityWithInvalidEmptyIndexProperty + { + public int Id { get; set; } + public int A { get; set; } + public int B { get; set; } + } + + [Index(nameof(A), " \r\n\t", Name = "IndexOnAAndWhiteSpace")] + private class EntityWithInvalidWhiteSpaceIndexProperty + { + public int Id { get; set; } + public int A { get; set; } + public int B { get; set; } + } + + [Index(nameof(A), nameof(B))] + private class EntityWithIgnoredProperty + { + public int Id { get; set; } + public int A { get; set; } + [NotMapped] + public int B { get; set; } + } + + [Index(nameof(A), "DoesNotExist", Name = "IndexOnAAndNonExistentProperty")] + private class EntityWithNonExistentProperty + { + public int Id { get; set; } + public int A { get; set; } + public int B { get; set; } + } + } +} diff --git a/test/EFCore.Tests/Metadata/Internal/InternalIndexBuilderTest.cs b/test/EFCore.Tests/Metadata/Internal/InternalIndexBuilderTest.cs index b1336c7f255..c9c0f83c7f0 100644 --- a/test/EFCore.Tests/Metadata/Internal/InternalIndexBuilderTest.cs +++ b/test/EFCore.Tests/Metadata/Internal/InternalIndexBuilderTest.cs @@ -43,6 +43,40 @@ public void Can_only_override_existing_IsUnique_value_explicitly() Assert.False(metadata.IsUnique); } + [ConditionalFact] + public void Can_only_override_lower_source_Name() + { + var builder = CreateInternalIndexBuilder(); + var metadata = builder.Metadata; + + Assert.NotNull(builder.HasName("ConventionIndexName", ConfigurationSource.Convention)); + Assert.NotNull(builder.HasName("AnnotationIndexName", ConfigurationSource.DataAnnotation)); + + Assert.Equal("AnnotationIndexName", metadata.Name); + + Assert.Null(builder.HasName("UpdatedConventionIndexName", ConfigurationSource.Convention)); + + Assert.Equal("AnnotationIndexName", metadata.Name); + } + + [ConditionalFact] + public void Can_only_override_existing_Name_value_explicitly() + { + var builder = CreateInternalIndexBuilder(); + var metadata = builder.Metadata; + metadata.Name = "TestIndexName"; + + Assert.Equal(ConfigurationSource.Explicit, metadata.GetConfigurationSource()); + Assert.NotNull(builder.HasName("TestIndexName", ConfigurationSource.DataAnnotation)); + Assert.Null(builder.HasName("NewIndexName", ConfigurationSource.DataAnnotation)); + + Assert.Equal("TestIndexName", metadata.Name); + + Assert.NotNull(builder.HasName("ExplicitIndexName", ConfigurationSource.Explicit)); + + Assert.Equal("ExplicitIndexName", metadata.Name); + } + private InternalIndexBuilder CreateInternalIndexBuilder() { var modelBuilder = new InternalModelBuilder(new Model()); diff --git a/test/EFCore.Tests/ModelBuilding/ModelBuilderGenericTest.cs b/test/EFCore.Tests/ModelBuilding/ModelBuilderGenericTest.cs index 303670b1bb6..1f5ff552a2e 100644 --- a/test/EFCore.Tests/ModelBuilding/ModelBuilderGenericTest.cs +++ b/test/EFCore.Tests/ModelBuilding/ModelBuilderGenericTest.cs @@ -474,6 +474,9 @@ public override TestIndexBuilder HasAnnotation(string annotation, objec public override TestIndexBuilder IsUnique(bool isUnique = true) => new GenericTestIndexBuilder(IndexBuilder.IsUnique(isUnique)); + public override TestIndexBuilder HasName(string name) + => new GenericTestIndexBuilder(IndexBuilder.HasName(name)); + IndexBuilder IInfrastructure>.Instance => IndexBuilder; } diff --git a/test/EFCore.Tests/ModelBuilding/ModelBuilderNonGenericTest.cs b/test/EFCore.Tests/ModelBuilding/ModelBuilderNonGenericTest.cs index 83e521a7d42..ed2f8eca405 100644 --- a/test/EFCore.Tests/ModelBuilding/ModelBuilderNonGenericTest.cs +++ b/test/EFCore.Tests/ModelBuilding/ModelBuilderNonGenericTest.cs @@ -459,6 +459,9 @@ public override TestIndexBuilder HasAnnotation(string annotation, objec public override TestIndexBuilder IsUnique(bool isUnique = true) => new NonGenericTestIndexBuilder(IndexBuilder.IsUnique(isUnique)); + public override TestIndexBuilder HasName(string name) + => new NonGenericTestIndexBuilder(IndexBuilder.HasName(name)); + IndexBuilder IInfrastructure.Instance => IndexBuilder; } diff --git a/test/EFCore.Tests/ModelBuilding/ModelBuilderTestBase.cs b/test/EFCore.Tests/ModelBuilding/ModelBuilderTestBase.cs index 7fa1ced5ed8..f7c75be5b46 100644 --- a/test/EFCore.Tests/ModelBuilding/ModelBuilderTestBase.cs +++ b/test/EFCore.Tests/ModelBuilding/ModelBuilderTestBase.cs @@ -306,6 +306,7 @@ public abstract class TestIndexBuilder public abstract TestIndexBuilder HasAnnotation(string annotation, object value); public abstract TestIndexBuilder IsUnique(bool isUnique = true); + public abstract TestIndexBuilder HasName(string name); } public abstract class TestPropertyBuilder diff --git a/test/EFCore.Tests/ModelBuilding/NonRelationshipTestBase.cs b/test/EFCore.Tests/ModelBuilding/NonRelationshipTestBase.cs index 454716c3898..6505d6d0d88 100644 --- a/test/EFCore.Tests/ModelBuilding/NonRelationshipTestBase.cs +++ b/test/EFCore.Tests/ModelBuilding/NonRelationshipTestBase.cs @@ -1195,13 +1195,14 @@ public virtual void Can_add_multiple_indexes() var entityBuilder = modelBuilder.Entity(); var firstIndexBuilder = entityBuilder.HasIndex(ix => ix.Id).IsUnique(); - var secondIndexBuilder = entityBuilder.HasIndex(ix => ix.Name).HasAnnotation("A1", "V1"); + var secondIndexBuilder = entityBuilder.HasIndex(ix => ix.Name).HasName("MyIndex").HasAnnotation("A1", "V1"); var entityType = (IEntityType)model.FindEntityType(typeof(Customer)); Assert.Equal(2, entityType.GetIndexes().Count()); Assert.True(firstIndexBuilder.Metadata.IsUnique); Assert.False(((IIndex)secondIndexBuilder.Metadata).IsUnique); + Assert.Equal("MyIndex", secondIndexBuilder.Metadata.Name); Assert.Equal("V1", secondIndexBuilder.Metadata["A1"]); }