diff --git a/src/EFCore/Extensions/ConventionNavigationExtensions.cs b/src/EFCore/Extensions/ConventionNavigationExtensions.cs index e4caa0f6b8a..73b5cec61f5 100644 --- a/src/EFCore/Extensions/ConventionNavigationExtensions.cs +++ b/src/EFCore/Extensions/ConventionNavigationExtensions.cs @@ -22,7 +22,7 @@ public static class ConventionNavigationExtensions /// The inverse navigation, or null if none is defined. /// public static IConventionNavigation FindInverse([NotNull] this IConventionNavigation navigation) - => (IConventionNavigation)((INavigation)navigation).FindInverse(); + => ((Navigation)navigation).FindInverse(); /// /// Gets the entity type that a given navigation property will hold an instance of @@ -31,7 +31,7 @@ public static IConventionNavigation FindInverse([NotNull] this IConventionNaviga /// The navigation property to find the target entity type of. /// The target entity type. public static IConventionEntityType GetTargetType([NotNull] this IConventionNavigation navigation) - => (IConventionEntityType)((INavigation)navigation).GetTargetType(); + => ((Navigation)navigation).GetTargetType(); /// /// Sets a value indicating whether this navigation should be eager loaded by default. diff --git a/src/EFCore/Extensions/EntityTypeExtensions.cs b/src/EFCore/Extensions/EntityTypeExtensions.cs index 9fa89af5a72..12e3ee60728 100644 --- a/src/EFCore/Extensions/EntityTypeExtensions.cs +++ b/src/EFCore/Extensions/EntityTypeExtensions.cs @@ -397,7 +397,7 @@ public static IKey FindKey([NotNull] this IEntityType entityType, [NotNull] IPro /// The property to find the foreign keys on. /// The foreign keys. public static IEnumerable FindForeignKeys([NotNull] this IEntityType entityType, [NotNull] IProperty property) - => entityType.FindForeignKeys(new[] { property }); + => property.GetContainingForeignKeys(); /// /// Gets the foreign keys defined on the given properties. Only foreign keys that are defined on exactly the specified @@ -413,13 +413,8 @@ public static IEnumerable FindForeignKeys( Check.NotEmpty(properties, nameof(properties)); Check.HasNoNulls(properties, nameof(properties)); - foreach (var foreignKey in entityType.GetForeignKeys()) - { - if (PropertyListComparer.Instance.Equals(foreignKey.Properties, properties)) - { - yield return foreignKey; - } - } + return entityType.GetForeignKeys() + .Where(foreignKey => PropertyListComparer.Instance.Equals(foreignKey.Properties, properties)); } /// diff --git a/src/EFCore/Extensions/MutableNavigationExtensions.cs b/src/EFCore/Extensions/MutableNavigationExtensions.cs index 725114b89d9..620ba2a7d74 100644 --- a/src/EFCore/Extensions/MutableNavigationExtensions.cs +++ b/src/EFCore/Extensions/MutableNavigationExtensions.cs @@ -22,7 +22,7 @@ public static class MutableNavigationExtensions /// The inverse navigation, or null if none is defined. /// public static IMutableNavigation FindInverse([NotNull] this IMutableNavigation navigation) - => (IMutableNavigation)((INavigation)navigation).FindInverse(); + => ((Navigation)navigation).FindInverse(); /// /// Gets the entity type that a given navigation property will hold an instance of @@ -31,7 +31,7 @@ public static IMutableNavigation FindInverse([NotNull] this IMutableNavigation n /// The navigation property to find the target entity type of. /// The target entity type. public static IMutableEntityType GetTargetType([NotNull] this IMutableNavigation navigation) - => (IMutableEntityType)((INavigation)navigation).GetTargetType(); + => ((Navigation)navigation).GetTargetType(); /// /// Sets a value indicating whether this navigation should be eager loaded by default. diff --git a/src/EFCore/Extensions/NavigationExtensions.cs b/src/EFCore/Extensions/NavigationExtensions.cs index 06b6faf68d9..eba202e4705 100644 --- a/src/EFCore/Extensions/NavigationExtensions.cs +++ b/src/EFCore/Extensions/NavigationExtensions.cs @@ -62,13 +62,7 @@ public static bool IsCollection([NotNull] this INavigation navigation) /// [DebuggerStepThrough] public static INavigation FindInverse([NotNull] this INavigation navigation) - { - Check.NotNull(navigation, nameof(navigation)); - - return navigation.IsDependentToPrincipal() - ? navigation.ForeignKey.PrincipalToDependent - : navigation.ForeignKey.DependentToPrincipal; - } + => ((Navigation)Check.NotNull(navigation, nameof(navigation))).FindInverse(); /// /// Gets the entity type that a given navigation property will hold an instance of @@ -78,13 +72,7 @@ public static INavigation FindInverse([NotNull] this INavigation navigation) /// The target entity type. [DebuggerStepThrough] public static IEntityType GetTargetType([NotNull] this INavigation navigation) - { - Check.NotNull(navigation, nameof(navigation)); - - return navigation.IsDependentToPrincipal() - ? navigation.ForeignKey.PrincipalEntityType - : navigation.ForeignKey.DeclaringEntityType; - } + => (Check.NotNull(navigation, nameof(navigation)) as Navigation)?.GetTargetType(); /// /// Gets a value indicating whether this navigation should be eager loaded by default. diff --git a/src/EFCore/Infrastructure/ModelValidator.cs b/src/EFCore/Infrastructure/ModelValidator.cs index 2ab1d729899..ecc38f320b8 100644 --- a/src/EFCore/Infrastructure/ModelValidator.cs +++ b/src/EFCore/Infrastructure/ModelValidator.cs @@ -107,6 +107,21 @@ protected virtual void ValidateRelationships( : "." + foreignKey.PrincipalToDependent.Name))); } } + + foreach (var navigation in entityType.GetDeclaredSkipNavigations()) + { + if (!navigation.IsCollection) + { + throw new InvalidOperationException(CoreStrings.SkipNavigationNonCollection( + navigation.Name, navigation.DeclaringEntityType.DisplayName())); + } + + if (navigation.Inverse == null) + { + throw new InvalidOperationException(CoreStrings.SkipNavigationNoInverse( + navigation.Name, navigation.DeclaringEntityType.DisplayName())); + } + } } } @@ -155,6 +170,7 @@ protected virtual void ValidatePropertyMapping( clrProperties.ExceptWith(entityType.GetProperties().Select(p => p.Name)); clrProperties.ExceptWith(entityType.GetNavigations().Select(p => p.Name)); + clrProperties.ExceptWith(entityType.GetSkipNavigations().Select(p => p.Name)); clrProperties.ExceptWith(entityType.GetServiceProperties().Select(p => p.Name)); clrProperties.RemoveWhere(p => entityType.FindIgnoredConfigurationSource(p) != null); diff --git a/src/EFCore/Metadata/Builders/IConventionSkipNavigationBuilder.cs b/src/EFCore/Metadata/Builders/IConventionSkipNavigationBuilder.cs new file mode 100644 index 00000000000..97cc68d1e79 --- /dev/null +++ b/src/EFCore/Metadata/Builders/IConventionSkipNavigationBuilder.cs @@ -0,0 +1,22 @@ +// 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. + +namespace Microsoft.EntityFrameworkCore.Metadata.Builders +{ + /// + /// + /// Provides a simple API surface for configuring an from conventions. + /// + /// + /// This interface is typically used by database providers (and other extensions). It is generally + /// not used in application code. + /// + /// + public interface IConventionSkipNavigationBuilder : IConventionAnnotatableBuilder + { + /// + /// The navigation property being configured. + /// + new IConventionSkipNavigation Metadata { get; } + } +} diff --git a/src/EFCore/Metadata/Conventions/ConventionSet.cs b/src/EFCore/Metadata/Conventions/ConventionSet.cs index b296fc09bc5..8219dbe0c9e 100644 --- a/src/EFCore/Metadata/Conventions/ConventionSet.cs +++ b/src/EFCore/Metadata/Conventions/ConventionSet.cs @@ -121,11 +121,41 @@ public class ConventionSet /// public virtual IList NavigationAddedConventions { get; } = new List(); + /// + /// Conventions to run when an annotation is changed on a navigation property. + /// + public virtual IList NavigationAnnotationChangedConventions { get; } + = new List(); + /// /// Conventions to run when a navigation property is removed. /// public virtual IList NavigationRemovedConventions { get; } = new List(); + /// + /// Conventions to run when a skip navigation property is added. + /// + public virtual IList SkipNavigationAddedConventions { get; } + = new List(); + + /// + /// Conventions to run when an annotation is changed on a skip navigation property. + /// + public virtual IList SkipNavigationAnnotationChangedConventions { get; } + = new List(); + + /// + /// Conventions to run when a skip navigation inverse is changed. + /// + public virtual IList SkipNavigationInverseChangedConventions { get; } + = new List(); + + /// + /// Conventions to run when a skip navigation property is removed. + /// + public virtual IList SkipNavigationRemovedConventions { get; } + = new List(); + /// /// Conventions to run when a key is added. /// @@ -187,6 +217,11 @@ public class ConventionSet public virtual IList PropertyAnnotationChangedConventions { get; } = new List(); + /// + /// Conventions to run when a property is removed. + /// + public virtual IList PropertyRemovedConventions { get; } = new List(); + /// /// Replaces an existing convention with a derived convention. /// diff --git a/src/EFCore/Metadata/Conventions/INavigationAnnotationChangedConvention.cs b/src/EFCore/Metadata/Conventions/INavigationAnnotationChangedConvention.cs new file mode 100644 index 00000000000..8497930f5a9 --- /dev/null +++ b/src/EFCore/Metadata/Conventions/INavigationAnnotationChangedConvention.cs @@ -0,0 +1,31 @@ +// 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 an annotation is changed on a navigation. + /// + public interface INavigationAnnotationChangedConvention : IConvention + { + /// + /// Called after an annotation is changed on a navigation. + /// + /// The builder for the foreign key. + /// The navigation. + /// The annotation name. + /// The new annotation. + /// The old annotation. + /// Additional information associated with convention execution. + void ProcessNavigationAnnotationChanged( + [NotNull] IConventionRelationshipBuilder relationshipBuilder, + [NotNull] IConventionNavigation navigation, + [NotNull] string name, + [CanBeNull] IConventionAnnotation annotation, + [CanBeNull] IConventionAnnotation oldAnnotation, + [NotNull] IConventionContext context); + } +} diff --git a/src/EFCore/Metadata/Conventions/IPropertyRemovedConvention.cs b/src/EFCore/Metadata/Conventions/IPropertyRemovedConvention.cs new file mode 100644 index 00000000000..06efe5c2de7 --- /dev/null +++ b/src/EFCore/Metadata/Conventions/IPropertyRemovedConvention.cs @@ -0,0 +1,25 @@ +// 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 a property is removed from the entity type. + /// + public interface IPropertyRemovedConvention : IConvention + { + /// + /// Called after a property is removed from the entity type. + /// + /// The builder for the entity type that contained the property. + /// The removed property. + /// Additional information associated with convention execution. + void ProcessPropertyRemoved( + [NotNull] IConventionEntityTypeBuilder entityTypeBuilder, + [NotNull] IConventionProperty property, + [NotNull] IConventionContext context); + } +} diff --git a/src/EFCore/Metadata/Conventions/ISkipNavigationAddedConvention.cs b/src/EFCore/Metadata/Conventions/ISkipNavigationAddedConvention.cs new file mode 100644 index 00000000000..b22b563c227 --- /dev/null +++ b/src/EFCore/Metadata/Conventions/ISkipNavigationAddedConvention.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 a skip navigation is added to the entity type. + /// + public interface ISkipNavigationAddedConvention : IConvention + { + /// + /// Called after a skip navigation is added to the entity type. + /// + /// The builder for the skip navigation. + /// Additional information associated with convention execution. + void ProcessSkipNavigationAdded( + [NotNull] IConventionSkipNavigationBuilder skipNavigationBuilder, + [NotNull] IConventionContext context); + } +} diff --git a/src/EFCore/Metadata/Conventions/ISkipNavigationAnnotationChangedConvention.cs b/src/EFCore/Metadata/Conventions/ISkipNavigationAnnotationChangedConvention.cs new file mode 100644 index 00000000000..90c701529ae --- /dev/null +++ b/src/EFCore/Metadata/Conventions/ISkipNavigationAnnotationChangedConvention.cs @@ -0,0 +1,29 @@ +// 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 an annotation is changed on a skip navigation. + /// + public interface ISkipNavigationAnnotationChangedConvention : IConvention + { + /// + /// Called after an annotation is changed on a skip navigation. + /// + /// The builder for the skip navigation. + /// The annotation name. + /// The new annotation. + /// The old annotation. + /// Additional information associated with convention execution. + void ProcessSkipNavigationAnnotationChanged( + [NotNull] IConventionSkipNavigationBuilder skipNavigationBuilder, + [NotNull] string name, + [CanBeNull] IConventionAnnotation annotation, + [CanBeNull] IConventionAnnotation oldAnnotation, + [NotNull] IConventionContext context); + } +} diff --git a/src/EFCore/Metadata/Conventions/ISkipNavigationInverseChangedConvention.cs b/src/EFCore/Metadata/Conventions/ISkipNavigationInverseChangedConvention.cs new file mode 100644 index 00000000000..24697101879 --- /dev/null +++ b/src/EFCore/Metadata/Conventions/ISkipNavigationInverseChangedConvention.cs @@ -0,0 +1,27 @@ +// 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 a skip navigation inverse is changed. + /// + public interface ISkipNavigationInverseChangedConvention : IConvention + { + /// + /// Called after a skip navigation inverse is changed. + /// + /// The builder for the skip navigation. + /// The current inverse skip navigation. + /// The old inverse skip navigation. + /// Additional information associated with convention execution. + void ProcessSkipNavigationInverseChanged( + [NotNull] IConventionSkipNavigationBuilder skipNavigationBuilder, + [NotNull] IConventionSkipNavigation inverse, + [NotNull] IConventionSkipNavigation oldInverse, + [NotNull] IConventionContext context); + } +} diff --git a/src/EFCore/Metadata/Conventions/ISkipNavigationRemovedConvention.cs b/src/EFCore/Metadata/Conventions/ISkipNavigationRemovedConvention.cs new file mode 100644 index 00000000000..850772fa88e --- /dev/null +++ b/src/EFCore/Metadata/Conventions/ISkipNavigationRemovedConvention.cs @@ -0,0 +1,25 @@ +// 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 a skip navigation is removed from the entity type. + /// + public interface ISkipNavigationRemovedConvention : IConvention + { + /// + /// Called after a skip navigation is removed from the entity type. + /// + /// The builder for the entity type that contained the navigation. + /// The removed navigation. + /// Additional information associated with convention execution. + void ProcessSkipNavigationRemoved( + [NotNull] IConventionEntityTypeBuilder entityTypeBuilder, + [NotNull] IConventionSkipNavigation navigation, + [NotNull] IConventionContext context); + } +} diff --git a/src/EFCore/Metadata/Conventions/Internal/ConventionDispatcher.ConventionScope.cs b/src/EFCore/Metadata/Conventions/Internal/ConventionDispatcher.ConventionScope.cs index 650f9a9a0a3..8d1be5be210 100644 --- a/src/EFCore/Metadata/Conventions/Internal/ConventionDispatcher.ConventionScope.cs +++ b/src/EFCore/Metadata/Conventions/Internal/ConventionDispatcher.ConventionScope.cs @@ -141,6 +141,31 @@ public abstract string OnNavigationRemoved( [NotNull] string navigationName, [CanBeNull] MemberInfo memberInfo); + public abstract IConventionAnnotation OnNavigationAnnotationChanged( + [NotNull] IConventionRelationshipBuilder relationshipBuilder, + [NotNull] IConventionNavigation navigation, + [NotNull] string name, + [CanBeNull] IConventionAnnotation annotation, + [CanBeNull] IConventionAnnotation oldAnnotation); + + public abstract IConventionSkipNavigationBuilder OnSkipNavigationAdded( + [NotNull] IConventionSkipNavigationBuilder navigationBuilder); + + public abstract IConventionAnnotation OnSkipNavigationAnnotationChanged( + [NotNull] IConventionSkipNavigationBuilder navigationBuilder, + [NotNull] string name, + [CanBeNull] IConventionAnnotation annotation, + [CanBeNull] IConventionAnnotation oldAnnotation); + + public abstract IConventionSkipNavigation OnSkipNavigationInverseChanged( + [NotNull] IConventionSkipNavigationBuilder navigationBuilder, + [NotNull] IConventionSkipNavigation inverse, + [NotNull] IConventionSkipNavigation oldInverse); + + public abstract IConventionSkipNavigation OnSkipNavigationRemoved( + [NotNull] IConventionEntityTypeBuilder entityTypeBuilder, + [NotNull] IConventionSkipNavigation navigation); + public abstract IConventionPropertyBuilder OnPropertyAdded([NotNull] IConventionPropertyBuilder propertyBuilder); public abstract IConventionAnnotation OnPropertyAnnotationChanged( @@ -153,6 +178,8 @@ public abstract FieldInfo OnPropertyFieldChanged( [NotNull] IConventionPropertyBuilder propertyBuilder, FieldInfo newFieldInfo, [CanBeNull] FieldInfo oldFieldInfo); public abstract IConventionPropertyBuilder OnPropertyNullableChanged([NotNull] IConventionPropertyBuilder propertyBuilder); + + public abstract IConventionProperty OnPropertyRemoved([NotNull] IConventionEntityTypeBuilder entityTypeBuilder, [NotNull] IConventionProperty property); } } } diff --git a/src/EFCore/Metadata/Conventions/Internal/ConventionDispatcher.DelayedConventionScope.cs b/src/EFCore/Metadata/Conventions/Internal/ConventionDispatcher.DelayedConventionScope.cs index 0f21202c421..aa374e6fca7 100644 --- a/src/EFCore/Metadata/Conventions/Internal/ConventionDispatcher.DelayedConventionScope.cs +++ b/src/EFCore/Metadata/Conventions/Internal/ConventionDispatcher.DelayedConventionScope.cs @@ -199,6 +199,17 @@ public override IConventionNavigation OnNavigationAdded( return navigation; } + public override IConventionAnnotation OnNavigationAnnotationChanged( + IConventionRelationshipBuilder relationshipBuilder, + IConventionNavigation navigation, + string name, + IConventionAnnotation annotation, + IConventionAnnotation oldAnnotation) + { + Add(new OnNavigationAnnotationChangedNode(relationshipBuilder, navigation, name, annotation, oldAnnotation)); + return annotation; + } + public override string OnNavigationRemoved( IConventionEntityTypeBuilder sourceEntityTypeBuilder, IConventionEntityTypeBuilder targetEntityTypeBuilder, @@ -209,6 +220,40 @@ public override string OnNavigationRemoved( return navigationName; } + public override IConventionSkipNavigationBuilder OnSkipNavigationAdded( + IConventionSkipNavigationBuilder navigationBuilder) + { + Add(new OnSkipNavigationAddedNode(navigationBuilder)); + return navigationBuilder; + } + + public override IConventionAnnotation OnSkipNavigationAnnotationChanged( + IConventionSkipNavigationBuilder navigationBuilder, + string name, + IConventionAnnotation annotation, + IConventionAnnotation oldAnnotation) + { + Add(new OnSkipNavigationAnnotationChangedNode(navigationBuilder, name, annotation, oldAnnotation)); + return annotation; + } + + public override IConventionSkipNavigation OnSkipNavigationInverseChanged( + IConventionSkipNavigationBuilder navigationBuilder, + IConventionSkipNavigation inverse, + IConventionSkipNavigation oldInverse) + { + Add(new OnSkipNavigationInverseChangedNode(navigationBuilder, inverse, oldInverse)); + return inverse; + } + + public override IConventionSkipNavigation OnSkipNavigationRemoved( + IConventionEntityTypeBuilder entityTypeBuilder, + IConventionSkipNavigation navigation) + { + Add(new OnSkipNavigationRemovedNode(entityTypeBuilder, navigation)); + return navigation; + } + public override IConventionRelationshipBuilder OnForeignKeyPropertiesChanged( IConventionRelationshipBuilder relationshipBuilder, IReadOnlyList oldDependentProperties, @@ -274,6 +319,14 @@ public override IConventionAnnotation OnPropertyAnnotationChanged( Add(new OnPropertyAnnotationChangedNode(propertyBuilder, name, annotation, oldAnnotation)); return annotation; } + + public override IConventionProperty OnPropertyRemoved( + IConventionEntityTypeBuilder entityTypeBuilder, + IConventionProperty property) + { + Add(new OnPropertyRemovedNode(entityTypeBuilder, property)); + return property; + } } private sealed class OnModelAnnotationChangedNode : ConventionNode @@ -545,6 +598,33 @@ public override void Run(ConventionDispatcher dispatcher) => dispatcher._immediateConventionScope.OnNavigationAdded(RelationshipBuilder, Navigation); } + private sealed class OnNavigationAnnotationChangedNode : ConventionNode + { + public OnNavigationAnnotationChangedNode( + IConventionRelationshipBuilder relationshipBuilder, + IConventionNavigation navigation, + string name, + IConventionAnnotation annotation, + IConventionAnnotation oldAnnotation) + { + RelationshipBuilder = relationshipBuilder; + Navigation = navigation; + Name = name; + Annotation = annotation; + OldAnnotation = oldAnnotation; + } + + public IConventionRelationshipBuilder RelationshipBuilder { get; } + public IConventionNavigation Navigation { get; } + public string Name { get; } + public IConventionAnnotation Annotation { get; } + public IConventionAnnotation OldAnnotation { get; } + + public override void Run(ConventionDispatcher dispatcher) + => dispatcher._immediateConventionScope.OnNavigationAnnotationChanged( + RelationshipBuilder, Navigation, Name, Annotation, OldAnnotation); + } + private sealed class OnNavigationRemovedNode : ConventionNode { public OnNavigationRemovedNode( @@ -569,6 +649,80 @@ public override void Run(ConventionDispatcher dispatcher) SourceEntityTypeBuilder, TargetEntityTypeBuilder, NavigationName, MemberInfo); } + private sealed class OnSkipNavigationAddedNode : ConventionNode + { + public OnSkipNavigationAddedNode(IConventionSkipNavigationBuilder navigationBuilder) + { + NavigationBuilder = navigationBuilder; + } + + public IConventionSkipNavigationBuilder NavigationBuilder { get; } + + public override void Run(ConventionDispatcher dispatcher) + => dispatcher._immediateConventionScope.OnSkipNavigationAdded(NavigationBuilder); + } + + private sealed class OnSkipNavigationAnnotationChangedNode : ConventionNode + { + public OnSkipNavigationAnnotationChangedNode( + IConventionSkipNavigationBuilder navigationBuilder, + string name, + IConventionAnnotation annotation, + IConventionAnnotation oldAnnotation) + { + NavigationBuilder = navigationBuilder; + Name = name; + Annotation = annotation; + OldAnnotation = oldAnnotation; + } + + public IConventionSkipNavigationBuilder NavigationBuilder { get; } + public string Name { get; } + public IConventionAnnotation Annotation { get; } + public IConventionAnnotation OldAnnotation { get; } + + public override void Run(ConventionDispatcher dispatcher) + => dispatcher._immediateConventionScope.OnSkipNavigationAnnotationChanged( + NavigationBuilder, Name, Annotation, OldAnnotation); + } + + private sealed class OnSkipNavigationInverseChangedNode : ConventionNode + { + public OnSkipNavigationInverseChangedNode( + IConventionSkipNavigationBuilder navigationBuilder, + IConventionSkipNavigation inverse, + IConventionSkipNavigation oldInverse) + { + NavigationBuilder = navigationBuilder; + Inverse = inverse; + OldInverse = oldInverse; + } + + public IConventionSkipNavigationBuilder NavigationBuilder { get; } + public IConventionSkipNavigation Inverse { get; } + public IConventionSkipNavigation OldInverse { get; } + + public override void Run(ConventionDispatcher dispatcher) + => dispatcher._immediateConventionScope.OnSkipNavigationInverseChanged(NavigationBuilder, Inverse, OldInverse); + } + + private sealed class OnSkipNavigationRemovedNode : ConventionNode + { + public OnSkipNavigationRemovedNode( + IConventionEntityTypeBuilder entityTypeBuilder, + IConventionSkipNavigation navigation) + { + EntityTypeBuilder = entityTypeBuilder; + Navigation = navigation; + } + + public IConventionEntityTypeBuilder EntityTypeBuilder { get; } + public IConventionSkipNavigation Navigation { get; } + + public override void Run(ConventionDispatcher dispatcher) + => dispatcher._immediateConventionScope.OnSkipNavigationRemoved(EntityTypeBuilder, Navigation); + } + private sealed class OnKeyAddedNode : ConventionNode { public OnKeyAddedNode(IConventionKeyBuilder keyBuilder) @@ -768,5 +922,22 @@ public override void Run(ConventionDispatcher dispatcher) => dispatcher._immediateConventionScope.OnPropertyAnnotationChanged( PropertyBuilder, Name, Annotation, OldAnnotation); } + + private sealed class OnPropertyRemovedNode : ConventionNode + { + public OnPropertyRemovedNode( + IConventionEntityTypeBuilder entityTypeBuilder, + IConventionProperty property) + { + EntityTypeBuilder = entityTypeBuilder; + Property = property; + } + + public IConventionEntityTypeBuilder EntityTypeBuilder { get; } + public IConventionProperty Property { get; } + + public override void Run(ConventionDispatcher dispatcher) + => dispatcher._immediateConventionScope.OnPropertyRemoved(EntityTypeBuilder, Property); + } } } diff --git a/src/EFCore/Metadata/Conventions/Internal/ConventionDispatcher.ImmediateConventionScope.cs b/src/EFCore/Metadata/Conventions/Internal/ConventionDispatcher.ImmediateConventionScope.cs index e9514bce63b..156e63bc02d 100644 --- a/src/EFCore/Metadata/Conventions/Internal/ConventionDispatcher.ImmediateConventionScope.cs +++ b/src/EFCore/Metadata/Conventions/Internal/ConventionDispatcher.ImmediateConventionScope.cs @@ -6,6 +6,7 @@ using System.Reflection; using JetBrains.Annotations; using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Microsoft.EntityFrameworkCore.Utilities; namespace Microsoft.EntityFrameworkCore.Metadata.Conventions.Internal { @@ -19,12 +20,15 @@ private sealed class ImmediateConventionScope : ConventionScope private readonly ConventionContext _entityTypeConventionContext; private readonly ConventionContext _relationshipBuilderConventionContext; private readonly ConventionContext _foreignKeyConventionContext; + private readonly ConventionContext _skipNavigationBuilderConventionContext; + private readonly ConventionContext _skipNavigationConventionContext; private readonly ConventionContext _navigationConventionContext; private readonly ConventionContext _indexBuilderConventionContext; private readonly ConventionContext _indexConventionContext; private readonly ConventionContext _keyBuilderConventionContext; private readonly ConventionContext _keyConventionContext; private readonly ConventionContext _propertyBuilderConventionContext; + private readonly ConventionContext _propertyConventionContext; private readonly ConventionContext _modelBuilderConventionContext; private readonly ConventionContext _annotationConventionContext; private readonly ConventionContext _stringConventionContext; @@ -38,12 +42,15 @@ public ImmediateConventionScope([NotNull] ConventionSet conventionSet, Conventio _entityTypeConventionContext = new ConventionContext(dispatcher); _relationshipBuilderConventionContext = new ConventionContext(dispatcher); _foreignKeyConventionContext = new ConventionContext(dispatcher); + _skipNavigationBuilderConventionContext = new ConventionContext(dispatcher); + _skipNavigationConventionContext = new ConventionContext(dispatcher); _navigationConventionContext = new ConventionContext(dispatcher); _indexBuilderConventionContext = new ConventionContext(dispatcher); _indexConventionContext = new ConventionContext(dispatcher); _keyBuilderConventionContext = new ConventionContext(dispatcher); _keyConventionContext = new ConventionContext(dispatcher); _propertyBuilderConventionContext = new ConventionContext(dispatcher); + _propertyConventionContext = new ConventionContext(dispatcher); _modelBuilderConventionContext = new ConventionContext(dispatcher); _annotationConventionContext = new ConventionContext(dispatcher); _stringConventionContext = new ConventionContext(dispatcher); @@ -600,6 +607,36 @@ public override IConventionNavigation OnNavigationAdded( return navigation; } + public override IConventionAnnotation OnNavigationAnnotationChanged( + IConventionRelationshipBuilder relationshipBuilder, + IConventionNavigation navigation, + string name, + IConventionAnnotation annotation, + IConventionAnnotation oldAnnotation) + { + if (relationshipBuilder.Metadata.Builder == null + || relationshipBuilder.Metadata.GetNavigation(navigation.IsDependentToPrincipal()) != navigation) + { + return null; + } + + using (_dispatcher.DelayConventions()) + { + _annotationConventionContext.ResetState(annotation); + foreach (var navigationConvention in _conventionSet.NavigationAnnotationChangedConventions) + { + navigationConvention.ProcessNavigationAnnotationChanged( + relationshipBuilder, navigation, name, annotation, oldAnnotation, _annotationConventionContext); + if (_annotationConventionContext.ShouldStopProcessing()) + { + return _annotationConventionContext.Result; + } + } + } + + return annotation; + } + public override string OnNavigationRemoved( IConventionEntityTypeBuilder sourceEntityTypeBuilder, IConventionEntityTypeBuilder targetEntityTypeBuilder, @@ -639,6 +676,140 @@ public override string OnNavigationRemoved( return navigationName; } + public override IConventionSkipNavigationBuilder OnSkipNavigationAdded( + IConventionSkipNavigationBuilder navigationBuilder) + { + if (navigationBuilder.Metadata.DeclaringEntityType.Builder == null) + { + return null; + } + + using (_dispatcher.DelayConventions()) + { + _skipNavigationBuilderConventionContext.ResetState(navigationBuilder); + foreach (var skipNavigationConvention in _conventionSet.SkipNavigationAddedConventions) + { + if (navigationBuilder.Metadata.Builder == null) + { + Check.DebugAssert(false, "null builder"); + return null; + } + + skipNavigationConvention.ProcessSkipNavigationAdded(navigationBuilder, _skipNavigationBuilderConventionContext); + if (_skipNavigationBuilderConventionContext.ShouldStopProcessing()) + { + return _skipNavigationBuilderConventionContext.Result; + } + } + } + + if (navigationBuilder.Metadata.Builder == null) + { + return null; + } + + return navigationBuilder; + } + + public override IConventionAnnotation OnSkipNavigationAnnotationChanged( + IConventionSkipNavigationBuilder navigationBuilder, + string name, + IConventionAnnotation annotation, + IConventionAnnotation oldAnnotation) + { + if (navigationBuilder.Metadata.Builder == null) + { + return null; + } + + using (_dispatcher.DelayConventions()) + { + _annotationConventionContext.ResetState(annotation); + foreach (var skipNavigationConvention in _conventionSet.SkipNavigationAnnotationChangedConventions) + { + if (navigationBuilder.Metadata.Builder != null + && navigationBuilder.Metadata.FindAnnotation(name) != annotation) + { + Check.DebugAssert(false, "annotation removed"); + return null; + } + + skipNavigationConvention.ProcessSkipNavigationAnnotationChanged( + navigationBuilder, name, annotation, oldAnnotation, _annotationConventionContext); + if (_annotationConventionContext.ShouldStopProcessing()) + { + return _annotationConventionContext.Result; + } + } + } + + return annotation; + } + + public override IConventionSkipNavigation OnSkipNavigationInverseChanged( + IConventionSkipNavigationBuilder navigationBuilder, + IConventionSkipNavigation inverse, + IConventionSkipNavigation oldInverse) + { + if (navigationBuilder.Metadata.DeclaringEntityType.Builder == null) + { + return null; + } + + using (_dispatcher.DelayConventions()) + { + _skipNavigationConventionContext.ResetState(inverse); + foreach (var skipNavigationConvention in _conventionSet.SkipNavigationInverseChangedConventions) + { + if (navigationBuilder.Metadata.Builder == null + || navigationBuilder.Metadata.Inverse != inverse) + { + Check.DebugAssert(false, "inverse changed"); + return null; + } + + skipNavigationConvention.ProcessSkipNavigationInverseChanged( + navigationBuilder, inverse, oldInverse, _skipNavigationConventionContext); + if (_skipNavigationConventionContext.ShouldStopProcessing()) + { + return _skipNavigationConventionContext.Result; + } + } + } + + if (navigationBuilder.Metadata.Builder == null) + { + return null; + } + + return inverse; + } + + public override IConventionSkipNavigation OnSkipNavigationRemoved( + IConventionEntityTypeBuilder entityTypeBuilder, + IConventionSkipNavigation navigation) + { + if (entityTypeBuilder.Metadata.Builder == null) + { + return null; + } + + using (_dispatcher.DelayConventions()) + { + _skipNavigationConventionContext.ResetState(navigation); + foreach (var skipNavigationConvention in _conventionSet.SkipNavigationRemovedConventions) + { + skipNavigationConvention.ProcessSkipNavigationRemoved(entityTypeBuilder, navigation, _skipNavigationConventionContext); + if (_skipNavigationConventionContext.ShouldStopProcessing()) + { + return _skipNavigationConventionContext.Result; + } + } + } + + return navigation; + } + public override IConventionKeyBuilder OnKeyAdded(IConventionKeyBuilder keyBuilder) { if (keyBuilder.Metadata.DeclaringEntityType.Builder == null) @@ -953,6 +1124,31 @@ public override IConventionAnnotation OnPropertyAnnotationChanged( return annotation; } + + public override IConventionProperty OnPropertyRemoved( + IConventionEntityTypeBuilder entityTypeBuilder, + IConventionProperty property) + { + if (entityTypeBuilder.Metadata.Builder == null) + { + return null; + } + + using (_dispatcher.DelayConventions()) + { + _propertyConventionContext.ResetState(property); + foreach (var propertyConvention in _conventionSet.PropertyRemovedConventions) + { + propertyConvention.ProcessPropertyRemoved(entityTypeBuilder, property, _propertyConventionContext); + if (_propertyConventionContext.ShouldStopProcessing()) + { + return _propertyConventionContext.Result; + } + } + } + + return property; + } } } } diff --git a/src/EFCore/Metadata/Conventions/Internal/ConventionDispatcher.cs b/src/EFCore/Metadata/Conventions/Internal/ConventionDispatcher.cs index 6e0fcd79301..dad11083014 100644 --- a/src/EFCore/Metadata/Conventions/Internal/ConventionDispatcher.cs +++ b/src/EFCore/Metadata/Conventions/Internal/ConventionDispatcher.cs @@ -287,6 +287,89 @@ public virtual string OnNavigationRemoved( navigationName, memberInfo); + /// + /// 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 IConventionAnnotation OnNavigationAnnotationChanged( + [NotNull] IConventionRelationshipBuilder relationshipBuilder, + [NotNull] IConventionNavigation navigation, + [NotNull] string name, + [CanBeNull] IConventionAnnotation annotation, + [CanBeNull] IConventionAnnotation oldAnnotation) + { + if (CoreAnnotationNames.AllNames.Contains(name)) + { + return annotation; + } + + return _scope.OnNavigationAnnotationChanged( + relationshipBuilder, + navigation, + name, + annotation, + oldAnnotation); + } + + /// + /// 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 IConventionSkipNavigationBuilder OnSkipNavigationAdded( + [NotNull] IConventionSkipNavigationBuilder navigationBuilder) + => _scope.OnSkipNavigationAdded(navigationBuilder); + + /// + /// 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 IConventionSkipNavigation OnSkipNavigationInverseChanged( + [NotNull] IConventionSkipNavigationBuilder navigationBuilder, + [NotNull] IConventionSkipNavigation inverse, + [NotNull] IConventionSkipNavigation oldInverse) + => _scope.OnSkipNavigationInverseChanged(navigationBuilder, inverse, oldInverse); + + /// + /// 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 IConventionSkipNavigation OnSkipNavigationRemoved( + [NotNull] IConventionEntityTypeBuilder entityTypeBuilder, + [NotNull] IConventionSkipNavigation navigation) + => _scope.OnSkipNavigationRemoved(entityTypeBuilder, navigation); + + /// + /// 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 IConventionAnnotation OnSkipNavigationAnnotationChanged( + [NotNull] IConventionSkipNavigationBuilder navigationBuilder, + [NotNull] string name, + [CanBeNull] IConventionAnnotation annotation, + [CanBeNull] IConventionAnnotation oldAnnotation) + { + if (CoreAnnotationNames.AllNames.Contains(name)) + { + return annotation; + } + + return _scope.OnSkipNavigationAnnotationChanged( + navigationBuilder, + name, + annotation, + oldAnnotation); + } + /// /// 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 @@ -402,6 +485,17 @@ public virtual IConventionAnnotation OnIndexAnnotationChanged( public virtual IConventionPropertyBuilder OnPropertyAdded([NotNull] IConventionPropertyBuilder propertyBuilder) => _scope.OnPropertyAdded(propertyBuilder); + /// + /// 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 IConventionProperty OnPropertyRemoved( + [NotNull] IConventionEntityTypeBuilder entityTypeBuilder, + [NotNull] IConventionProperty property) + => _scope.OnPropertyRemoved(entityTypeBuilder, property); + /// /// 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/IConventionEntityType.cs b/src/EFCore/Metadata/IConventionEntityType.cs index 7c8f0e4ac34..87332cceb39 100644 --- a/src/EFCore/Metadata/IConventionEntityType.cs +++ b/src/EFCore/Metadata/IConventionEntityType.cs @@ -3,9 +3,11 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Reflection; using JetBrains.Annotations; using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Microsoft.EntityFrameworkCore.Utilities; namespace Microsoft.EntityFrameworkCore.Metadata { @@ -167,6 +169,85 @@ IConventionForeignKey AddForeignKey( /// The foreign key to be removed. void RemoveForeignKey([NotNull] IConventionForeignKey foreignKey); + /// + /// Adds a new skip navigation properties to this entity type. + /// + /// The name of the skip navigation property to add. + /// + /// + /// The corresponding CLR type member or null for a shadow property. + /// + /// + /// An indexer with a string parameter and object return type can be used. + /// + /// + /// The entity type that the skip navigation property will hold an instance(s) of. + /// The foreign key to the association type. + /// Whether the navigation property is a collection property. + /// + /// Whether the navigation property is defined on the principal side of the underlying foreign key. + /// + /// Indicates whether the configuration was specified using a data annotation. + /// The newly created skip navigation property. + IConventionSkipNavigation AddSkipNavigation( + [NotNull] string name, + [CanBeNull] MemberInfo memberInfo, + [NotNull] IConventionEntityType targetEntityType, + [NotNull] IConventionForeignKey foreignKey, + bool collection, + bool onPrincipal, + bool fromDataAnnotation = false); + + /// + /// Gets a skip navigation property on this entity type. Returns null if no navigation property is found. + /// + /// The navigation property on the entity class. + /// The navigation property, or null if none is found. + new IConventionSkipNavigation FindSkipNavigation([NotNull] MemberInfo memberInfo) + => (IConventionSkipNavigation)((IEntityType)this).FindSkipNavigation(memberInfo); + + /// + /// Gets a skip navigation property on this entity type. Returns null if no skip navigation property is found. + /// + /// The name of the navigation property on the entity class. + /// The navigation property, or null if none is found. + new IConventionSkipNavigation FindSkipNavigation([NotNull] string name); + + /// + /// Gets a skip navigation property on this entity type. Does not return skip navigation properties defined on a base type. + /// Returns null if no skip navigation property is found. + /// + /// The name of the navigation property on the entity class. + /// The navigation property, or null if none is found. + new IConventionSkipNavigation FindDeclaredSkipNavigation([NotNull] string name) + => (IConventionSkipNavigation)((IEntityType)this).FindDeclaredSkipNavigation(name); + + /// + /// + /// Gets all skip navigation properties declared on this entity type. + /// + /// + /// This method does not return skip navigation properties declared declared on base types. + /// It is useful when iterating over all entity types to avoid processing the same foreign key more than once. + /// Use to also return skip navigation properties declared on base types. + /// + /// + /// Declared foreign keys. + new IEnumerable GetDeclaredSkipNavigations() + => ((IEntityType)this).GetDeclaredSkipNavigations().Cast(); + + /// + /// Gets all skip navigation properties on this entity type. + /// + /// All skip navigation properties on this entity type. + new IEnumerable GetSkipNavigations(); + + /// + /// Removes a skip navigation property from this entity type. + /// + /// The skip navigation to be removed. + void RemoveSkipNavigation([NotNull] IConventionSkipNavigation navigation); + /// /// Adds an index to this entity type. /// diff --git a/src/EFCore/Metadata/IConventionForeignKey.cs b/src/EFCore/Metadata/IConventionForeignKey.cs index be65d4e9520..048e006fdbe 100644 --- a/src/EFCore/Metadata/IConventionForeignKey.cs +++ b/src/EFCore/Metadata/IConventionForeignKey.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 System.Linq; using System.Reflection; using JetBrains.Annotations; using Microsoft.EntityFrameworkCore.Metadata.Builders; @@ -207,5 +208,12 @@ void SetProperties( /// /// The configuration source for . ConfigurationSource? GetPrincipalToDependentConfigurationSource(); + + /// + /// Gets all skip navigations using this foreign key. + /// + /// The skip navigations using this foreign key. + new IEnumerable GetReferencingSkipNavigations() + => ((IForeignKey)this).GetReferencingSkipNavigations().Cast(); } } diff --git a/src/EFCore/Metadata/IConventionNavigation.cs b/src/EFCore/Metadata/IConventionNavigation.cs index 99b9339321c..b666ba8f88e 100644 --- a/src/EFCore/Metadata/IConventionNavigation.cs +++ b/src/EFCore/Metadata/IConventionNavigation.cs @@ -15,7 +15,7 @@ namespace Microsoft.EntityFrameworkCore.Metadata public interface IConventionNavigation : INavigation, IConventionPropertyBase { /// - /// Gets the type that this property belongs to. + /// Gets the type that this navigation property belongs to. /// new IConventionEntityType DeclaringEntityType { get; } diff --git a/src/EFCore/Metadata/IConventionSkipNavigation.cs b/src/EFCore/Metadata/IConventionSkipNavigation.cs new file mode 100644 index 00000000000..4edba214618 --- /dev/null +++ b/src/EFCore/Metadata/IConventionSkipNavigation.cs @@ -0,0 +1,72 @@ +// 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 +{ + /// + /// + /// Represents a navigation property that is part of a relationship + /// that is forwarded through a third entity type. + /// + /// + /// This interface is used during model creation and allows the metadata to be modified. + /// Once the model is built, represents a read-only view of the same metadata. + /// + /// + public interface IConventionSkipNavigation : ISkipNavigation, IConventionPropertyBase + { + /// + /// Gets the builder that can be used to configure this property. + /// + IConventionSkipNavigationBuilder Builder { get; } + + /// + /// Gets the type that this navigation property belongs to. + /// + new IConventionEntityType DeclaringEntityType => IsOnPrincipal ? ForeignKey.PrincipalEntityType : ForeignKey.DeclaringEntityType; + + /// + /// Gets the association type used by the foreign key. + /// + new IConventionEntityType AssociationEntityType => IsOnPrincipal ? ForeignKey.DeclaringEntityType : ForeignKey.PrincipalEntityType; + + /// + /// Gets the entity type that this navigation property will hold an instance(s) of. + /// + new IConventionEntityType TargetEntityType { get; } + + /// + /// Gets the foreign key to the association type. + /// + new IConventionForeignKey ForeignKey { get; } + + /// + /// Gets the inverse skip navigation. + /// + new IConventionSkipNavigation Inverse { get; } + + /// + /// Returns the configuration source for this property. + /// + /// The configuration source. + ConfigurationSource GetConfigurationSource(); + + /// + /// Sets the inverse skip navigation. + /// + /// + /// The inverse skip navigation. Passing null will result in there being no inverse navigation property defined. + /// + /// Indicates whether the configuration was specified using a data annotation. + IConventionSkipNavigation SetInverse([CanBeNull] IConventionSkipNavigation inverse, bool fromDataAnnotation = false); + + /// + /// Returns the configuration source for . + /// + /// The configuration source for . + ConfigurationSource? GetInverseConfigurationSource(); + } +} diff --git a/src/EFCore/Metadata/IEntityType.cs b/src/EFCore/Metadata/IEntityType.cs index 3ec6aacf054..64cf67caeab 100644 --- a/src/EFCore/Metadata/IEntityType.cs +++ b/src/EFCore/Metadata/IEntityType.cs @@ -2,7 +2,10 @@ // 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 System.Reflection; using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Utilities; namespace Microsoft.EntityFrameworkCore.Metadata { @@ -69,6 +72,58 @@ IForeignKey FindForeignKey( /// The foreign keys defined on this entity type. IEnumerable GetForeignKeys(); + /// + /// Gets a skip navigation property on this entity type. Returns null if no navigation property is found. + /// + /// The navigation property on the entity class. + /// The navigation property, or null if none is found. + ISkipNavigation FindSkipNavigation([NotNull] MemberInfo memberInfo) + => FindSkipNavigation(Check.NotNull(memberInfo, nameof(memberInfo)).GetSimpleMemberName()); + + /// + /// Gets a skip navigation property on this entity type. Returns null if no skip navigation property is found. + /// + /// The name of the navigation property on the entity class. + /// The navigation property, or null if none is found. + ISkipNavigation FindSkipNavigation([NotNull] string name); + + /// + /// + /// Gets a skip navigation property on this entity type. + /// + /// + /// Does not return skip navigation properties defined on a base type. + /// Returns null if no skip navigation property is found. + /// + /// + /// The name of the navigation property on the entity class. + /// The navigation property, or null if none is found. + ISkipNavigation FindDeclaredSkipNavigation([NotNull] string name) + { + var navigation = FindSkipNavigation(name); + return navigation?.DeclaringEntityType == this ? navigation : null; + } + + /// + /// + /// Gets all skip navigation properties declared on this entity type. + /// + /// + /// This method does not return skip navigation properties declared declared on base types. + /// It is useful when iterating over all entity types to avoid processing the same foreign key more than once. + /// Use to also return skip navigation properties declared on base types. + /// + /// + /// Declared foreign keys. + IEnumerable GetDeclaredSkipNavigations() + => GetSkipNavigations().Where(n => n.DeclaringEntityType == this); + + /// + /// Gets all skip navigation properties on this entity type. + /// + /// All skip navigation properties on this entity type. + IEnumerable GetSkipNavigations(); + /// /// Gets the index defined on the given properties. Returns null if no index is defined. /// diff --git a/src/EFCore/Metadata/IForeignKey.cs b/src/EFCore/Metadata/IForeignKey.cs index 794c0aa5bc0..6ecac7cf913 100644 --- a/src/EFCore/Metadata/IForeignKey.cs +++ b/src/EFCore/Metadata/IForeignKey.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 System.Linq; using Microsoft.EntityFrameworkCore.Infrastructure; namespace Microsoft.EntityFrameworkCore.Metadata @@ -68,5 +69,13 @@ public interface IForeignKey : IAnnotatable /// principal is deleted or the relationship is severed. /// DeleteBehavior DeleteBehavior { get; } + + /// + /// Gets all skip navigations using this foreign key. + /// + /// The skip navigations using this foreign key. + IEnumerable GetReferencingSkipNavigations() + => PrincipalEntityType.GetSkipNavigations().Where(n => n.IsOnPrincipal && n.ForeignKey == this) + .Concat(DeclaringEntityType.GetSkipNavigations().Where(n => !n.IsOnPrincipal && n.ForeignKey == this)); } } diff --git a/src/EFCore/Metadata/IMutableEntityType.cs b/src/EFCore/Metadata/IMutableEntityType.cs index 93e667589bd..23ce720e8a0 100644 --- a/src/EFCore/Metadata/IMutableEntityType.cs +++ b/src/EFCore/Metadata/IMutableEntityType.cs @@ -3,8 +3,10 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Reflection; using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Utilities; namespace Microsoft.EntityFrameworkCore.Metadata { @@ -125,6 +127,83 @@ IMutableForeignKey AddForeignKey( /// The foreign key to be removed. void RemoveForeignKey([NotNull] IMutableForeignKey foreignKey); + /// + /// Adds a new skip navigation properties to this entity type. + /// + /// The name of the skip navigation property to add. + /// + /// + /// The corresponding CLR type member or null for a shadow property. + /// + /// + /// An indexer with a string parameter and object return type can be used. + /// + /// + /// The entity type that the skip navigation property will hold an instance(s) of. + /// The foreign key to the association type. + /// Whether the navigation property is a collection property. + /// + /// Whether the navigation property is defined on the principal side of the underlying foreign key. + /// + /// The newly created skip navigation property. + IMutableSkipNavigation AddSkipNavigation( + [NotNull] string name, + [CanBeNull] MemberInfo memberInfo, + [NotNull] IMutableEntityType targetEntityType, + [NotNull] IMutableForeignKey foreignKey, + bool collection, + bool onPrincipal); + + /// + /// Gets a skip navigation property on this entity type. Returns null if no navigation property is found. + /// + /// The navigation property on the entity class. + /// The navigation property, or null if none is found. + new IMutableSkipNavigation FindSkipNavigation([NotNull] MemberInfo memberInfo) + => (IMutableSkipNavigation)((IEntityType)this).FindSkipNavigation(memberInfo); + + /// + /// Gets a skip navigation property on this entity type. Returns null if no skip navigation property is found. + /// + /// The name of the navigation property on the entity class. + /// The navigation property, or null if none is found. + new IMutableSkipNavigation FindSkipNavigation([NotNull] string name); + + /// + /// Gets a skip navigation property on this entity type. Does not return skip navigation properties defined on a base type. + /// Returns null if no skip navigation property is found. + /// + /// The name of the navigation property on the entity class. + /// The navigation property, or null if none is found. + new IMutableSkipNavigation FindDeclaredSkipNavigation([NotNull] string name) + => (IMutableSkipNavigation)((IEntityType)this).FindDeclaredSkipNavigation(name); + + /// + /// + /// Gets all skip navigation properties declared on this entity type. + /// + /// + /// This method does not return skip navigation properties declared declared on base types. + /// It is useful when iterating over all entity types to avoid processing the same foreign key more than once. + /// Use to also return skip navigation properties declared on base types. + /// + /// + /// Declared foreign keys. + new IEnumerable GetDeclaredSkipNavigations() + => ((IEntityType)this).GetDeclaredSkipNavigations().Cast(); + + /// + /// Gets all skip navigation properties on this entity type. + /// + /// All skip navigation properties on this entity type. + new IEnumerable GetSkipNavigations(); + + /// + /// Removes a skip navigation properties from this entity type. + /// + /// The skip navigation to be removed. + void RemoveSkipNavigation([NotNull] IMutableSkipNavigation navigation); + /// /// Adds an index to this entity type. /// diff --git a/src/EFCore/Metadata/IMutableForeignKey.cs b/src/EFCore/Metadata/IMutableForeignKey.cs index 26c1ce3f097..64284f7338c 100644 --- a/src/EFCore/Metadata/IMutableForeignKey.cs +++ b/src/EFCore/Metadata/IMutableForeignKey.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 System.Linq; using System.Reflection; using JetBrains.Annotations; @@ -122,5 +123,12 @@ public interface IMutableForeignKey : IForeignKey, IMutableAnnotatable /// /// The newly created navigation property. IMutableNavigation HasPrincipalToDependent([CanBeNull] MemberInfo property); + + /// + /// Gets all skip navigations using this foreign key. + /// + /// The skip navigations using this foreign key. + new IEnumerable GetReferencingSkipNavigations() + => ((IForeignKey)this).GetReferencingSkipNavigations().Cast(); } } diff --git a/src/EFCore/Metadata/IMutableNavigation.cs b/src/EFCore/Metadata/IMutableNavigation.cs index ef96b910dae..cbd610e626b 100644 --- a/src/EFCore/Metadata/IMutableNavigation.cs +++ b/src/EFCore/Metadata/IMutableNavigation.cs @@ -15,7 +15,7 @@ namespace Microsoft.EntityFrameworkCore.Metadata public interface IMutableNavigation : INavigation, IMutablePropertyBase { /// - /// Gets the type that this property belongs to. + /// Gets the type that this navigation property belongs to. /// new IMutableEntityType DeclaringEntityType { get; } diff --git a/src/EFCore/Metadata/IMutableSkipNavigation.cs b/src/EFCore/Metadata/IMutableSkipNavigation.cs new file mode 100644 index 00000000000..c999e40e359 --- /dev/null +++ b/src/EFCore/Metadata/IMutableSkipNavigation.cs @@ -0,0 +1,53 @@ +// 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 +{ + /// + /// + /// Represents a navigation property that is part of a relationship + /// that is forwarded through a third entity type. + /// + /// + /// This interface is used during model creation and allows the metadata to be modified. + /// Once the model is built, represents a read-only view of the same metadata. + /// + /// + public interface IMutableSkipNavigation : ISkipNavigation, IMutablePropertyBase + { + /// + /// Gets the type that this navigation property belongs to. + /// + new IMutableEntityType DeclaringEntityType => IsOnPrincipal ? ForeignKey.PrincipalEntityType : ForeignKey.DeclaringEntityType; + + /// + /// Gets the association type used by the foreign key. + /// + new IMutableEntityType AssociationEntityType => IsOnPrincipal ? ForeignKey.DeclaringEntityType : ForeignKey.PrincipalEntityType; + + /// + /// Gets the entity type that this navigation property will hold an instance(s) of. + /// + new IMutableEntityType TargetEntityType { get; } + + /// + /// Gets the foreign key to the association type. + /// + new IMutableForeignKey ForeignKey { get; } + + /// + /// Gets the inverse skip navigation. + /// + new IMutableSkipNavigation Inverse { get; } + + /// + /// Sets the inverse skip navigation. + /// + /// + /// The inverse skip navigation. Passing null will result in there being no inverse navigation property defined. + /// + IConventionSkipNavigation SetInverse([CanBeNull] IMutableSkipNavigation inverse); + } +} diff --git a/src/EFCore/Metadata/INavigation.cs b/src/EFCore/Metadata/INavigation.cs index 239af697a8b..685e9f356d0 100644 --- a/src/EFCore/Metadata/INavigation.cs +++ b/src/EFCore/Metadata/INavigation.cs @@ -9,7 +9,7 @@ namespace Microsoft.EntityFrameworkCore.Metadata public interface INavigation : IPropertyBase { /// - /// Gets the entity type that this property belongs to. + /// Gets the entity type that this navigation property belongs to. /// IEntityType DeclaringEntityType { get; } diff --git a/src/EFCore/Metadata/IPropertyBase.cs b/src/EFCore/Metadata/IPropertyBase.cs index 9d2208fed88..1050c10374e 100644 --- a/src/EFCore/Metadata/IPropertyBase.cs +++ b/src/EFCore/Metadata/IPropertyBase.cs @@ -13,29 +13,29 @@ namespace Microsoft.EntityFrameworkCore.Metadata public interface IPropertyBase : IAnnotatable { /// - /// Gets the name of the property. + /// Gets the name of this property-like object. /// string Name { get; } /// - /// Gets the type that this property belongs to. + /// Gets the type that this property-like object belongs to. /// ITypeBase DeclaringType { get; } /// - /// Gets the type of value that this property holds. + /// Gets the type of value that this property-like object holds. /// Type ClrType { get; } /// - /// Gets the for the underlying CLR property that this - /// object represents. This may be null for shadow properties or properties mapped directly to fields. + /// Gets the for the underlying CLR property for this property-like object. + /// This may be null for shadow properties or if mapped directly to a field. /// PropertyInfo PropertyInfo { get; } /// - /// Gets the for the underlying CLR field for this property. - /// This may be null for shadow properties or if the backing field for the property is not known. + /// Gets the for the underlying CLR field for this property-like object. + /// This may be null for shadow properties or if the backing field is not known. /// FieldInfo FieldInfo { get; } } diff --git a/src/EFCore/Metadata/ISkipNavigation.cs b/src/EFCore/Metadata/ISkipNavigation.cs new file mode 100644 index 00000000000..2ab04b220f0 --- /dev/null +++ b/src/EFCore/Metadata/ISkipNavigation.cs @@ -0,0 +1,63 @@ +// 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 Microsoft.EntityFrameworkCore.Metadata.Internal; + +namespace Microsoft.EntityFrameworkCore.Metadata +{ + /// + /// Represents a navigation property that is part of a relationship + /// that is forwarded through a third entity type. + /// + public interface ISkipNavigation : IPropertyBase + { + /// + /// Gets the entity type that this navigation belongs to. + /// + IEntityType DeclaringEntityType => IsOnPrincipal ? ForeignKey.PrincipalEntityType : ForeignKey.DeclaringEntityType; + + /// + /// Gets the association type used by the foreign key. + /// + IEntityType AssociationEntityType => IsOnPrincipal ? ForeignKey.DeclaringEntityType : ForeignKey.PrincipalEntityType; + + /// + /// Gets the entity type that this navigation property will hold an instance(s) of. + /// + IEntityType TargetEntityType { get; } + + /// + /// Gets the foreign key to the association type. + /// + IForeignKey ForeignKey { get; } + + /// + /// Gets the inverse skip navigation. + /// + ISkipNavigation Inverse { get; } + + /// + /// Gets a value indicating whether the navigation property is a collection property. + /// + bool IsCollection { get; } + + /// + /// Gets a value indicating whether the navigation property is defined on the principal side of the underlying foreign key. + /// + bool IsOnPrincipal { get; } + + /// + /// Gets a value indicating whether this navigation should be eager loaded by default. + /// + bool IsEagerLoaded + => (bool?)this[CoreAnnotationNames.EagerLoaded] ?? false; + + /// + /// Gets the for this navigation property, which must be a collection + /// navigation. + /// + /// The accessor. + IClrCollectionAccessor GetCollectionAccessor() + => new ClrCollectionAccessorFactory().Create(this); + } +} diff --git a/src/EFCore/Metadata/Internal/ClrCollectionAccessorFactory.cs b/src/EFCore/Metadata/Internal/ClrCollectionAccessorFactory.cs index 549f1306129..ea8801d2db9 100644 --- a/src/EFCore/Metadata/Internal/ClrCollectionAccessorFactory.cs +++ b/src/EFCore/Metadata/Internal/ClrCollectionAccessorFactory.cs @@ -49,27 +49,30 @@ private static readonly MethodInfo _createObservableHashSet /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public virtual IClrCollectionAccessor Create([NotNull] INavigation navigation) - { - MemberInfo GetMostDerivedMemberInfo() - { - var propertyInfo = navigation.PropertyInfo; - var fieldInfo = navigation.FieldInfo; + => !navigation.IsCollection() || navigation.IsShadowProperty() ? null : Create(navigation, navigation.GetTargetType()); - return fieldInfo == null - ? propertyInfo - : propertyInfo == null - ? fieldInfo - : fieldInfo.FieldType.IsAssignableFrom(propertyInfo.PropertyType) - ? (MemberInfo)propertyInfo - : fieldInfo; - } + /// + /// 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 IClrCollectionAccessor Create([NotNull] ISkipNavigation navigation) + => !navigation.IsCollection || navigation.IsShadowProperty() ? null : Create(navigation, navigation.TargetEntityType); + private IClrCollectionAccessor Create(IPropertyBase navigation, IEntityType targetType) + { // ReSharper disable once SuspiciousTypeConversion.Global if (navigation is IClrCollectionAccessor accessor) { return accessor; } + if (targetType == null) + { + return null; + } + var memberInfo = GetMostDerivedMemberInfo(); var propertyType = memberInfo.GetMemberType(); var elementType = propertyType.TryGetElementType(typeof(IEnumerable<>)); @@ -79,9 +82,9 @@ MemberInfo GetMostDerivedMemberInfo() throw new InvalidOperationException( CoreStrings.NavigationBadType( navigation.Name, - navigation.DeclaringEntityType.DisplayName(), + navigation.DeclaringType.DisplayName(), propertyType.ShortDisplayName(), - navigation.GetTargetType().DisplayName())); + targetType.DisplayName())); } if (propertyType.IsArray) @@ -89,7 +92,7 @@ MemberInfo GetMostDerivedMemberInfo() throw new InvalidOperationException( CoreStrings.NavigationArray( navigation.Name, - navigation.DeclaringEntityType.DisplayName(), + navigation.DeclaringType.DisplayName(), propertyType.ShortDisplayName())); } @@ -105,6 +108,20 @@ MemberInfo GetMostDerivedMemberInfo() { throw invocationException.InnerException; } + + MemberInfo GetMostDerivedMemberInfo() + { + var propertyInfo = navigation.PropertyInfo; + var fieldInfo = navigation.FieldInfo; + + return fieldInfo == null + ? propertyInfo + : propertyInfo == null + ? fieldInfo + : fieldInfo.FieldType.IsAssignableFrom(propertyInfo.PropertyType) + ? (MemberInfo)propertyInfo + : fieldInfo; + } } [UsedImplicitly] diff --git a/src/EFCore/Metadata/Internal/EntityType.cs b/src/EFCore/Metadata/Internal/EntityType.cs index 3f4c6a341f6..81f540f5689 100644 --- a/src/EFCore/Metadata/Internal/EntityType.cs +++ b/src/EFCore/Metadata/Internal/EntityType.cs @@ -34,6 +34,9 @@ private readonly SortedSet _foreignKeys private readonly SortedDictionary _navigations = new SortedDictionary(StringComparer.Ordinal); + private readonly SortedDictionary _skipNavigations + = new SortedDictionary(StringComparer.Ordinal); + private readonly SortedDictionary, Index> _indexes = new SortedDictionary, Index>(PropertyListComparer.Instance); @@ -73,7 +76,7 @@ private readonly SortedDictionary _serviceProperties public EntityType([NotNull] string name, [NotNull] Model model, ConfigurationSource configurationSource) : base(name, model, configurationSource) { - _properties = new SortedDictionary(new PropertyComparer(this)); + _properties = new SortedDictionary(new PropertyNameComparer(this)); Builder = new InternalEntityTypeBuilder(this, model.Builder); } @@ -91,7 +94,7 @@ public EntityType([NotNull] Type clrType, [NotNull] Model model, ConfigurationSo throw new ArgumentException(CoreStrings.InvalidEntityType(clrType)); } - _properties = new SortedDictionary(new PropertyComparer(this)); + _properties = new SortedDictionary(new PropertyNameComparer(this)); Builder = new InternalEntityTypeBuilder(this, model.Builder); } @@ -139,8 +142,8 @@ public EntityType( /// public virtual InternalEntityTypeBuilder Builder { - [DebuggerStepThrough] get; - [DebuggerStepThrough] + get; + [param: CanBeNull] set; } @@ -187,9 +190,7 @@ public virtual bool IsKeyless /// 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 void HasNoKey( - bool? keyless, - ConfigurationSource configurationSource) + public virtual void HasNoKey(bool? keyless, ConfigurationSource configurationSource) { if (_isKeyless == keyless) { @@ -246,9 +247,7 @@ public virtual void UpdateIsKeylessConfigurationSource(ConfigurationSource confi /// 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 void HasBaseType( - [CanBeNull] EntityType newBaseType, - ConfigurationSource configurationSource) + public virtual void HasBaseType([CanBeNull] EntityType newBaseType, ConfigurationSource configurationSource) { Check.DebugAssert(Builder != null, "Builder is null"); @@ -485,7 +484,8 @@ protected override IConventionAnnotation OnAnnotationSet( public virtual IEnumerable FindMembersInHierarchy([NotNull] string name) => FindPropertiesInHierarchy(name).Cast() .Concat(FindServicePropertiesInHierarchy(name)) - .Concat(FindNavigationsInHierarchy(name)); + .Concat(FindNavigationsInHierarchy(name)) + .Concat(FindSkipNavigationsInHierarchy(name)); #region Primary and Candidate Keys @@ -804,12 +804,13 @@ public virtual Key RemoveKey([NotNull] IReadOnlyList properties) /// public virtual Key RemoveKey([NotNull] Key key) { + Check.NotNull(key, nameof(key)); Check.DebugAssert(Builder != null, "Builder is null"); if (key.DeclaringEntityType != this) { throw new InvalidOperationException( - CoreStrings.KeyWrongType(key.Properties.Format(), key.DeclaringEntityType.DisplayName(), this.DisplayName())); + CoreStrings.KeyWrongType(key.Properties.Format(), this.DisplayName(), key.DeclaringEntityType.DisplayName())); } CheckKeyNotInUse(key); @@ -844,8 +845,11 @@ private void CheckKeyNotInUse(Key key) var foreignKey = key.GetReferencingForeignKeys().FirstOrDefault(); if (foreignKey != null) { - throw new InvalidOperationException( - CoreStrings.KeyInUse(key.Properties.Format(), this.DisplayName(), foreignKey.DeclaringEntityType.DisplayName())); + throw new InvalidOperationException(CoreStrings.KeyInUse( + key.Properties.Format(), + this.DisplayName(), + foreignKey.Properties.Format(), + foreignKey.DeclaringEntityType.DisplayName())); } } @@ -1200,6 +1204,9 @@ public virtual ForeignKey RemoveForeignKey( /// public virtual ForeignKey RemoveForeignKey([NotNull] ForeignKey foreignKey) { + Check.NotNull(foreignKey, nameof(foreignKey)); + Check.DebugAssert(Builder != null, "Builder is null"); + if (foreignKey.DeclaringEntityType != this) { throw new InvalidOperationException( @@ -1207,8 +1214,19 @@ public virtual ForeignKey RemoveForeignKey([NotNull] ForeignKey foreignKey) foreignKey.Properties.Format(), foreignKey.PrincipalKey.Properties.Format(), foreignKey.PrincipalEntityType.DisplayName(), - foreignKey.DeclaringEntityType.DisplayName(), - this.DisplayName())); + this.DisplayName(), + foreignKey.DeclaringEntityType.DisplayName())); + } + + var referencingSkipNavigation = foreignKey.ReferencingSkipNavigations?.FirstOrDefault(); + if (referencingSkipNavigation != null) + { + throw new InvalidOperationException( + CoreStrings.ForeignKeyInUseSkipNavigation( + foreignKey.Properties.Format(), + this.DisplayName(), + referencingSkipNavigation.Name, + referencingSkipNavigation.DeclaringType.DisplayName())); } if (foreignKey.DependentToPrincipal != null) @@ -1243,9 +1261,7 @@ public virtual ForeignKey RemoveForeignKey([NotNull] ForeignKey foreignKey) foreignKey.PrincipalToDependent.GetIdentifyingMemberInfo()); } - Model.ConventionDispatcher.OnForeignKeyRemoved(Builder, foreignKey); - - return foreignKey; + return (ForeignKey)Model.ConventionDispatcher.OnForeignKeyRemoved(Builder, foreignKey); } /// @@ -1306,19 +1322,19 @@ public virtual Navigation AddNavigation( /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public virtual Navigation AddNavigation( - [NotNull] MemberInfo navigationProperty, + [NotNull] MemberInfo navigationMember, [NotNull] ForeignKey foreignKey, bool pointsToPrincipal) { - Check.NotNull(navigationProperty, nameof(navigationProperty)); + Check.NotNull(navigationMember, nameof(navigationMember)); Check.NotNull(foreignKey, nameof(foreignKey)); - return AddNavigation(new MemberIdentity(navigationProperty), foreignKey, pointsToPrincipal); + return AddNavigation(new MemberIdentity(navigationMember), foreignKey, pointsToPrincipal); } - private Navigation AddNavigation(MemberIdentity propertyIdentity, ForeignKey foreignKey, bool pointsToPrincipal) + private Navigation AddNavigation(MemberIdentity navigationMember, ForeignKey foreignKey, bool pointsToPrincipal) { - var name = propertyIdentity.Name; + var name = navigationMember.Name; var duplicateNavigation = FindNavigationsInHierarchy(name).FirstOrDefault(); if (duplicateNavigation != null) { @@ -1337,14 +1353,12 @@ private Navigation AddNavigation(MemberIdentity propertyIdentity, ForeignKey for name, this.DisplayName(), duplicateNavigation.DeclaringEntityType.DisplayName())); } - var duplicateProperty = FindPropertiesInHierarchy(name).Cast() - .Concat(FindServicePropertiesInHierarchy(name)).FirstOrDefault(); + var duplicateProperty = FindMembersInHierarchy(name).FirstOrDefault(); if (duplicateProperty != null) { throw new InvalidOperationException( CoreStrings.ConflictingPropertyOrNavigation( - name, this.DisplayName(), - duplicateProperty.DeclaringType.DisplayName())); + name, this.DisplayName(), duplicateProperty.DeclaringType.DisplayName())); } Check.DebugAssert( @@ -1355,20 +1369,28 @@ private Navigation AddNavigation(MemberIdentity propertyIdentity, ForeignKey for (pointsToPrincipal ? foreignKey.DeclaringEntityType : foreignKey.PrincipalEntityType) == this, "EntityType mismatch"); - var navigationProperty = propertyIdentity.MemberInfo - ?? ClrType?.GetMembersInHierarchy(name).FirstOrDefault(); + var memberInfo = navigationMember.MemberInfo; + if (memberInfo != null) + { + ValidateClrMember(name, memberInfo); + } + else + { + memberInfo = ClrType?.GetMembersInHierarchy(name).FirstOrDefault(); + } + if (ClrType != null) { Navigation.IsCompatible( - propertyIdentity.Name, - navigationProperty, + name, + memberInfo, this, pointsToPrincipal ? foreignKey.PrincipalEntityType : foreignKey.DeclaringEntityType, !pointsToPrincipal && !foreignKey.IsUnique, shouldThrow: true); } - var navigation = new Navigation(name, navigationProperty as PropertyInfo, navigationProperty as FieldInfo, foreignKey); + var navigation = new Navigation(name, memberInfo as PropertyInfo, memberInfo as FieldInfo, foreignKey); _navigations.Add(name, navigation); @@ -1440,11 +1462,11 @@ public virtual IEnumerable GetDerivedNavigationsInclusive() /// 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 IEnumerable FindDerivedNavigations([NotNull] string navigationName) + public virtual IEnumerable FindDerivedNavigations([NotNull] string name) { - Check.NotNull(navigationName, nameof(navigationName)); + Check.NotNull(name, nameof(name)); - return GetDerivedTypes().Select(et => et.FindDeclaredNavigation(navigationName)).Where(n => n != null); + return GetDerivedTypes().Select(et => et.FindDeclaredNavigation(name)).Where(n => n != null); } /// @@ -1453,8 +1475,8 @@ public virtual IEnumerable FindDerivedNavigations([NotNull] string n /// 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 IEnumerable FindNavigationsInHierarchy([NotNull] string navigationName) - => ToEnumerable(FindNavigation(navigationName)).Concat(FindDerivedNavigations(navigationName)); + public virtual IEnumerable FindNavigationsInHierarchy([NotNull] string name) + => ToEnumerable(FindNavigation(name)).Concat(FindDerivedNavigations(name)); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -1486,6 +1508,257 @@ public virtual Navigation RemoveNavigation([NotNull] string name) public virtual IEnumerable GetNavigations() => _baseType?.GetNavigations().Concat(_navigations.Values) ?? _navigations.Values; + /// + /// 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 SkipNavigation AddSkipNavigation( + [NotNull] string name, + [CanBeNull] MemberInfo memberInfo, + [NotNull] EntityType targetEntityType, + [NotNull] ForeignKey foreignKey, + bool collection, + bool onPrincipal, + ConfigurationSource configurationSource) + { + Check.NotEmpty(name, nameof(name)); + Check.NotNull(targetEntityType, nameof(targetEntityType)); + Check.NotNull(foreignKey, nameof(foreignKey)); + + var duplicateProperty = FindMembersInHierarchy(name).FirstOrDefault(); + if (duplicateProperty != null) + { + throw new InvalidOperationException( + CoreStrings.ConflictingPropertyOrNavigation( + name, this.DisplayName(), duplicateProperty.DeclaringType.DisplayName())); + } + + if (memberInfo != null) + { + ValidateClrMember(name, memberInfo); + } + else + { + memberInfo = ClrType?.GetMembersInHierarchy(name).FirstOrDefault(); + } + + if (ClrType != null) + { + Navigation.IsCompatible( + name, + memberInfo, + this, + targetEntityType, + collection, + shouldThrow: true); + } + + var expectedEntityType = onPrincipal ? foreignKey.PrincipalEntityType : foreignKey.DeclaringEntityType; + if (expectedEntityType != this) + { + var message = onPrincipal + ? CoreStrings.SkipNavigationWrongPrincipalType( + name, this.DisplayName(), expectedEntityType.DisplayName(), foreignKey.Properties.Format()) + : CoreStrings.SkipNavigationWrongDependentType( + name, this.DisplayName(), expectedEntityType.DisplayName(), foreignKey.Properties.Format()); + throw new InvalidOperationException(message); + } + + var skipNavigation = new SkipNavigation( + name, + memberInfo as PropertyInfo, + memberInfo as FieldInfo, + targetEntityType, + foreignKey, + collection, + onPrincipal, + configurationSource); + + _skipNavigations.Add(name, skipNavigation); + + if (foreignKey.ReferencingSkipNavigations == null) + { + foreignKey.ReferencingSkipNavigations = new SortedSet(SkipNavigationComparer.Instance) { skipNavigation }; + } + else + { + foreignKey.ReferencingSkipNavigations.Add(skipNavigation); + } + + return (SkipNavigation)Model.ConventionDispatcher.OnSkipNavigationAdded(skipNavigation.Builder)?.Metadata; + } + + private Type ValidateClrMember(string name, MemberInfo memberInfo, bool throwOnNameMismatch = true) + { + if (ClrType == null) + { + throw new InvalidOperationException(CoreStrings.ClrPropertyOnShadowEntity(memberInfo.Name, this.DisplayName())); + } + + if (name != memberInfo.GetSimpleMemberName()) + { + if ((memberInfo as PropertyInfo)?.IsEFIndexerProperty() != true) + { + if (throwOnNameMismatch) + { + throw new InvalidOperationException( + CoreStrings.PropertyWrongName( + name, + this.DisplayName(), + memberInfo.GetSimpleMemberName())); + } + + return memberInfo.GetMemberType(); + } + else + { + var clashingMemberInfo = ClrType?.GetMembersInHierarchy(name).FirstOrDefault(); + if (clashingMemberInfo != null) + { + throw new InvalidOperationException( + CoreStrings.PropertyClashingNonIndexer( + name, + this.DisplayName())); + } + } + } + + return null; + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual SkipNavigation FindSkipNavigation([NotNull] string name) + { + Check.NotEmpty(name, nameof(name)); + + return FindDeclaredSkipNavigation(name) ?? _baseType?.FindSkipNavigation(name); + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// 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 SkipNavigation FindSkipNavigation([NotNull] MemberInfo memberInfo) + => FindSkipNavigation(Check.NotNull(memberInfo, nameof(memberInfo)).GetSimpleMemberName()); + + /// + /// 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 SkipNavigation FindDeclaredSkipNavigation([NotNull] string name) + => _skipNavigations.TryGetValue(Check.NotEmpty(name, nameof(name)), out var navigation) + ? navigation + : null; + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual IEnumerable GetDeclaredSkipNavigations() => _skipNavigations.Values; + + /// + /// 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 IEnumerable GetDerivedSkipNavigations() + => GetDerivedTypes().SelectMany(et => et.GetDeclaredSkipNavigations()); + + /// + /// 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 IEnumerable GetDerivedSkipNavigationsInclusive() + => GetDerivedTypesInclusive().SelectMany(et => et.GetDeclaredSkipNavigations()); + + /// + /// 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 IEnumerable FindDerivedSkipNavigations([NotNull] string name) + { + Check.NotNull(name, nameof(name)); + + return GetDerivedTypes().Select(et => et.FindDeclaredSkipNavigation(name)).Where(n => n != null); + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual IEnumerable FindSkipNavigationsInHierarchy([NotNull] string name) + => ToEnumerable(FindSkipNavigation(name)).Concat(FindDerivedSkipNavigations(name)); + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// 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 SkipNavigation RemoveSkipNavigation([NotNull] string name) + { + Check.NotEmpty(name, nameof(name)); + + var navigation = FindDeclaredSkipNavigation(name); + return navigation == null ? null : RemoveSkipNavigation(navigation); + } + + /// + /// 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 SkipNavigation RemoveSkipNavigation([NotNull] SkipNavigation navigation) + { + Check.NotNull(navigation, nameof(navigation)); + Check.DebugAssert(Builder != null, "Builder is null"); + + if (navigation.DeclaringType != this) + { + throw new InvalidOperationException(CoreStrings.SkipNavigationWrongType( + navigation.Name, this.DisplayName(), navigation.DeclaringType.DisplayName())); + } + + var removed = _skipNavigations.Remove(navigation.Name); + Check.DebugAssert(removed, "Expected the navigation to be removed"); + + navigation.ForeignKey.ReferencingSkipNavigations.Remove(navigation); + + navigation.Builder = null; + + return (SkipNavigation)Model.ConventionDispatcher.OnSkipNavigationRemoved(Builder, navigation); + } + + /// + /// 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 IEnumerable GetSkipNavigations() + => _baseType?.GetSkipNavigations().Concat(_skipNavigations.Values) ?? _skipNavigations.Values; + #endregion #region Indexes @@ -1652,10 +1925,13 @@ public virtual Index RemoveIndex([NotNull] IReadOnlyList properties) /// public virtual Index RemoveIndex([NotNull] Index index) { + Check.NotNull(index, nameof(index)); + Check.DebugAssert(Builder != null, "Builder is null"); + if (!_indexes.Remove(index.Properties)) { throw new InvalidOperationException( - CoreStrings.IndexWrongType(index.Properties.Format(), index.DeclaringEntityType.DisplayName(), this.DisplayName())); + CoreStrings.IndexWrongType(index.Properties.Format(), this.DisplayName(), index.DeclaringEntityType.DisplayName())); } index.Builder = null; @@ -1672,9 +1948,7 @@ public virtual Index RemoveIndex([NotNull] Index index) } } - Model.ConventionDispatcher.OnIndexRemoved(Builder, index); - - return index; + return (Index)Model.ConventionDispatcher.OnIndexRemoved(Builder, index); } /// @@ -1775,45 +2049,15 @@ public virtual Property AddProperty( if (memberInfo != null) { - if (ClrType == null) - { - throw new InvalidOperationException(CoreStrings.ClrPropertyOnShadowEntity(memberInfo.Name, this.DisplayName())); - } + propertyType = ValidateClrMember(name, memberInfo, typeConfigurationSource != null) + ?? propertyType; if (memberInfo.DeclaringType?.IsAssignableFrom(ClrType) != true) { - throw new ArgumentException( + throw new InvalidOperationException( CoreStrings.PropertyWrongEntityClrType( memberInfo.Name, this.DisplayName(), memberInfo.DeclaringType?.ShortDisplayName())); } - - if (name != memberInfo.GetSimpleMemberName()) - { - if ((memberInfo as PropertyInfo)?.IsEFIndexerProperty() != true) - { - if (typeConfigurationSource != null) - { - throw new InvalidOperationException( - CoreStrings.PropertyWrongName( - name, - this.DisplayName(), - memberInfo.GetSimpleMemberName())); - } - - propertyType = memberInfo.GetMemberType(); - } - else - { - var clashingMemberInfo = ClrType?.GetMembersInHierarchy(name).FirstOrDefault(); - if (clashingMemberInfo != null) - { - throw new InvalidOperationException( - CoreStrings.PropertyClashingNonIndexer( - name, - this.DisplayName())); - } - } - } } else { @@ -1952,22 +2196,26 @@ public virtual Property RemoveProperty([NotNull] string name) /// public virtual Property RemoveProperty([NotNull] Property property) { + Check.NotNull(property, nameof(property)); + Check.DebugAssert(Builder != null, "Builder is null"); + if (property.DeclaringEntityType != this) { throw new InvalidOperationException( CoreStrings.PropertyWrongType( property.Name, - property.DeclaringEntityType.DisplayName(), - this.DisplayName())); + this.DisplayName(), + property.DeclaringEntityType.DisplayName())); } CheckPropertyNotInUse(property); var removed = _properties.Remove(property.Name); Check.DebugAssert(removed, "removed is false"); + property.Builder = null; - return property; + return (Property)Model.ConventionDispatcher.OnPropertyRemoved(Builder, property); } private void CheckPropertyNotInUse(Property property) @@ -2109,6 +2357,8 @@ public virtual ServiceProperty AddServiceProperty( duplicateMember.DeclaringType.DisplayName())); } + ValidateClrMember(name, memberInfo, false); + var serviceProperty = new ServiceProperty( name, memberInfo as PropertyInfo, @@ -2209,9 +2459,29 @@ public virtual ServiceProperty RemoveServiceProperty([NotNull] string name) : RemoveServiceProperty(property); } - private ServiceProperty RemoveServiceProperty(ServiceProperty property) + /// + /// 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 ServiceProperty RemoveServiceProperty([NotNull] ServiceProperty property) { - _serviceProperties.Remove(property.Name); + Check.NotNull(property, nameof(property)); + Check.DebugAssert(Builder != null, "Builder is null"); + + if (property.DeclaringEntityType != this) + { + throw new InvalidOperationException( + CoreStrings.PropertyWrongType( + property.Name, + this.DisplayName(), + property.DeclaringEntityType.DisplayName())); + } + + var removed = _serviceProperties.Remove(property.Name); + Check.DebugAssert(removed, "removed is false"); + property.Builder = null; return property; @@ -2526,9 +2796,10 @@ public virtual void CheckDiscriminatorValue([NotNull] IEntityType entityType, [C /// 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. /// - IModel ITypeBase.Model + IConventionEntityTypeBuilder IConventionEntityType.Builder { - [DebuggerStepThrough] get => Model; + [DebuggerStepThrough] + get => Builder; } /// @@ -2537,7 +2808,7 @@ IModel ITypeBase.Model /// 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. /// - IMutableModel IMutableTypeBase.Model + IModel ITypeBase.Model { [DebuggerStepThrough] get => Model; } @@ -2548,7 +2819,7 @@ IMutableModel IMutableTypeBase.Model /// 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. /// - IMutableModel IMutableEntityType.Model + IMutableModel IMutableTypeBase.Model { [DebuggerStepThrough] get => Model; } @@ -2559,9 +2830,32 @@ IMutableModel IMutableEntityType.Model /// 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. /// - IEntityType IEntityType.BaseType + IMutableModel IMutableEntityType.Model { - [DebuggerStepThrough] get => _baseType; + [DebuggerStepThrough] get => Model; + } + + /// + /// 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. + /// + IConventionModel IConventionEntityType.Model + { + [DebuggerStepThrough] + get => Model; + } + + /// + /// 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. + /// + IEntityType IEntityType.BaseType + { + [DebuggerStepThrough] get => _baseType; } /// @@ -2576,6 +2870,18 @@ IMutableEntityType IMutableEntityType.BaseType set => HasBaseType((EntityType)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. + /// + IConventionEntityType IConventionEntityType.BaseType + { + [DebuggerStepThrough] + get => BaseType; + } + /// /// 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 @@ -2615,9 +2921,42 @@ IConventionEntityType IConventionEntityType.DefiningEntityType /// 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. /// + void IConventionEntityType.HasBaseType(IConventionEntityType entityType, bool fromDataAnnotation) + => HasBaseType( + (EntityType)entityType, 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] + void IConventionEntityType.HasNoKey(bool? keyless, bool fromDataAnnotation) + => HasNoKey(keyless, 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] IMutableKey IMutableEntityType.SetPrimaryKey(IReadOnlyList properties) => SetPrimaryKey(properties?.Cast().ToList(), 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. + /// + [DebuggerStepThrough] + IConventionKey IConventionEntityType.SetPrimaryKey(IReadOnlyList properties, bool fromDataAnnotation) + => SetPrimaryKey( + properties?.Cast().ToList(), + 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 @@ -2642,9 +2981,31 @@ IMutableKey IMutableEntityType.SetPrimaryKey(IReadOnlyList pro /// 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] + IConventionKey IConventionEntityType.FindPrimaryKey() => FindPrimaryKey(); + + /// + /// 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] IMutableKey IMutableEntityType.AddKey(IReadOnlyList properties) => AddKey(properties.Cast().ToList(), 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. + /// + [DebuggerStepThrough] + IConventionKey IConventionEntityType.AddKey(IReadOnlyList properties, bool fromDataAnnotation) + => AddKey( + properties.Cast().ToList(), + 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 @@ -2669,6 +3030,16 @@ IMutableKey IMutableEntityType.AddKey(IReadOnlyList properties /// 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] + IConventionKey IConventionEntityType.FindKey(IReadOnlyList properties) => FindKey(properties); + + /// + /// 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] IEnumerable IEntityType.GetKeys() => GetKeys(); /// @@ -2677,6 +3048,7 @@ IMutableKey IMutableEntityType.AddKey(IReadOnlyList properties /// 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] IEnumerable IMutableEntityType.GetKeys() => GetKeys(); /// @@ -2685,6 +3057,16 @@ IMutableKey IMutableEntityType.AddKey(IReadOnlyList properties /// 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] + IEnumerable IConventionEntityType.GetKeys() => GetKeys(); + + /// + /// 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] void IMutableEntityType.RemoveKey(IMutableKey key) => RemoveKey((Key)key); /// @@ -2693,6 +3075,16 @@ IMutableKey IMutableEntityType.AddKey(IReadOnlyList properties /// 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] + void IConventionEntityType.RemoveKey(IConventionKey key) => RemoveKey((Key)key); + + /// + /// 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] IMutableForeignKey IMutableEntityType.AddForeignKey( IReadOnlyList properties, IMutableKey principalKey, IMutableEntityType principalEntityType) => AddForeignKey( @@ -2702,6 +3094,39 @@ IMutableForeignKey IMutableEntityType.AddForeignKey( ConfigurationSource.Explicit, 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. + /// + [DebuggerStepThrough] + IConventionForeignKey IConventionEntityType.AddForeignKey( + IReadOnlyList properties, + IConventionKey principalKey, + IConventionEntityType principalEntityType, + bool setComponentConfigurationSource, + bool fromDataAnnotation) + => AddForeignKey( + properties.Cast().ToList(), + (Key)principalKey, + (EntityType)principalEntityType, + setComponentConfigurationSource + ? fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention + : (ConfigurationSource?)null, + 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] + IForeignKey IEntityType.FindForeignKey( + IReadOnlyList properties, IKey principalKey, IEntityType principalEntityType) + => FindForeignKey(properties, principalKey, principalEntityType); + /// /// 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 @@ -2720,7 +3145,8 @@ IMutableForeignKey IMutableEntityType.FindForeignKey( /// doing so can result in application failures when updating to a new Entity Framework Core release. /// [DebuggerStepThrough] - IForeignKey IEntityType.FindForeignKey(IReadOnlyList properties, IKey principalKey, IEntityType principalEntityType) + IConventionForeignKey IConventionEntityType.FindForeignKey( + IReadOnlyList properties, IKey principalKey, IEntityType principalEntityType) => FindForeignKey(properties, principalKey, principalEntityType); /// @@ -2729,6 +3155,7 @@ IForeignKey IEntityType.FindForeignKey(IReadOnlyList properties, IKey /// 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] IEnumerable IEntityType.GetForeignKeys() => GetForeignKeys(); /// @@ -2737,6 +3164,7 @@ IForeignKey IEntityType.FindForeignKey(IReadOnlyList properties, IKey /// 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] IEnumerable IMutableEntityType.GetForeignKeys() => GetForeignKeys(); /// @@ -2745,8 +3173,8 @@ IForeignKey IEntityType.FindForeignKey(IReadOnlyList properties, IKey /// 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. /// - void IMutableEntityType.RemoveForeignKey(IMutableForeignKey foreignKey) - => RemoveForeignKey((ForeignKey)foreignKey); + [DebuggerStepThrough] + IEnumerable IConventionEntityType.GetForeignKeys() => GetForeignKeys(); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -2754,8 +3182,9 @@ void IMutableEntityType.RemoveForeignKey(IMutableForeignKey foreignKey) /// 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. /// - IMutableIndex IMutableEntityType.AddIndex(IReadOnlyList properties) - => AddIndex(properties.Cast().ToList(), ConfigurationSource.Explicit); + [DebuggerStepThrough] + void IMutableEntityType.RemoveForeignKey(IMutableForeignKey foreignKey) + => RemoveForeignKey((ForeignKey)foreignKey); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -2764,7 +3193,8 @@ IMutableIndex IMutableEntityType.AddIndex(IReadOnlyList proper /// doing so can result in application failures when updating to a new Entity Framework Core release. /// [DebuggerStepThrough] - IIndex IEntityType.FindIndex(IReadOnlyList properties) => FindIndex(properties); + void IConventionEntityType.RemoveForeignKey(IConventionForeignKey foreignKey) + => RemoveForeignKey((ForeignKey)foreignKey); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -2773,7 +3203,15 @@ IMutableIndex IMutableEntityType.AddIndex(IReadOnlyList proper /// doing so can result in application failures when updating to a new Entity Framework Core release. /// [DebuggerStepThrough] - IMutableIndex IMutableEntityType.FindIndex(IReadOnlyList properties) => FindIndex(properties); + IMutableSkipNavigation IMutableEntityType.AddSkipNavigation( + [NotNull] string name, + [CanBeNull] MemberInfo memberInfo, + [NotNull] IMutableEntityType targetEntityType, + [NotNull] IMutableForeignKey foreignKey, + bool collection, + bool onPrincipal) + => AddSkipNavigation(name, memberInfo, (EntityType)targetEntityType, (ForeignKey)foreignKey, collection, onPrincipal, + ConfigurationSource.Explicit); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -2781,7 +3219,17 @@ IMutableIndex IMutableEntityType.AddIndex(IReadOnlyList proper /// 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. /// - IEnumerable IEntityType.GetIndexes() => GetIndexes(); + [DebuggerStepThrough] + IConventionSkipNavigation IConventionEntityType.AddSkipNavigation( + [NotNull] string name, + [CanBeNull] MemberInfo memberInfo, + [NotNull] IConventionEntityType targetEntityType, + [NotNull] IConventionForeignKey foreignKey, + bool collection, + bool onPrincipal, + bool fromDataAnnotation) + => AddSkipNavigation(name, memberInfo, (EntityType)targetEntityType, (ForeignKey)foreignKey, collection, onPrincipal, + fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -2789,7 +3237,9 @@ IMutableIndex IMutableEntityType.AddIndex(IReadOnlyList proper /// 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. /// - IEnumerable IMutableEntityType.GetIndexes() => GetIndexes(); + [DebuggerStepThrough] + ISkipNavigation IEntityType.FindSkipNavigation(MemberInfo memberInfo) + => FindSkipNavigation(memberInfo); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -2797,7 +3247,9 @@ IMutableIndex IMutableEntityType.AddIndex(IReadOnlyList proper /// 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. /// - void IMutableEntityType.RemoveIndex(IMutableIndex index) => RemoveIndex((Index)index); + [DebuggerStepThrough] + ISkipNavigation IEntityType.FindSkipNavigation(string name) + => FindSkipNavigation(name); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -2805,8 +3257,9 @@ IMutableIndex IMutableEntityType.AddIndex(IReadOnlyList proper /// 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. /// - IMutableProperty IMutableEntityType.AddProperty(string name, Type propertyType, MemberInfo memberInfo) - => AddProperty(name, propertyType, memberInfo, ConfigurationSource.Explicit, ConfigurationSource.Explicit); + [DebuggerStepThrough] + IMutableSkipNavigation IMutableEntityType.FindSkipNavigation(string name) + => FindSkipNavigation(name); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -2815,7 +3268,8 @@ IMutableProperty IMutableEntityType.AddProperty(string name, Type propertyType, /// doing so can result in application failures when updating to a new Entity Framework Core release. /// [DebuggerStepThrough] - IProperty IEntityType.FindProperty(string name) => FindProperty(name); + IConventionSkipNavigation IConventionEntityType.FindSkipNavigation(string name) + => FindSkipNavigation(name); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -2824,7 +3278,8 @@ IMutableProperty IMutableEntityType.AddProperty(string name, Type propertyType, /// doing so can result in application failures when updating to a new Entity Framework Core release. /// [DebuggerStepThrough] - IMutableProperty IMutableEntityType.FindProperty(string name) => FindProperty(name); + ISkipNavigation IEntityType.FindDeclaredSkipNavigation(string name) + => FindDeclaredSkipNavigation(name); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -2833,7 +3288,8 @@ IMutableProperty IMutableEntityType.AddProperty(string name, Type propertyType, /// doing so can result in application failures when updating to a new Entity Framework Core release. /// [DebuggerStepThrough] - IEnumerable IEntityType.GetProperties() => GetProperties(); + IEnumerable IEntityType.GetDeclaredSkipNavigations() + => GetDeclaredSkipNavigations(); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -2842,7 +3298,8 @@ IMutableProperty IMutableEntityType.AddProperty(string name, Type propertyType, /// doing so can result in application failures when updating to a new Entity Framework Core release. /// [DebuggerStepThrough] - IEnumerable IMutableEntityType.GetProperties() => GetProperties(); + IEnumerable IEntityType.GetSkipNavigations() + => GetSkipNavigations(); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -2850,7 +3307,9 @@ IMutableProperty IMutableEntityType.AddProperty(string name, Type propertyType, /// 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. /// - void IMutableEntityType.RemoveProperty(IMutableProperty property) => RemoveProperty((Property)property); + [DebuggerStepThrough] + IEnumerable IMutableEntityType.GetSkipNavigations() + => GetSkipNavigations(); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -2858,8 +3317,9 @@ IMutableProperty IMutableEntityType.AddProperty(string name, Type propertyType, /// 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. /// - IMutableServiceProperty IMutableEntityType.AddServiceProperty(MemberInfo memberInfo) - => AddServiceProperty(memberInfo, ConfigurationSource.Explicit); + [DebuggerStepThrough] + IEnumerable IConventionEntityType.GetSkipNavigations() + => GetSkipNavigations(); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -2868,7 +3328,8 @@ IMutableServiceProperty IMutableEntityType.AddServiceProperty(MemberInfo memberI /// doing so can result in application failures when updating to a new Entity Framework Core release. /// [DebuggerStepThrough] - IServiceProperty IEntityType.FindServiceProperty(string name) => FindServiceProperty(name); + void IMutableEntityType.RemoveSkipNavigation([NotNull] IMutableSkipNavigation navigation) + => RemoveSkipNavigation((SkipNavigation)navigation); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -2877,7 +3338,8 @@ IMutableServiceProperty IMutableEntityType.AddServiceProperty(MemberInfo memberI /// doing so can result in application failures when updating to a new Entity Framework Core release. /// [DebuggerStepThrough] - IMutableServiceProperty IMutableEntityType.FindServiceProperty(string name) => FindServiceProperty(name); + void IConventionEntityType.RemoveSkipNavigation([NotNull] IConventionSkipNavigation navigation) + => RemoveSkipNavigation((SkipNavigation)navigation); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -2885,7 +3347,9 @@ IMutableServiceProperty IMutableEntityType.AddServiceProperty(MemberInfo memberI /// 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. /// - IEnumerable IEntityType.GetServiceProperties() => GetServiceProperties(); + [DebuggerStepThrough] + IMutableIndex IMutableEntityType.AddIndex(IReadOnlyList properties) + => AddIndex(properties.Cast().ToList(), ConfigurationSource.Explicit); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -2893,7 +3357,11 @@ IMutableServiceProperty IMutableEntityType.AddServiceProperty(MemberInfo memberI /// 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. /// - IEnumerable IMutableEntityType.GetServiceProperties() => GetServiceProperties(); + [DebuggerStepThrough] + IConventionIndex IConventionEntityType.AddIndex(IReadOnlyList properties, bool fromDataAnnotation) + => AddIndex( + properties.Cast().ToList(), + fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -2901,7 +3369,8 @@ IMutableServiceProperty IMutableEntityType.AddServiceProperty(MemberInfo memberI /// 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. /// - IMutableServiceProperty IMutableEntityType.RemoveServiceProperty(string name) => RemoveServiceProperty(name); + [DebuggerStepThrough] + IIndex IEntityType.FindIndex(IReadOnlyList properties) => FindIndex(properties); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -2909,10 +3378,8 @@ IMutableServiceProperty IMutableEntityType.AddServiceProperty(MemberInfo memberI /// 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. /// - IConventionEntityTypeBuilder IConventionEntityType.Builder - { - [DebuggerStepThrough] get => Builder; - } + [DebuggerStepThrough] + IMutableIndex IMutableEntityType.FindIndex(IReadOnlyList properties) => FindIndex(properties); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -2920,10 +3387,8 @@ IConventionEntityTypeBuilder IConventionEntityType.Builder /// 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. /// - IConventionModel IConventionEntityType.Model - { - [DebuggerStepThrough] get => Model; - } + [DebuggerStepThrough] + IConventionIndex IConventionEntityType.FindIndex(IReadOnlyList properties) => FindIndex(properties); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -2931,10 +3396,8 @@ IConventionModel IConventionEntityType.Model /// 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. /// - IConventionEntityType IConventionEntityType.BaseType - { - [DebuggerStepThrough] get => BaseType; - } + [DebuggerStepThrough] + IEnumerable IEntityType.GetIndexes() => GetIndexes(); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -2942,9 +3405,8 @@ IConventionEntityType IConventionEntityType.BaseType /// 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. /// - void IConventionEntityType.HasBaseType(IConventionEntityType entityType, bool fromDataAnnotation) - => HasBaseType( - (EntityType)entityType, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + [DebuggerStepThrough] + IEnumerable IMutableEntityType.GetIndexes() => GetIndexes(); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -2952,8 +3414,8 @@ void IConventionEntityType.HasBaseType(IConventionEntityType entityType, bool fr /// 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. /// - void IConventionEntityType.HasNoKey(bool? keyless, bool fromDataAnnotation) - => HasNoKey(keyless, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + [DebuggerStepThrough] + IEnumerable IConventionEntityType.GetIndexes() => GetIndexes(); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -2961,10 +3423,8 @@ void IConventionEntityType.HasNoKey(bool? keyless, bool fromDataAnnotation) /// 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. /// - IConventionKey IConventionEntityType.SetPrimaryKey(IReadOnlyList properties, bool fromDataAnnotation) - => SetPrimaryKey( - properties?.Cast().ToList(), - fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + [DebuggerStepThrough] + void IMutableEntityType.RemoveIndex(IMutableIndex index) => RemoveIndex((Index)index); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -2972,7 +3432,8 @@ IConventionKey IConventionEntityType.SetPrimaryKey(IReadOnlyList - IConventionKey IConventionEntityType.FindPrimaryKey() => FindPrimaryKey(); + [DebuggerStepThrough] + void IConventionEntityType.RemoveIndex(IConventionIndex index) => RemoveIndex((Index)index); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -2980,7 +3441,9 @@ IConventionKey IConventionEntityType.SetPrimaryKey(IReadOnlyList - IConventionKey IConventionEntityType.FindKey(IReadOnlyList properties) => FindKey(properties); + [DebuggerStepThrough] + IMutableProperty IMutableEntityType.AddProperty(string name, Type propertyType, MemberInfo memberInfo) + => AddProperty(name, propertyType, memberInfo, ConfigurationSource.Explicit, ConfigurationSource.Explicit); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -2988,7 +3451,17 @@ IConventionKey IConventionEntityType.SetPrimaryKey(IReadOnlyList - IEnumerable IConventionEntityType.GetKeys() => GetKeys(); + [DebuggerStepThrough] + IConventionProperty IConventionEntityType.AddProperty( + string name, Type propertyType, MemberInfo memberInfo, bool setTypeConfigurationSource, bool fromDataAnnotation) + => AddProperty( + name, + propertyType, + memberInfo, + setTypeConfigurationSource + ? fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention + : (ConfigurationSource?)null, + fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -2996,9 +3469,8 @@ IConventionKey IConventionEntityType.SetPrimaryKey(IReadOnlyList - IConventionForeignKey IConventionEntityType.FindForeignKey( - IReadOnlyList properties, IKey principalKey, IEntityType principalEntityType) - => FindForeignKey(properties, principalKey, principalEntityType); + [DebuggerStepThrough] + IProperty IEntityType.FindProperty(string name) => FindProperty(name); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -3006,7 +3478,8 @@ IConventionForeignKey IConventionEntityType.FindForeignKey( /// 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. /// - IEnumerable IConventionEntityType.GetForeignKeys() => GetForeignKeys(); + [DebuggerStepThrough] + IMutableProperty IMutableEntityType.FindProperty(string name) => FindProperty(name); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -3014,7 +3487,8 @@ IConventionForeignKey IConventionEntityType.FindForeignKey( /// 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. /// - IConventionIndex IConventionEntityType.FindIndex(IReadOnlyList properties) => FindIndex(properties); + [DebuggerStepThrough] + IConventionProperty IConventionEntityType.FindProperty(string name) => FindProperty(name); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -3022,7 +3496,8 @@ IConventionForeignKey IConventionEntityType.FindForeignKey( /// 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. /// - IEnumerable IConventionEntityType.GetIndexes() => GetIndexes(); + [DebuggerStepThrough] + IEnumerable IEntityType.GetProperties() => GetProperties(); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -3030,7 +3505,8 @@ IConventionForeignKey IConventionEntityType.FindForeignKey( /// 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. /// - IConventionProperty IConventionEntityType.FindProperty(string name) => FindProperty(name); + [DebuggerStepThrough] + IEnumerable IMutableEntityType.GetProperties() => GetProperties(); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -3038,6 +3514,7 @@ IConventionForeignKey IConventionEntityType.FindForeignKey( /// 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] IEnumerable IConventionEntityType.GetProperties() => GetProperties(); /// @@ -3046,7 +3523,8 @@ IConventionForeignKey IConventionEntityType.FindForeignKey( /// 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. /// - IConventionServiceProperty IConventionEntityType.FindServiceProperty(string name) => FindServiceProperty(name); + [DebuggerStepThrough] + void IMutableEntityType.RemoveProperty(IMutableProperty property) => RemoveProperty((Property)property); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -3054,7 +3532,8 @@ IConventionForeignKey IConventionEntityType.FindForeignKey( /// 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. /// - IEnumerable IConventionEntityType.GetServiceProperties() => GetServiceProperties(); + [DebuggerStepThrough] + void IConventionEntityType.RemoveProperty(IConventionProperty property) => RemoveProperty((Property)property); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -3062,7 +3541,9 @@ IConventionForeignKey IConventionEntityType.FindForeignKey( /// 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. /// - IConventionServiceProperty IConventionEntityType.RemoveServiceProperty(string name) => RemoveServiceProperty(name); + [DebuggerStepThrough] + IMutableServiceProperty IMutableEntityType.AddServiceProperty(MemberInfo memberInfo) + => AddServiceProperty(memberInfo, ConfigurationSource.Explicit); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -3070,7 +3551,9 @@ IConventionForeignKey IConventionEntityType.FindForeignKey( /// 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. /// - void IConventionEntityType.RemoveProperty(IConventionProperty property) => RemoveProperty((Property)property); + [DebuggerStepThrough] + IConventionServiceProperty IConventionEntityType.AddServiceProperty(MemberInfo memberInfo, bool fromDataAnnotation) + => AddServiceProperty(memberInfo, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -3078,8 +3561,8 @@ IConventionForeignKey IConventionEntityType.FindForeignKey( /// 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. /// - IConventionServiceProperty IConventionEntityType.AddServiceProperty(MemberInfo memberInfo, bool fromDataAnnotation) - => AddServiceProperty(memberInfo, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + [DebuggerStepThrough] + IServiceProperty IEntityType.FindServiceProperty(string name) => FindServiceProperty(name); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -3087,7 +3570,8 @@ IConventionServiceProperty IConventionEntityType.AddServiceProperty(MemberInfo m /// 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. /// - void IConventionEntityType.RemoveIndex(IConventionIndex index) => RemoveIndex((Index)index); + [DebuggerStepThrough] + IMutableServiceProperty IMutableEntityType.FindServiceProperty(string name) => FindServiceProperty(name); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -3095,16 +3579,8 @@ IConventionServiceProperty IConventionEntityType.AddServiceProperty(MemberInfo m /// 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. /// - IConventionProperty IConventionEntityType.AddProperty( - string name, Type propertyType, MemberInfo memberInfo, bool setTypeConfigurationSource, bool fromDataAnnotation) - => AddProperty( - name, - propertyType, - memberInfo, - setTypeConfigurationSource - ? fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention - : (ConfigurationSource?)null, - fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + [DebuggerStepThrough] + IConventionServiceProperty IConventionEntityType.FindServiceProperty(string name) => FindServiceProperty(name); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -3112,8 +3588,8 @@ IConventionProperty IConventionEntityType.AddProperty( /// 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. /// - void IConventionEntityType.RemoveForeignKey(IConventionForeignKey foreignKey) - => RemoveForeignKey((ForeignKey)foreignKey); + [DebuggerStepThrough] + IEnumerable IEntityType.GetServiceProperties() => GetServiceProperties(); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -3121,10 +3597,8 @@ void IConventionEntityType.RemoveForeignKey(IConventionForeignKey foreignKey) /// 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. /// - IConventionIndex IConventionEntityType.AddIndex(IReadOnlyList properties, bool fromDataAnnotation) - => AddIndex( - properties.Cast().ToList(), - fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + [DebuggerStepThrough] + IEnumerable IMutableEntityType.GetServiceProperties() => GetServiceProperties(); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -3132,7 +3606,8 @@ IConventionIndex IConventionEntityType.AddIndex(IReadOnlyList - void IConventionEntityType.RemoveKey(IConventionKey key) => RemoveKey((Key)key); + [DebuggerStepThrough] + IEnumerable IConventionEntityType.GetServiceProperties() => GetServiceProperties(); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -3140,20 +3615,8 @@ IConventionIndex IConventionEntityType.AddIndex(IReadOnlyList - IConventionForeignKey IConventionEntityType.AddForeignKey( - IReadOnlyList properties, - IConventionKey principalKey, - IConventionEntityType principalEntityType, - bool setComponentConfigurationSource, - bool fromDataAnnotation) - => AddForeignKey( - properties.Cast().ToList(), - (Key)principalKey, - (EntityType)principalEntityType, - setComponentConfigurationSource - ? fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention - : (ConfigurationSource?)null, - fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + [DebuggerStepThrough] + IMutableServiceProperty IMutableEntityType.RemoveServiceProperty(string name) => RemoveServiceProperty(name); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -3161,10 +3624,8 @@ IConventionForeignKey IConventionEntityType.AddForeignKey( /// 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. /// - IConventionKey IConventionEntityType.AddKey(IReadOnlyList properties, bool fromDataAnnotation) - => AddKey( - properties.Cast().ToList(), - fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + [DebuggerStepThrough] + IConventionServiceProperty IConventionEntityType.RemoveServiceProperty(string name) => RemoveServiceProperty(name); #endregion @@ -3174,64 +3635,6 @@ private static IEnumerable ToEnumerable(T element) ? Enumerable.Empty() : new[] { element }; - private sealed class PropertyComparer : IComparer - { - private readonly EntityType _entityType; - - public PropertyComparer(EntityType entityType) - { - _entityType = entityType; - } - - public int Compare(string x, string y) - { - var xIndex = -1; - var yIndex = -1; - - var properties = _entityType.FindPrimaryKey()?.Properties; - - if (properties != null) - { - for (var i = 0; i < properties.Count; i++) - { - var name = properties[i].Name; - - if (name == x) - { - xIndex = i; - } - - if (name == y) - { - yIndex = i; - } - } - } - - // Neither property is part of the Primary Key - // Compare the property names - if (xIndex == -1 - && yIndex == -1) - { - return StringComparer.Ordinal.Compare(x, y); - } - - // Both properties are part of the Primary Key - // Compare the indices - if (xIndex > -1 - && yIndex > -1) - { - return xIndex - yIndex; - } - - // One property is part of the Primary Key - // The primary key property is first - return xIndex > yIndex - ? -1 - : 1; - } - } - /// /// 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/Internal/ForeignKey.cs b/src/EFCore/Metadata/Internal/ForeignKey.cs index 504227bb798..7559aaac65a 100644 --- a/src/EFCore/Metadata/Internal/ForeignKey.cs +++ b/src/EFCore/Metadata/Internal/ForeignKey.cs @@ -83,7 +83,7 @@ public ForeignKey( /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - public virtual IReadOnlyList Properties { [DebuggerStepThrough] get; private set; } + public virtual IReadOnlyList Properties { get; private set; } /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -91,7 +91,7 @@ public ForeignKey( /// 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 Key PrincipalKey { [DebuggerStepThrough] get; private set; } + public virtual Key PrincipalKey { get; private set; } /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -99,7 +99,7 @@ public ForeignKey( /// 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 EntityType DeclaringEntityType { [DebuggerStepThrough] get; } + public virtual EntityType DeclaringEntityType { get; } /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -107,7 +107,7 @@ public ForeignKey( /// 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 EntityType PrincipalEntityType { [DebuggerStepThrough] get; } + public virtual EntityType PrincipalEntityType { get; } /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -117,12 +117,29 @@ public ForeignKey( /// public virtual InternalRelationshipBuilder Builder { - [DebuggerStepThrough] get; - [DebuggerStepThrough] + get; + [param: CanBeNull] set; } + /// + /// 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 SortedSet ReferencingSkipNavigations { get; [param: CanBeNull] set; } + + /// + /// 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 IEnumerable GetReferencingSkipNavigations() + => ReferencingSkipNavigations ?? Enumerable.Empty(); + /// /// 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 @@ -979,6 +996,14 @@ IConventionNavigation IConventionForeignKey.PrincipalToDependent [DebuggerStepThrough] get => PrincipalToDependent; } + /// + /// 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. + /// + IConventionRelationshipBuilder IConventionForeignKey.Builder => Builder; + /// /// 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 @@ -1065,6 +1090,17 @@ void IConventionForeignKey.SetIsOwnership(bool? ownership, bool fromDataAnnotati void IConventionForeignKey.SetDeleteBehavior(DeleteBehavior? deleteBehavior, bool fromDataAnnotation) => SetDeleteBehavior(deleteBehavior, 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. + /// + public virtual DebugView DebugView + => new DebugView( + () => this.ToDebugString(MetadataDebugStringOptions.ShortDefault), + () => this.ToDebugString(MetadataDebugStringOptions.LongDefault)); + /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to /// the same compatibility standards as public APIs. It may be changed or removed without notice in @@ -1253,24 +1289,5 @@ private static bool ArePropertyTypesCompatible( IReadOnlyList principalProperties, IReadOnlyList dependentProperties) => principalProperties.Select(p => p.ClrType.UnwrapNullableType()).SequenceEqual( dependentProperties.Select(p => p.ClrType.UnwrapNullableType())); - - /// - /// 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 DebugView DebugView - => new DebugView( - () => this.ToDebugString(MetadataDebugStringOptions.ShortDefault), - () => this.ToDebugString(MetadataDebugStringOptions.LongDefault)); - - /// - /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to - /// the same compatibility standards as public APIs. It may be changed or removed without notice in - /// any release. You should only use it directly in your code with extreme caution and knowing that - /// doing so can result in application failures when updating to a new Entity Framework Core release. - /// - IConventionRelationshipBuilder IConventionForeignKey.Builder => Builder; } } diff --git a/src/EFCore/Metadata/Internal/InternalEntityTypeBuilder.cs b/src/EFCore/Metadata/Internal/InternalEntityTypeBuilder.cs index fa706cad370..6aabdf9f7ba 100644 --- a/src/EFCore/Metadata/Internal/InternalEntityTypeBuilder.cs +++ b/src/EFCore/Metadata/Internal/InternalEntityTypeBuilder.cs @@ -119,7 +119,7 @@ public virtual InternalKeyBuilder PrimaryKey( } } - // TODO: Use convention batch to get the updated builder, see #214 + // TODO: Use convention batch to get the updated builder, see #15898 if (keyBuilder?.Metadata.Builder == null) { properties = GetActualProperties(properties, null); @@ -290,7 +290,7 @@ public virtual InternalEntityTypeBuilder HasNoKey([NotNull] Key key, Configurati && property.ClrType.IsNullableType() && !property.GetContainingForeignKeys().Any(fk => fk.IsRequired)) { - // TODO: This should be handled by reference tracking, see #214 + // TODO: This should be handled by reference tracking, see #15898 property.Builder?.IsRequired(null, configurationSource); } } diff --git a/src/EFCore/Metadata/Internal/InternalSkipNavigationBuilder.cs b/src/EFCore/Metadata/Internal/InternalSkipNavigationBuilder.cs new file mode 100644 index 00000000000..b4eaf47054a --- /dev/null +++ b/src/EFCore/Metadata/Internal/InternalSkipNavigationBuilder.cs @@ -0,0 +1,35 @@ +// 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.Diagnostics; +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Microsoft.EntityFrameworkCore.Metadata.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 class InternalSkipNavigationBuilder : InternalModelItemBuilder, IConventionSkipNavigationBuilder + { + /// + /// 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 InternalSkipNavigationBuilder([NotNull] SkipNavigation metadata, [NotNull] InternalModelBuilder modelBuilder) + : base(metadata, modelBuilder) + { + } + + IConventionSkipNavigation IConventionSkipNavigationBuilder.Metadata + { + [DebuggerStepThrough] + get => Metadata; + } + } +} diff --git a/src/EFCore/Metadata/Internal/Key.cs b/src/EFCore/Metadata/Internal/Key.cs index 777c6c8faf7..19f7d1e8ef5 100644 --- a/src/EFCore/Metadata/Internal/Key.cs +++ b/src/EFCore/Metadata/Internal/Key.cs @@ -148,7 +148,6 @@ public virtual IPrincipalKeyValueFactory GetPrincipalKeyValueFactory /// 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. /// - // Note this is ISet because there is no suitable readonly interface in the profiles we are using public virtual ISet ReferencingForeignKeys { get; [param: CanBeNull] set; } /// diff --git a/src/EFCore/Metadata/Internal/Navigation.cs b/src/EFCore/Metadata/Internal/Navigation.cs index dc2df28c5c2..1fe7a061292 100644 --- a/src/EFCore/Metadata/Internal/Navigation.cs +++ b/src/EFCore/Metadata/Internal/Navigation.cs @@ -65,13 +65,7 @@ public Navigation( /// 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 InternalNavigationBuilder Builder - { - [DebuggerStepThrough] get; - [DebuggerStepThrough] - [param: CanBeNull] - set; - } + public virtual InternalNavigationBuilder Builder { get; [param: CanBeNull] set; } /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -204,7 +198,9 @@ public static bool IsCompatible( /// [DebuggerStepThrough] public virtual Navigation FindInverse() - => (Navigation)((INavigation)this).FindInverse(); + => this.IsDependentToPrincipal() + ? ForeignKey.PrincipalToDependent + : ForeignKey.DependentToPrincipal; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -214,7 +210,9 @@ public virtual Navigation FindInverse() /// [DebuggerStepThrough] public virtual EntityType GetTargetType() - => (EntityType)((INavigation)this).GetTargetType(); + => this.IsDependentToPrincipal() + ? ForeignKey.PrincipalEntityType + : ForeignKey.DeclaringEntityType; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -224,10 +222,19 @@ public virtual EntityType GetTargetType() /// public virtual IClrCollectionAccessor CollectionAccessor => NonCapturingLazyInitializer.EnsureInitialized( - ref _collectionAccessor, this, n => - !n.IsCollection() || n.IsShadowProperty() - ? null - : new ClrCollectionAccessorFactory().Create(n)); + ref _collectionAccessor, this, n => new ClrCollectionAccessorFactory().Create(n)); + + /// + /// Runs the conventions when an annotation was set or removed. + /// + /// The key of the set annotation. + /// The annotation set. + /// The old annotation. + /// The annotation that was set. + protected override IConventionAnnotation OnAnnotationSet( + string name, IConventionAnnotation annotation, IConventionAnnotation oldAnnotation) + => DeclaringType.Model.ConventionDispatcher.OnNavigationAnnotationChanged( + ForeignKey.Builder, this, name, annotation, oldAnnotation); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -298,7 +305,10 @@ IMutableEntityType IMutableNavigation.DeclaringEntityType /// 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. /// - IConventionEntityType IConventionNavigation.DeclaringEntityType => DeclaringEntityType; + IConventionEntityType IConventionNavigation.DeclaringEntityType + { + [DebuggerStepThrough] get => DeclaringEntityType; + } /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -306,6 +316,9 @@ IMutableEntityType IMutableNavigation.DeclaringEntityType /// 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. /// - IConventionForeignKey IConventionNavigation.ForeignKey => ForeignKey; + IConventionForeignKey IConventionNavigation.ForeignKey + { + [DebuggerStepThrough] get => ForeignKey; + } } } diff --git a/src/EFCore/Metadata/Internal/Property.cs b/src/EFCore/Metadata/Internal/Property.cs index 9098201e9b4..b1eeeb581ec 100644 --- a/src/EFCore/Metadata/Internal/Property.cs +++ b/src/EFCore/Metadata/Internal/Property.cs @@ -69,7 +69,7 @@ public Property( /// 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 EntityType DeclaringEntityType { [DebuggerStepThrough] get; } + public virtual EntityType DeclaringEntityType { get; } /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -79,7 +79,8 @@ public Property( /// public override TypeBase DeclaringType { - [DebuggerStepThrough] get => DeclaringEntityType; + [DebuggerStepThrough] + get => DeclaringEntityType; } /// @@ -88,7 +89,7 @@ public override TypeBase DeclaringType /// 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 override Type ClrType { [DebuggerStepThrough] get; } + public override Type ClrType { get; } /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -96,13 +97,7 @@ public override TypeBase DeclaringType /// 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 InternalPropertyBuilder Builder - { - [DebuggerStepThrough] get; - [DebuggerStepThrough] - [param: CanBeNull] - set; - } + public virtual InternalPropertyBuilder Builder { get; [param: CanBeNull] set; } /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -126,7 +121,7 @@ public virtual bool UpdateConfigurationSource(ConfigurationSource configurationS } // Needed for a workaround before reference counting is implemented - // Issue #214 + // Issue #15898 /// /// 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 @@ -532,8 +527,15 @@ public virtual string CheckValueComparer([CanBeNull] ValueComparer comparer) /// 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 IEnumerable GetContainingForeignKeys() - => ForeignKeys ?? Enumerable.Empty(); + public virtual IKey PrimaryKey { get; [param: CanBeNull] set; } + + /// + /// 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 List Keys { get; [param: CanBeNull] set; } /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -544,6 +546,31 @@ public virtual IEnumerable GetContainingForeignKeys() public virtual IEnumerable GetContainingKeys() => Keys ?? Enumerable.Empty(); + /// + /// 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 List ForeignKeys { get; [param: CanBeNull] set; } + + /// + /// 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 IEnumerable GetContainingForeignKeys() + => ForeignKeys ?? Enumerable.Empty(); + + /// + /// 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 List Indexes { get; [param: CanBeNull] set; } + /// /// 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 @@ -598,38 +625,6 @@ public static bool AreCompatible([NotNull] IReadOnlyList properties, [ && entityType.GetRuntimeFields().ContainsKey(property.Name))))); } - /// - /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to - /// the same compatibility standards as public APIs. It may be changed or removed without notice in - /// 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 IKey PrimaryKey { get; [param: CanBeNull] set; } - - /// - /// 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 List Keys { get; [param: CanBeNull] set; } - - /// - /// 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 List ForeignKeys { get; [param: CanBeNull] set; } - - /// - /// 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 List Indexes { get; [param: CanBeNull] set; } - /// /// 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/Internal/PropertyBase.cs b/src/EFCore/Metadata/Internal/PropertyBase.cs index d27a4e21b5f..5e9a7e1e597 100644 --- a/src/EFCore/Metadata/Internal/PropertyBase.cs +++ b/src/EFCore/Metadata/Internal/PropertyBase.cs @@ -183,7 +183,7 @@ public virtual void SetPropertyAccessMode(PropertyAccessMode? propertyAccessMode /// public static bool IsCompatible( [NotNull] FieldInfo fieldInfo, - [NotNull] Type propertyType, + [CanBeNull] Type propertyType, [CanBeNull] Type entityClrType, [CanBeNull] string propertyName, bool shouldThrow) @@ -203,7 +203,8 @@ public static bool IsCompatible( } var fieldTypeInfo = fieldInfo.FieldType; - if (!fieldTypeInfo.IsAssignableFrom(propertyType) + if (propertyType != null + && !fieldTypeInfo.IsAssignableFrom(propertyType) && !propertyType.IsAssignableFrom(fieldTypeInfo)) { if (shouldThrow) diff --git a/src/EFCore/Metadata/Internal/PropertyNameComparer.cs b/src/EFCore/Metadata/Internal/PropertyNameComparer.cs new file mode 100644 index 00000000000..9c740341b2a --- /dev/null +++ b/src/EFCore/Metadata/Internal/PropertyNameComparer.cs @@ -0,0 +1,73 @@ +// 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.CodeAnalysis; + +namespace Microsoft.EntityFrameworkCore.Metadata.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 sealed class PropertyNameComparer : IComparer + { + private readonly EntityType _entityType; + + public PropertyNameComparer([NotNull] EntityType entityType) + { + _entityType = entityType; + } + + public int Compare(string x, string y) + { + var xIndex = -1; + var yIndex = -1; + + var properties = _entityType.FindPrimaryKey()?.Properties; + + if (properties != null) + { + for (var i = 0; i < properties.Count; i++) + { + var name = properties[i].Name; + + if (name == x) + { + xIndex = i; + } + + if (name == y) + { + yIndex = i; + } + } + } + + // Neither property is part of the Primary Key + // Compare the property names + if (xIndex == -1 + && yIndex == -1) + { + return StringComparer.Ordinal.Compare(x, y); + } + + // Both properties are part of the Primary Key + // Compare the indices + if (xIndex > -1 + && yIndex > -1) + { + return xIndex - yIndex; + } + + // One property is part of the Primary Key + // The primary key property is first + return xIndex > yIndex + ? -1 + : 1; + } + } +} diff --git a/src/EFCore/Metadata/Internal/SkipNavigation.cs b/src/EFCore/Metadata/Internal/SkipNavigation.cs new file mode 100644 index 00000000000..793914a7fa0 --- /dev/null +++ b/src/EFCore/Metadata/Internal/SkipNavigation.cs @@ -0,0 +1,317 @@ +// 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.Diagnostics; +using System.Reflection; +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Diagnostics; +using Microsoft.EntityFrameworkCore.Internal; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Microsoft.EntityFrameworkCore.Utilities; + +namespace Microsoft.EntityFrameworkCore.Metadata.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 class SkipNavigation : PropertyBase, IMutableSkipNavigation, IConventionSkipNavigation + { + private ConfigurationSource _configurationSource; + private ConfigurationSource? _inverseConfigurationSource; + + // Warning: Never access these fields directly as access needs to be thread-safe + private IClrCollectionAccessor _collectionAccessor; + + /// + /// 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 SkipNavigation( + [NotNull] string name, + [CanBeNull] PropertyInfo propertyInfo, + [CanBeNull] FieldInfo fieldInfo, + [NotNull] EntityType targetEntityType, + [NotNull] ForeignKey foreignKey, + bool collection, + bool onPrincipal, + ConfigurationSource configurationSource) + : base(name, propertyInfo, fieldInfo) + { + Check.NotNull(foreignKey, nameof(foreignKey)); + + TargetEntityType = targetEntityType; + ForeignKey = foreignKey; + IsCollection = collection; + IsOnPrincipal = onPrincipal; + _configurationSource = configurationSource; + Builder = new InternalSkipNavigationBuilder(this, targetEntityType.Model.Builder); + } + + /// + /// 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 override Type ClrType => this.GetIdentifyingMemberInfo()?.GetMemberType(); + + /// + /// 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 ForeignKey ForeignKey { get; } + + /// + /// 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 InternalSkipNavigationBuilder Builder { get; [param: CanBeNull] set; } + + /// + /// 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 override TypeBase DeclaringType => IsOnPrincipal ? ForeignKey.PrincipalEntityType : ForeignKey.DeclaringEntityType; + + /// + /// 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 EntityType AssociationEntityType => IsOnPrincipal ? ForeignKey.DeclaringEntityType : ForeignKey.PrincipalEntityType; + + /// + /// 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 EntityType TargetEntityType { get; } + + /// + /// 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 SkipNavigation Inverse { get; [param: NotNull] private set; } + + /// + /// 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 IsCollection { get; } + + /// + /// 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 IsOnPrincipal { get; } + + /// + /// 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 GetConfigurationSource() => _configurationSource; + + /// + /// 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 UpdateConfigurationSource(ConfigurationSource configurationSource) + { + var oldConfigurationSource = _configurationSource; + _configurationSource = configurationSource.Max(_configurationSource); + return _configurationSource != oldConfigurationSource; + } + + /// + /// 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 IConventionSkipNavigation SetInverse([CanBeNull] SkipNavigation inverse, ConfigurationSource configurationSource) + { + var oldInverse = Inverse; + var isChanging = inverse != Inverse; + if (inverse == null) + { + Inverse = null; + _inverseConfigurationSource = null; + + return isChanging + ? DeclaringType.Model.ConventionDispatcher.OnSkipNavigationInverseChanged(Builder, inverse, oldInverse) + : inverse; + } + + if (inverse.DeclaringType != TargetEntityType) + { + throw new InvalidOperationException(CoreStrings.SkipNavigationWrongInverse( + inverse.Name, inverse.DeclaringType.DisplayName(), Name, TargetEntityType.DisplayName())); + } + + if (inverse.AssociationEntityType != AssociationEntityType) + { + throw new InvalidOperationException(CoreStrings.SkipInverseMismatchedAssociationType( + inverse.Name, inverse.AssociationEntityType.DisplayName(), Name, AssociationEntityType.DisplayName())); + } + + Inverse = inverse; + UpdateInverseConfigurationSource(configurationSource); + + return isChanging + ? DeclaringType.Model.ConventionDispatcher.OnSkipNavigationInverseChanged(Builder, inverse, oldInverse) + : inverse; + } + + /// + /// 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? GetInverseConfigurationSource() + => _inverseConfigurationSource; + + /// + /// 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 void UpdateInverseConfigurationSource(ConfigurationSource configurationSource) + => _inverseConfigurationSource = _inverseConfigurationSource.Max(configurationSource); + + /// + /// Runs the conventions when an annotation was set or removed. + /// + /// The key of the set annotation. + /// The annotation set. + /// The old annotation. + /// The annotation that was set. + protected override IConventionAnnotation OnAnnotationSet( + string name, IConventionAnnotation annotation, IConventionAnnotation oldAnnotation) + => DeclaringType.Model.ConventionDispatcher.OnSkipNavigationAnnotationChanged( + Builder, name, annotation, oldAnnotation); + + /// + /// 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 IClrCollectionAccessor CollectionAccessor + => NonCapturingLazyInitializer.EnsureInitialized( + ref _collectionAccessor, this, n => new ClrCollectionAccessorFactory().Create(n)); + + /// + /// 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 DebugView DebugView + => new DebugView( + () => this.ToDebugString(MetadataDebugStringOptions.ShortDefault), + () => this.ToDebugString(MetadataDebugStringOptions.LongDefault)); + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + [DebuggerStepThrough] + public override string ToString() => this.ToDebugString(MetadataDebugStringOptions.SingleLineDefault); + + IConventionSkipNavigationBuilder IConventionSkipNavigation.Builder + { + [DebuggerStepThrough] + get => Builder; + } + + IEntityType ISkipNavigation.TargetEntityType + { + [DebuggerStepThrough] + get => TargetEntityType; + } + + IMutableEntityType IMutableSkipNavigation.TargetEntityType + { + [DebuggerStepThrough] + get => TargetEntityType; + } + + IConventionEntityType IConventionSkipNavigation.TargetEntityType + { + [DebuggerStepThrough] + get => TargetEntityType; + } + + IForeignKey ISkipNavigation.ForeignKey + { + [DebuggerStepThrough] + get => ForeignKey; + } + + IMutableForeignKey IMutableSkipNavigation.ForeignKey + { + [DebuggerStepThrough] + get => ForeignKey; + } + + IConventionForeignKey IConventionSkipNavigation.ForeignKey + { + [DebuggerStepThrough] + get => ForeignKey; + } + + ISkipNavigation ISkipNavigation.Inverse + { + [DebuggerStepThrough] + get => Inverse; + } + + IMutableSkipNavigation IMutableSkipNavigation.Inverse + { + [DebuggerStepThrough] + get => Inverse; + } + + IConventionSkipNavigation IConventionSkipNavigation.Inverse + { + [DebuggerStepThrough] + get => Inverse; + } + + [DebuggerStepThrough] + IConventionSkipNavigation IMutableSkipNavigation.SetInverse([CanBeNull] IMutableSkipNavigation inverse) + => SetInverse((SkipNavigation)inverse, ConfigurationSource.Explicit); + + [DebuggerStepThrough] + IConventionSkipNavigation IConventionSkipNavigation.SetInverse([CanBeNull] IConventionSkipNavigation inverse, bool fromDataAnnotation) + => SetInverse((SkipNavigation)inverse, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + } +} diff --git a/src/EFCore/Metadata/Internal/SkipNavigationComparer.cs b/src/EFCore/Metadata/Internal/SkipNavigationComparer.cs new file mode 100644 index 00000000000..40e9472cb29 --- /dev/null +++ b/src/EFCore/Metadata/Internal/SkipNavigationComparer.cs @@ -0,0 +1,32 @@ +// 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; + +namespace Microsoft.EntityFrameworkCore.Metadata.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 sealed class SkipNavigationComparer : IComparer + { + private SkipNavigationComparer() + { + } + + /// + /// 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 readonly SkipNavigationComparer Instance = new SkipNavigationComparer(); + + public int Compare(SkipNavigation x, SkipNavigation y) + => StringComparer.Ordinal.Compare(x.Name, y.Name); + } +} diff --git a/src/EFCore/Metadata/Internal/SkipNavigationExtensions.cs b/src/EFCore/Metadata/Internal/SkipNavigationExtensions.cs new file mode 100644 index 00000000000..c975a4137da --- /dev/null +++ b/src/EFCore/Metadata/Internal/SkipNavigationExtensions.cs @@ -0,0 +1,95 @@ +// 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.Text; +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Infrastructure.Internal; + +namespace Microsoft.EntityFrameworkCore.Metadata.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 SkipNavigationExtensions + { + /// + /// 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 ToDebugString( + [NotNull] this ISkipNavigation navigation, + MetadataDebugStringOptions options, + [NotNull] string indent = "") + { + var builder = new StringBuilder(); + + builder.Append(indent); + + var singleLine = (options & MetadataDebugStringOptions.SingleLine) != 0; + if (singleLine) + { + builder.Append($"SkipNavigation: {navigation.DeclaringEntityType.DisplayName()}."); + } + + builder.Append(navigation.Name); + + var field = navigation.GetFieldName(); + if (field == null) + { + builder.Append(" (no field, "); + } + else if (!field.EndsWith(">k__BackingField", StringComparison.Ordinal)) + { + builder.Append($" ({field}, "); + } + else + { + builder.Append(" ("); + } + + builder.Append(navigation.ClrType?.ShortDisplayName()).Append(")"); + + if (navigation.IsCollection) + { + builder.Append(" Collection"); + } + + builder.Append(navigation.TargetEntityType.DisplayName()); + + if (navigation.Inverse != null) + { + builder.Append(" Inverse: ").Append(navigation.Inverse.Name); + } + + if (navigation.GetPropertyAccessMode() != PropertyAccessMode.PreferField) + { + builder.Append(" PropertyAccessMode.").Append(navigation.GetPropertyAccessMode()); + } + + if ((options & MetadataDebugStringOptions.IncludePropertyIndexes) != 0) + { + var indexes = navigation.GetPropertyIndexes(); + builder.Append(" ").Append(indexes.Index); + builder.Append(" ").Append(indexes.OriginalValueIndex); + builder.Append(" ").Append(indexes.RelationshipIndex); + builder.Append(" ").Append(indexes.ShadowIndex); + builder.Append(" ").Append(indexes.StoreGenerationIndex); + } + + if (!singleLine && + (options & MetadataDebugStringOptions.IncludeAnnotations) != 0) + { + builder.Append(navigation.AnnotationsToDebugString(indent + " ")); + } + + return builder.ToString(); + } + } +} diff --git a/src/EFCore/Properties/CoreStrings.Designer.cs b/src/EFCore/Properties/CoreStrings.Designer.cs index 677a7dfa10c..85f83f0a9b1 100644 --- a/src/EFCore/Properties/CoreStrings.Designer.cs +++ b/src/EFCore/Properties/CoreStrings.Designer.cs @@ -545,12 +545,12 @@ public static string PropertyInUseKey([CanBeNull] object property, [CanBeNull] o property, entityType, key); /// - /// Cannot remove key {key} from entity type '{entityType}' because it is referenced by a foreign key in entity type '{dependentType}'. All foreign keys must be removed or redefined before the referenced key can be removed. + /// Cannot remove key {key} from entity type '{entityType}' because it is referenced by a foreign key {foreignKey} in entity type '{dependentType}'. All foreign keys must be removed or redefined before the referenced key can be removed. /// - public static string KeyInUse([CanBeNull] object key, [CanBeNull] object entityType, [CanBeNull] object dependentType) + public static string KeyInUse([CanBeNull] object key, [CanBeNull] object entityType, [CanBeNull] object foreignKey, [CanBeNull] object dependentType) => string.Format( - GetString("KeyInUse", nameof(key), nameof(entityType), nameof(dependentType)), - key, entityType, dependentType); + GetString("KeyInUse", nameof(key), nameof(entityType), nameof(foreignKey), nameof(dependentType)), + key, entityType, foreignKey, dependentType); /// /// The service property '{property}' of type '{serviceType}' cannot be added to the entity type '{entityType}' because service property '{duplicateName}' of the same type already exists on entity type '{duplicateEntityType}'. @@ -2097,36 +2097,36 @@ public static string NoDiscriminatorValue([CanBeNull] object entityType) entityType); /// - /// The foreign key {foreignKey} targeting the key {key} on '{principalType}' is defined on the entity type '{otherEntityType}', not '{entityType}'. + /// The foreign key {foreignKey} targeting the key {key} on '{principalType}' cannot be removed from the entity type '{entityType}' because it is defined on the entity type '{otherEntityType}'. /// - public static string ForeignKeyWrongType([CanBeNull] object foreignKey, [CanBeNull] object key, [CanBeNull] object principalType, [CanBeNull] object otherEntityType, [CanBeNull] object entityType) + public static string ForeignKeyWrongType([CanBeNull] object foreignKey, [CanBeNull] object key, [CanBeNull] object principalType, [CanBeNull] object entityType, [CanBeNull] object otherEntityType) => string.Format( - GetString("ForeignKeyWrongType", nameof(foreignKey), nameof(key), nameof(principalType), nameof(otherEntityType), nameof(entityType)), - foreignKey, key, principalType, otherEntityType, entityType); + GetString("ForeignKeyWrongType", nameof(foreignKey), nameof(key), nameof(principalType), nameof(entityType), nameof(otherEntityType)), + foreignKey, key, principalType, entityType, otherEntityType); /// - /// The index {index} is defined on the entity type '{otherEntityType}', not '{entityType}'. + /// The index {index} cannot be removed from the entity type '{entityType}' because it is defined on the entity type '{otherEntityType}'. /// - public static string IndexWrongType([CanBeNull] object index, [CanBeNull] object otherEntityType, [CanBeNull] object entityType) + public static string IndexWrongType([CanBeNull] object index, [CanBeNull] object entityType, [CanBeNull] object otherEntityType) => string.Format( - GetString("IndexWrongType", nameof(index), nameof(otherEntityType), nameof(entityType)), - index, otherEntityType, entityType); + GetString("IndexWrongType", nameof(index), nameof(entityType), nameof(otherEntityType)), + index, entityType, otherEntityType); /// - /// The key {key} cis defined on the entity type '{otherEntityType}', not '{entityType}'. + /// The key {key} cannot be removed from the entity type '{entityType}' because it is defined on the entity type '{otherEntityType}'. /// - public static string KeyWrongType([CanBeNull] object key, [CanBeNull] object otherEntityType, [CanBeNull] object entityType) + public static string KeyWrongType([CanBeNull] object key, [CanBeNull] object entityType, [CanBeNull] object otherEntityType) => string.Format( - GetString("KeyWrongType", nameof(key), nameof(otherEntityType), nameof(entityType)), - key, otherEntityType, entityType); + GetString("KeyWrongType", nameof(key), nameof(entityType), nameof(otherEntityType)), + key, entityType, otherEntityType); /// - /// The specified property '{property}' is declared on the entity type '{otherEntityType}', not '{entityType}'. + /// The property '{property}' cannot be removed from the entity type '{entityType}' because it is declared on the entity type '{otherEntityType}'. /// - public static string PropertyWrongType([CanBeNull] object property, [CanBeNull] object otherEntityType, [CanBeNull] object entityType) + public static string PropertyWrongType([CanBeNull] object property, [CanBeNull] object entityType, [CanBeNull] object otherEntityType) => string.Format( - GetString("PropertyWrongType", nameof(property), nameof(otherEntityType), nameof(entityType)), - property, otherEntityType, entityType); + GetString("PropertyWrongType", nameof(property), nameof(entityType), nameof(otherEntityType)), + property, entityType, otherEntityType); /// /// There is no navigation on entity type '{entityType}' associated with the foreign key {foreignKey}. @@ -2236,6 +2236,70 @@ public static string ClientProjectionCapturingConstantInTree([CanBeNull] object GetString("ClientProjectionCapturingConstantInTree", nameof(constantType)), constantType); + /// + /// Cannot remove foreign key {foreigKey} from entity type '{entityType}' because it is referenced by a skip navigation '{navigation}' on entity type '{navigationEntityType}'. All referencing skip navigation must be removed before the referenced foreign key can be removed. + /// + public static string ForeignKeyInUseSkipNavigation([CanBeNull] object foreigKey, [CanBeNull] object entityType, [CanBeNull] object navigation, [CanBeNull] object navigationEntityType) + => string.Format( + GetString("ForeignKeyInUseSkipNavigation", nameof(foreigKey), nameof(entityType), nameof(navigation), nameof(navigationEntityType)), + foreigKey, entityType, navigation, navigationEntityType); + + /// + /// The skip navigation property '{navigation}' cannot be added to entity type '{entityType}' because it is expected to be on the dependent entity type '{dependentEntityType}' of the foreign key {foreigKey}. + /// + public static string SkipNavigationWrongDependentType([CanBeNull] object navigation, [CanBeNull] object entityType, [CanBeNull] object dependentEntityType, [CanBeNull] object foreigKey) + => string.Format( + GetString("SkipNavigationWrongDependentType", nameof(navigation), nameof(entityType), nameof(dependentEntityType), nameof(foreigKey)), + navigation, entityType, dependentEntityType, foreigKey); + + /// + /// The skip navigation '{inverse}' declared on the entity type '{inverseEntityType}' cannot be set as the inverse of '{navigation}' that targets '{targetEntityType}'. The inverse should be declared on the target entity type. + /// + public static string SkipNavigationWrongInverse([CanBeNull] object inverse, [CanBeNull] object inverseEntityType, [CanBeNull] object navigation, [CanBeNull] object targetEntityType) + => string.Format( + GetString("SkipNavigationWrongInverse", nameof(inverse), nameof(inverseEntityType), nameof(navigation), nameof(targetEntityType)), + inverse, inverseEntityType, navigation, targetEntityType); + + /// + /// The skip navigation property '{navigation}' cannot be added to entity type '{entityType}' because it is expected to be on the principal entity type '{principalEntityType}' of the foreign key {foreigKey}. + /// + public static string SkipNavigationWrongPrincipalType([CanBeNull] object navigation, [CanBeNull] object entityType, [CanBeNull] object principalEntityType, [CanBeNull] object foreigKey) + => string.Format( + GetString("SkipNavigationWrongPrincipalType", nameof(navigation), nameof(entityType), nameof(principalEntityType), nameof(foreigKey)), + navigation, entityType, principalEntityType, foreigKey); + + /// + /// The skip navigation property '{navigation}' cannot be removed from the entity type '{entityType}' because it is defined on the entity type '{otherEntityType}'. + /// + public static string SkipNavigationWrongType([CanBeNull] object navigation, [CanBeNull] object entityType, [CanBeNull] object otherEntityType) + => string.Format( + GetString("SkipNavigationWrongType", nameof(navigation), nameof(entityType), nameof(otherEntityType)), + navigation, entityType, otherEntityType); + + /// + /// The skip navigation '{inverse}' using the association entity type '{inverseAssociationType}' cannot be set as the inverse of '{navigation}' that uses the association entity type '{associationType}'. The inverse should use the same association entity type. + /// + public static string SkipInverseMismatchedAssociationType([CanBeNull] object inverse, [CanBeNull] object inverseAssociationType, [CanBeNull] object navigation, [CanBeNull] object associationType) + => string.Format( + GetString("SkipInverseMismatchedAssociationType", nameof(inverse), nameof(inverseAssociationType), nameof(navigation), nameof(associationType)), + inverse, inverseAssociationType, navigation, associationType); + + /// + /// The skip navigation '{navigation}' on entity type '{entityType}' doesn't have an inverse configured. Every skip navigation should have an inverse skip navigation. + /// + public static string SkipNavigationNoInverse([CanBeNull] object navigation, [CanBeNull] object entityType) + => string.Format( + GetString("SkipNavigationNoInverse", nameof(navigation), nameof(entityType)), + navigation, entityType); + + /// + /// The skip navigation '{navigation}' on entity type '{entityType}' is not a collection. Only collection skip navigation properties are currently supported. + /// + public static string SkipNavigationNonCollection([CanBeNull] object navigation, [CanBeNull] object entityType) + => string.Format( + GetString("SkipNavigationNonCollection", nameof(navigation), nameof(entityType)), + navigation, entityType); + 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 8192768d23d..d73698fd7fb 100644 --- a/src/EFCore/Properties/CoreStrings.resx +++ b/src/EFCore/Properties/CoreStrings.resx @@ -443,7 +443,7 @@ The property '{property}' cannot be removed from entity type '{entityType}' because it is being used in the key {key}. All containing keys must be removed or redefined before the property can be removed. - Cannot remove key {key} from entity type '{entityType}' because it is referenced by a foreign key in entity type '{dependentType}'. All foreign keys must be removed or redefined before the referenced key can be removed. + Cannot remove key {key} from entity type '{entityType}' because it is referenced by a foreign key {foreignKey} in entity type '{dependentType}'. All foreign keys must be removed or redefined before the referenced key can be removed. The service property '{property}' of type '{serviceType}' cannot be added to the entity type '{entityType}' because service property '{duplicateName}' of the same type already exists on entity type '{duplicateEntityType}'. @@ -1149,16 +1149,16 @@ The entity type '{entityType}' is part of a hierarchy, but does not have a discriminator value configured. - The foreign key {foreignKey} targeting the key {key} on '{principalType}' is defined on the entity type '{otherEntityType}', not '{entityType}'. + The foreign key {foreignKey} targeting the key {key} on '{principalType}' cannot be removed from the entity type '{entityType}' because it is defined on the entity type '{otherEntityType}'. - The index {index} is defined on the entity type '{otherEntityType}', not '{entityType}'. + The index {index} cannot be removed from the entity type '{entityType}' because it is defined on the entity type '{otherEntityType}'. - The key {key} cis defined on the entity type '{otherEntityType}', not '{entityType}'. + The key {key} cannot be removed from the entity type '{entityType}' because it is defined on the entity type '{otherEntityType}'. - The specified property '{property}' is declared on the entity type '{otherEntityType}', not '{entityType}'. + The property '{property}' cannot be removed from the entity type '{entityType}' because it is declared on the entity type '{otherEntityType}'. There is no navigation on entity type '{entityType}' associated with the foreign key {foreignKey}. @@ -1218,4 +1218,28 @@ Client projection contains reference to constant expression of '{constantType}'. This could potentially cause memory leak. Consider assigning this constant to local variable and using the variable in the query instead. See https://go.microsoft.com/fwlink/?linkid=2103067 for more information. + + Cannot remove foreign key {foreigKey} from entity type '{entityType}' because it is referenced by a skip navigation '{navigation}' on entity type '{navigationEntityType}'. All referencing skip navigation must be removed before the referenced foreign key can be removed. + + + The skip navigation property '{navigation}' cannot be added to entity type '{entityType}' because it is expected to be on the dependent entity type '{dependentEntityType}' of the foreign key {foreigKey}. + + + The skip navigation '{inverse}' declared on the entity type '{inverseEntityType}' cannot be set as the inverse of '{navigation}' that targets '{targetEntityType}'. The inverse should be declared on the target entity type. + + + The skip navigation property '{navigation}' cannot be added to entity type '{entityType}' because it is expected to be on the principal entity type '{principalEntityType}' of the foreign key {foreigKey}. + + + The skip navigation property '{navigation}' cannot be removed from the entity type '{entityType}' because it is defined on the entity type '{otherEntityType}'. + + + The skip navigation '{inverse}' using the association entity type '{inverseAssociationType}' cannot be set as the inverse of '{navigation}' that uses the association entity type '{associationType}'. The inverse should use the same association entity type. + + + The skip navigation '{navigation}' on entity type '{entityType}' doesn't have an inverse configured. Every skip navigation should have an inverse skip navigation. + + + The skip navigation '{navigation}' on entity type '{entityType}' is not a collection. Only collection skip navigation properties are currently supported. + \ No newline at end of file diff --git a/test/EFCore.Cosmos.Tests/Infrastructure/CosmosModelValidatorTest.cs b/test/EFCore.Cosmos.Tests/Infrastructure/CosmosModelValidatorTest.cs index adb97b9df6f..268d40d4335 100644 --- a/test/EFCore.Cosmos.Tests/Infrastructure/CosmosModelValidatorTest.cs +++ b/test/EFCore.Cosmos.Tests/Infrastructure/CosmosModelValidatorTest.cs @@ -128,27 +128,5 @@ public virtual void Detects_duplicate_discriminator_values() } protected override TestHelpers TestHelpers => CosmosTestHelpers.Instance; - - private class Customer - { - public int Id { get; set; } - public string Name { get; set; } - public string PartitionId { get; set; } - public ICollection Orders { get; set; } - } - - private class Order - { - public int Id { get; set; } - public string PartitionId { get; set; } - public Customer Customer { get; set; } - public OrderDetails OrderDetails { get; set; } - } - - [Owned] - private class OrderDetails - { - public string ShippingAddress { get; set; } - } } } diff --git a/test/EFCore.Relational.Tests/Infrastructure/RelationalModelValidatorTest.cs b/test/EFCore.Relational.Tests/Infrastructure/RelationalModelValidatorTest.cs index 9ab4bba86da..6e0f7dea40a 100644 --- a/test/EFCore.Relational.Tests/Infrastructure/RelationalModelValidatorTest.cs +++ b/test/EFCore.Relational.Tests/Infrastructure/RelationalModelValidatorTest.cs @@ -723,7 +723,7 @@ public virtual void Passes_for_compatible_duplicate_foreignKey_names_within_hier { et.Property(c => c.Breed).HasColumnName("Breed"); fk2 = et - .HasOne(a => (Customer)a.FavoritePerson) + .HasOne(a => (Employee)a.FavoritePerson) .WithMany() .HasForeignKey( c => new { c.Name, c.Breed }) @@ -770,7 +770,7 @@ public virtual void Passes_for_compatible_duplicate_foreignKey_names_within_hier { et.Property(c => c.Breed).HasColumnName("Breed"); fk2 = et - .HasOne() + .HasOne() .WithMany() .HasForeignKey( c => new { c.Name, c.Breed }) @@ -1081,7 +1081,7 @@ protected class Person public string FavoriteBreed { get; set; } } - protected class Customer : Person + protected class Employee : Person { } diff --git a/test/EFCore.Tests/ApiConsistencyTest.cs b/test/EFCore.Tests/ApiConsistencyTest.cs index 6dd026bc097..6967031db9d 100644 --- a/test/EFCore.Tests/ApiConsistencyTest.cs +++ b/test/EFCore.Tests/ApiConsistencyTest.cs @@ -74,6 +74,7 @@ public class ApiConsistencyTest : ApiConsistencyTestBase { typeof(IIndex), (typeof(IMutableIndex), typeof(IConventionIndex)) }, { typeof(IProperty), (typeof(IMutableProperty), typeof(IConventionProperty)) }, { typeof(INavigation), (typeof(IMutableNavigation), typeof(IConventionNavigation)) }, + { typeof(ISkipNavigation), (typeof(IMutableSkipNavigation), typeof(IConventionSkipNavigation)) }, { typeof(IServiceProperty), (typeof(IMutableServiceProperty), typeof(IConventionServiceProperty)) }, { typeof(IPropertyBase), (typeof(IMutablePropertyBase), typeof(IConventionPropertyBase)) }, { typeof(ModelExtensions), (typeof(MutableModelExtensions), typeof(ConventionModelExtensions)) }, diff --git a/test/EFCore.Tests/ApiConsistencyTestBase.cs b/test/EFCore.Tests/ApiConsistencyTestBase.cs index 5907fe60943..b44d0517c67 100644 --- a/test/EFCore.Tests/ApiConsistencyTestBase.cs +++ b/test/EFCore.Tests/ApiConsistencyTestBase.cs @@ -145,8 +145,8 @@ private string MatchMutable((MethodInfo Readonly, MethodInfo Mutable) methodTupl { if (mutableMethod.ReturnType != expectedReturnTypes.Mutable) { - return - $"{mutableMethod.DeclaringType.Name}.{mutableMethod.Name} expected to have {expectedReturnTypes.Mutable} return type"; + return $"{mutableMethod.DeclaringType.Name}.{mutableMethod.Name}" + + $" expected to have {expectedReturnTypes.Mutable} return type"; } } else @@ -157,8 +157,8 @@ private string MatchMutable((MethodInfo Readonly, MethodInfo Mutable) methodTupl { if (mutableMethod.ReturnType.TryGetSequenceType() != expectedReturnTypes.Mutable) { - return - $"{mutableMethod.DeclaringType.Name}.{mutableMethod.Name} expected to have a return type that derives from IEnumerable<{expectedReturnTypes.Mutable}>."; + return $"{mutableMethod.DeclaringType.Name}.{mutableMethod.Name}" + + $" expected to have a return type that derives from IEnumerable<{expectedReturnTypes.Mutable}>."; } } } diff --git a/test/EFCore.Tests/Infrastructure/ModelValidatorTest.cs b/test/EFCore.Tests/Infrastructure/ModelValidatorTest.cs index 6d61910a606..3a29b6135e7 100644 --- a/test/EFCore.Tests/Infrastructure/ModelValidatorTest.cs +++ b/test/EFCore.Tests/Infrastructure/ModelValidatorTest.cs @@ -478,6 +478,56 @@ public virtual void Detects_generic_leaf_type() VerifyError(CoreStrings.AbstractLeafEntityType(entityGeneric.DisplayName()), model); } + [ConditionalFact] + public virtual void Passes_on_valid_many_to_many_navigations() + { + var modelBuilder = CreateConventionalModelBuilder(); + + var model = modelBuilder.Model; + var orderProductEntity = model.AddEntityType(typeof(OrderProduct)); + var orderEntity = model.FindEntityType(typeof(Order)); + var productEntity = model.FindEntityType(typeof(Product)); + var orderProductForeignKey = orderProductEntity + .GetForeignKeys().Single(fk => fk.PrincipalEntityType == orderEntity); + var productOrderForeignKey = orderProductEntity + .GetForeignKeys().Single(fk => fk.PrincipalEntityType == productEntity); + orderProductEntity.SetPrimaryKey(new[] { orderProductForeignKey.Properties.Single(), productOrderForeignKey.Properties.Single() }); + + var productsNavigation = orderEntity.AddSkipNavigation( + nameof(Order.Products), null, productEntity, orderProductForeignKey, true, true); + + var ordersNavigation = productEntity.AddSkipNavigation( + nameof(Product.Orders), null, orderEntity, productOrderForeignKey, true, true); + + productsNavigation.SetInverse(ordersNavigation); + ordersNavigation.SetInverse(productsNavigation); + + Validate(model); + } + + [ConditionalFact] + public virtual void Detects_missing_inverse_skip_navigations() + { + var modelBuilder = CreateConventionalModelBuilder(); + + var model = modelBuilder.Model; + var orderProductEntity = model.AddEntityType(typeof(OrderProduct)); + var orderEntity = model.FindEntityType(typeof(Order)); + var productEntity = model.FindEntityType(typeof(Product)); + var orderProductForeignKey = orderProductEntity + .GetForeignKeys().Single(fk => fk.PrincipalEntityType == orderEntity); + var productOrderForeignKey = orderProductEntity + .GetForeignKeys().Single(fk => fk.PrincipalEntityType == productEntity); + orderProductEntity.SetPrimaryKey(new[] { orderProductForeignKey.Properties.Single(), productOrderForeignKey.Properties.Single() }); + + var productsNavigation = orderEntity.AddSkipNavigation( + nameof(Order.Products), null, productEntity, orderProductForeignKey, true, true); + + VerifyError( + CoreStrings.SkipNavigationNoInverse(nameof(Order.Products), nameof(Order)), + model); + } + [ConditionalFact] public virtual void Passes_on_valid_owned_entity_types() { diff --git a/test/EFCore.Tests/Infrastructure/ModelValidatorTestBase.cs b/test/EFCore.Tests/Infrastructure/ModelValidatorTestBase.cs index 640d6f3a144..8122637d51e 100644 --- a/test/EFCore.Tests/Infrastructure/ModelValidatorTestBase.cs +++ b/test/EFCore.Tests/Infrastructure/ModelValidatorTestBase.cs @@ -6,6 +6,7 @@ using System.ComponentModel.DataAnnotations.Schema; using System.Diagnostics; using System.Linq; +using System.Reflection; using Microsoft.EntityFrameworkCore.Diagnostics.Internal; using Microsoft.EntityFrameworkCore.Internal; using Microsoft.EntityFrameworkCore.Metadata; @@ -192,6 +193,55 @@ public int this[int index] } } + protected class Customer + { + public int Id { get; set; } + public string Name { get; set; } + public string PartitionId { get; set; } + public ICollection Orders { get; set; } + } + + protected class Order + { + public static readonly PropertyInfo IdProperty = typeof(Order).GetProperty(nameof(Id)); + + public int Id { get; set; } + public string PartitionId { get; set; } + public Customer Customer { get; set; } + + public OrderDetails OrderDetails { get; set; } + + [NotMapped] + public virtual ICollection Products { get; set; } + } + + [Owned] + protected class OrderDetails + { + public string ShippingAddress { get; set; } + } + + protected class OrderProduct + { + public static readonly PropertyInfo OrderIdProperty = typeof(OrderProduct).GetProperty(nameof(OrderId)); + public static readonly PropertyInfo ProductIdProperty = typeof(OrderProduct).GetProperty(nameof(ProductId)); + + public int OrderId { get; set; } + public int ProductId { get; set; } + public virtual Order Order { get; set; } + public virtual Product Product { get; set; } + } + + protected class Product + { + public static readonly PropertyInfo IdProperty = typeof(Product).GetProperty(nameof(Id)); + + public int Id { get; set; } + + [NotMapped] + public virtual ICollection Orders { get; set; } + } + protected ModelValidatorTestBase() => LoggerFactory = new ListLoggerFactory(l => l == DbLoggerCategory.Model.Validation.Name || l == DbLoggerCategory.Model.Name); diff --git a/test/EFCore.Tests/Metadata/Conventions/ConventionDispatcherTest.cs b/test/EFCore.Tests/Metadata/Conventions/ConventionDispatcherTest.cs index 6e40eb0d0b8..9808d62787b 100644 --- a/test/EFCore.Tests/Metadata/Conventions/ConventionDispatcherTest.cs +++ b/test/EFCore.Tests/Metadata/Conventions/ConventionDispatcherTest.cs @@ -941,8 +941,17 @@ public void OnForeignKeyRemoved_calls_conventions_in_order(bool useScope) var scope = useScope ? builder.Metadata.ConventionDispatcher.DelayConventions() : null; - Assert.NotNull( - entityBuilder.Metadata.RemoveForeignKey(foreignKey.Properties, foreignKey.PrincipalKey, foreignKey.PrincipalEntityType)); + var result = entityBuilder.Metadata.RemoveForeignKey( + foreignKey.Properties, foreignKey.PrincipalKey, foreignKey.PrincipalEntityType); + + if (useScope) + { + Assert.Same(foreignKey, result); + } + else + { + Assert.Null(result); + } if (useScope) { @@ -1681,6 +1690,111 @@ public void ProcessNavigationAdded( } } + [InlineData(false, false)] + [InlineData(true, false)] + [InlineData(false, true)] + [InlineData(true, true)] + [ConditionalTheory] + public void OnNavigationAnnotationChanged_calls_conventions_in_order(bool useBuilder, bool useScope) + { + var conventions = new ConventionSet(); + + var convention1 = new NavigationAnnotationChangedConvention(terminate: false); + var convention2 = new NavigationAnnotationChangedConvention(terminate: true); + var convention3 = new NavigationAnnotationChangedConvention(terminate: false); + conventions.NavigationAnnotationChangedConventions.Add(convention1); + conventions.NavigationAnnotationChangedConventions.Add(convention2); + conventions.NavigationAnnotationChangedConventions.Add(convention3); + + var builder = new InternalModelBuilder(new Model(conventions)); + var principalEntityBuilder = builder.Entity(typeof(Order), ConfigurationSource.Convention); + var dependentEntityBuilder = builder.Entity(typeof(OrderDetails), ConfigurationSource.Convention); + var navigation = dependentEntityBuilder.HasRelationship( + principalEntityBuilder.Metadata, OrderDetails.OrderProperty, ConfigurationSource.Convention) + .Metadata.DependentToPrincipal; + + var scope = useScope ? builder.Metadata.ConventionDispatcher.DelayConventions() : null; + + if (useBuilder) + { + Assert.NotNull(navigation.Builder.HasAnnotation("foo", "bar", ConfigurationSource.Convention)); + } + else + { + navigation["foo"] = "bar"; + } + + if (useScope) + { + Assert.Empty(convention1.Calls); + Assert.Empty(convention2.Calls); + scope.Dispose(); + } + + Assert.Equal(new[] { "bar" }, convention1.Calls); + Assert.Equal(new[] { "bar" }, convention2.Calls); + Assert.Empty(convention3.Calls); + + if (useBuilder) + { + Assert.NotNull(navigation.Builder.HasAnnotation("foo", "bar", ConfigurationSource.Convention)); + } + else + { + navigation["foo"] = "bar"; + } + + Assert.Equal(new[] { "bar" }, convention1.Calls); + Assert.Equal(new[] { "bar" }, convention2.Calls); + Assert.Empty(convention3.Calls); + + if (useBuilder) + { + Assert.NotNull(navigation.Builder.HasAnnotation("foo", null, ConfigurationSource.Convention)); + } + else + { + navigation.RemoveAnnotation("foo"); + } + + Assert.Equal(new[] { "bar", null }, convention1.Calls); + Assert.Equal(new[] { "bar", null }, convention2.Calls); + Assert.Empty(convention3.Calls); + + navigation[CoreAnnotationNames.EagerLoaded] = true; + + Assert.Equal(new[] { "bar", null }, convention1.Calls); + } + + private class NavigationAnnotationChangedConvention : INavigationAnnotationChangedConvention + { + private readonly bool _terminate; + public readonly List Calls = new List(); + + public NavigationAnnotationChangedConvention(bool terminate) + { + _terminate = terminate; + } + + public virtual void ProcessNavigationAnnotationChanged( + IConventionRelationshipBuilder relationshipBuilder, + IConventionNavigation navigation, + string name, + IConventionAnnotation annotation, + IConventionAnnotation oldAnnotation, + IConventionContext context) + { + Assert.NotNull(relationshipBuilder.Metadata.Builder); + + Calls.Add(annotation?.Value); + + if (_terminate) + { + context.StopProcessing(); + } + } + } + [InlineData(false, false)] [InlineData(true, false)] [InlineData(false, true)] @@ -1772,6 +1886,365 @@ public void ProcessNavigationRemoved( } } + [InlineData(false, false)] + //[InlineData(true, false)] + [InlineData(false, true)] + //[InlineData(true, true)] + [ConditionalTheory] + public void OnSkipNavigationAdded_calls_conventions_in_order(bool useBuilder, bool useScope) + { + var conventions = new ConventionSet(); + + var convention1 = new SkipNavigationAddedConvention(terminate: false); + var convention2 = new SkipNavigationAddedConvention(terminate: true); + var convention3 = new SkipNavigationAddedConvention(terminate: false); + conventions.SkipNavigationAddedConventions.Add(convention1); + conventions.SkipNavigationAddedConventions.Add(convention2); + conventions.SkipNavigationAddedConventions.Add(convention3); + + var builder = new InternalModelBuilder(new Model(conventions)); + var firstEntityBuilder = builder.Entity(typeof(Order), ConfigurationSource.Convention); + var secondEntityBuilder = builder.Entity(typeof(Product), ConfigurationSource.Convention); + var associationEntityBuilder = builder.Entity(typeof(OrderProduct), ConfigurationSource.Convention); + + var firstFk = associationEntityBuilder + .HasRelationship(typeof(Order), new [] { OrderProduct.OrderIdProperty }, ConfigurationSource.Convention) + .IsUnique(false, ConfigurationSource.Convention) + .Metadata; + + var scope = useScope ? builder.Metadata.ConventionDispatcher.DelayConventions() : null; + + if (useBuilder) + { + throw new NotImplementedException(); + } + else + { + var result = firstEntityBuilder.Metadata.AddSkipNavigation( + nameof(Order.Products), null, secondEntityBuilder.Metadata, firstFk, true, true, ConfigurationSource.Convention); + + Assert.Equal(!useScope, result == null); + } + + if (useScope) + { + Assert.Empty(convention1.Calls); + Assert.Empty(convention2.Calls); + scope.Dispose(); + } + + Assert.Equal(new[] { nameof(Order.Products) }, convention1.Calls); + Assert.Equal(new[] { nameof(Order.Products) }, convention2.Calls); + Assert.Empty(convention3.Calls); + } + + private class SkipNavigationAddedConvention : ISkipNavigationAddedConvention + { + private readonly bool _terminate; + public readonly List Calls = new List(); + + public SkipNavigationAddedConvention(bool terminate) + { + _terminate = terminate; + } + + public void ProcessSkipNavigationAdded( + IConventionSkipNavigationBuilder skipNavigationBuilder, + IConventionContext context) + { + Assert.NotNull(skipNavigationBuilder.Metadata.Builder); + + Calls.Add(skipNavigationBuilder.Metadata.Name); + + if (_terminate) + { + skipNavigationBuilder.Metadata.DeclaringEntityType.RemoveSkipNavigation(skipNavigationBuilder.Metadata); + + context.StopProcessing(); + } + } + } + + [InlineData(false, false)] + [InlineData(true, false)] + [InlineData(false, true)] + [InlineData(true, true)] + [ConditionalTheory] + public void OnSkipNavigationAnnotationChanged_calls_conventions_in_order(bool useBuilder, bool useScope) + { + var conventions = new ConventionSet(); + + var convention1 = new SkipNavigationAnnotationChangedConvention(terminate: false); + var convention2 = new SkipNavigationAnnotationChangedConvention(terminate: true); + var convention3 = new SkipNavigationAnnotationChangedConvention(terminate: false); + conventions.SkipNavigationAnnotationChangedConventions.Add(convention1); + conventions.SkipNavigationAnnotationChangedConventions.Add(convention2); + conventions.SkipNavigationAnnotationChangedConventions.Add(convention3); + + var builder = new InternalModelBuilder(new Model(conventions)); + var firstEntityBuilder = builder.Entity(typeof(Order), ConfigurationSource.Convention); + var secondEntityBuilder = builder.Entity(typeof(Product), ConfigurationSource.Convention); + var associationEntityBuilder = builder.Entity(typeof(OrderProduct), ConfigurationSource.Convention); + + var firstFk = associationEntityBuilder + .HasRelationship(typeof(Order), new[] { OrderProduct.OrderIdProperty }, ConfigurationSource.Convention) + .IsUnique(false, ConfigurationSource.Convention) + .Metadata; + var navigation = firstEntityBuilder.Metadata.AddSkipNavigation( + nameof(Order.Products), null, secondEntityBuilder.Metadata, firstFk, true, true, ConfigurationSource.Convention); + + var scope = useScope ? builder.Metadata.ConventionDispatcher.DelayConventions() : null; + + if (useBuilder) + { + Assert.NotNull(navigation.Builder.HasAnnotation("foo", "bar", ConfigurationSource.Convention)); + } + else + { + navigation["foo"] = "bar"; + } + + if (useScope) + { + Assert.Empty(convention1.Calls); + Assert.Empty(convention2.Calls); + scope.Dispose(); + } + + Assert.Equal(new[] { "bar" }, convention1.Calls); + Assert.Equal(new[] { "bar" }, convention2.Calls); + Assert.Empty(convention3.Calls); + + if (useBuilder) + { + Assert.NotNull(navigation.Builder.HasAnnotation("foo", "bar", ConfigurationSource.Convention)); + } + else + { + navigation["foo"] = "bar"; + } + + Assert.Equal(new[] { "bar" }, convention1.Calls); + Assert.Equal(new[] { "bar" }, convention2.Calls); + Assert.Empty(convention3.Calls); + + if (useBuilder) + { + Assert.NotNull(navigation.Builder.HasAnnotation("foo", null, ConfigurationSource.Convention)); + } + else + { + navigation.RemoveAnnotation("foo"); + } + + Assert.Equal(new[] { "bar", null }, convention1.Calls); + Assert.Equal(new[] { "bar", null }, convention2.Calls); + Assert.Empty(convention3.Calls); + + navigation[CoreAnnotationNames.EagerLoaded] = true; + + Assert.Equal(new[] { "bar", null }, convention1.Calls); + } + + private class SkipNavigationAnnotationChangedConvention : ISkipNavigationAnnotationChangedConvention + { + private readonly bool _terminate; + public readonly List Calls = new List(); + + public SkipNavigationAnnotationChangedConvention(bool terminate) + { + _terminate = terminate; + } + + public virtual void ProcessSkipNavigationAnnotationChanged( + IConventionSkipNavigationBuilder navigationBuilder, + string name, + IConventionAnnotation annotation, + IConventionAnnotation oldAnnotation, + IConventionContext context) + { + Assert.NotNull(navigationBuilder.Metadata.Builder); + + Calls.Add(annotation?.Value); + + if (_terminate) + { + context.StopProcessing(); + } + } + } + + [InlineData(false, false)] + //[InlineData(true, false)] + [InlineData(false, true)] + //[InlineData(true, true)] + [ConditionalTheory] + public void OnSkipNavigationInverseChanged_calls_conventions_in_order(bool useBuilder, bool useScope) + { + var conventions = new ConventionSet(); + + var convention1 = new SkipNavigationInverseChangedConvention(terminate: false); + var convention2 = new SkipNavigationInverseChangedConvention(terminate: true); + var convention3 = new SkipNavigationInverseChangedConvention(terminate: false); + conventions.SkipNavigationInverseChangedConventions.Add(convention1); + conventions.SkipNavigationInverseChangedConventions.Add(convention2); + conventions.SkipNavigationInverseChangedConventions.Add(convention3); + + var builder = new InternalModelBuilder(new Model(conventions)); + var firstEntityBuilder = builder.Entity(typeof(Order), ConfigurationSource.Convention); + var secondEntityBuilder = builder.Entity(typeof(Product), ConfigurationSource.Convention); + var associationEntityBuilder = builder.Entity(typeof(OrderProduct), ConfigurationSource.Convention); + + var firstFk = associationEntityBuilder + .HasRelationship(typeof(Order), new[] { OrderProduct.OrderIdProperty }, ConfigurationSource.Convention) + .IsUnique(false, ConfigurationSource.Convention) + .Metadata; + var secondFk = associationEntityBuilder + .HasRelationship(typeof(Product), new[] { OrderProduct.ProductIdProperty }, ConfigurationSource.Convention) + .IsUnique(false, ConfigurationSource.Convention) + .Metadata; + var navigation = firstEntityBuilder.Metadata.AddSkipNavigation( + nameof(Order.Products), null, secondEntityBuilder.Metadata, firstFk, true, true, ConfigurationSource.Convention); + + var scope = useScope ? builder.Metadata.ConventionDispatcher.DelayConventions() : null; + + if (useBuilder) + { + throw new NotImplementedException(); + } + else + { + var inverse = secondEntityBuilder.Metadata.AddSkipNavigation( + nameof(Product.Orders), null, firstEntityBuilder.Metadata, secondFk, true, true, ConfigurationSource.Convention); + + var result = navigation.SetInverse(inverse, ConfigurationSource.Convention); + + if (useScope) + { + Assert.Same(inverse, result); + } + else + { + Assert.Null(result); + } + } + + if (useScope) + { + Assert.Empty(convention1.Calls); + Assert.Empty(convention2.Calls); + scope.Dispose(); + } + + Assert.Equal(new[] { nameof(Product.Orders) }, convention1.Calls); + Assert.Equal(new[] { nameof(Product.Orders) }, convention2.Calls); + Assert.Empty(convention3.Calls); + } + + private class SkipNavigationInverseChangedConvention : ISkipNavigationInverseChangedConvention + { + private readonly bool _terminate; + public readonly List Calls = new List(); + + public SkipNavigationInverseChangedConvention(bool terminate) + { + _terminate = terminate; + } + + public virtual void ProcessSkipNavigationInverseChanged( + IConventionSkipNavigationBuilder skipNavigationBuilder, + IConventionSkipNavigation inverse, + IConventionSkipNavigation oldInverse, + IConventionContext context) + { + Assert.NotNull(skipNavigationBuilder.Metadata.Builder); + + Calls.Add(inverse.Name); + + if (_terminate) + { + context.StopProcessing(); + } + } + } + + [InlineData(false)] + [InlineData(true)] + [ConditionalTheory] + public void OnSkipNavigationRemoved_calls_conventions_in_order(bool useScope) + { + var conventions = new ConventionSet(); + + var convention1 = new SkipNavigationRemovedConvention(terminate: false); + var convention2 = new SkipNavigationRemovedConvention(terminate: true); + var convention3 = new SkipNavigationRemovedConvention(terminate: false); + conventions.SkipNavigationRemovedConventions.Add(convention1); + conventions.SkipNavigationRemovedConventions.Add(convention2); + conventions.SkipNavigationRemovedConventions.Add(convention3); + + var builder = new InternalModelBuilder(new Model(conventions)); + var firstEntityBuilder = builder.Entity(typeof(Order), ConfigurationSource.Convention); + var secondEntityBuilder = builder.Entity(typeof(Product), ConfigurationSource.Convention); + var associationEntityBuilder = builder.Entity(typeof(OrderProduct), ConfigurationSource.Convention); + + var firstFk = associationEntityBuilder + .HasRelationship(typeof(Order), new[] { OrderProduct.OrderIdProperty }, ConfigurationSource.Convention) + .IsUnique(false, ConfigurationSource.Convention) + .Metadata; + var navigation = firstEntityBuilder.Metadata.AddSkipNavigation( + nameof(Order.Products), null, secondEntityBuilder.Metadata, firstFk, true, true, ConfigurationSource.Convention); + + var scope = useScope ? builder.Metadata.ConventionDispatcher.DelayConventions() : null; + + var result = firstEntityBuilder.Metadata.RemoveSkipNavigation(navigation); + + if (useScope) + { + Assert.Same(navigation, result); + } + else + { + Assert.Null(result); + } + + if (useScope) + { + Assert.Empty(convention1.Calls); + Assert.Empty(convention2.Calls); + scope.Dispose(); + } + + Assert.Equal(new[] { nameof(Order.Products) }, convention1.Calls); + Assert.Equal(new[] { nameof(Order.Products) }, convention2.Calls); + Assert.Empty(convention3.Calls); + } + + private class SkipNavigationRemovedConvention : ISkipNavigationRemovedConvention + { + private readonly bool _terminate; + public readonly List Calls = new List(); + + public SkipNavigationRemovedConvention(bool terminate) + { + _terminate = terminate; + } + + public void ProcessSkipNavigationRemoved( + IConventionEntityTypeBuilder entityTypeBuilder, + IConventionSkipNavigation navigation, + IConventionContext context) + { + Assert.NotNull(entityTypeBuilder.Metadata.Builder); + + Calls.Add(navigation.Name); + + if (_terminate) + { + context.StopProcessing(); + } + } + } + [InlineData(false, false)] [InlineData(true, false)] [InlineData(false, true)] @@ -2103,7 +2576,15 @@ public void OnIndexRemoved_calls_conventions_in_order(bool useScope) var scope = useScope ? builder.Metadata.ConventionDispatcher.DelayConventions() : null; - Assert.Same(index, entityBuilder.Metadata.RemoveIndex(index.Properties)); + var result = entityBuilder.Metadata.RemoveIndex(index.Properties); + if (useScope) + { + Assert.Same(index, result); + } + else + { + Assert.Null(result); + } if (useScope) { @@ -2771,6 +3252,77 @@ public void ProcessPropertyAnnotationChanged( } } + [InlineData(false)] + [InlineData(true)] + [ConditionalTheory] + public void OnPropertyRemoved_calls_conventions_in_order(bool useScope) + { + var conventions = new ConventionSet(); + + var convention1 = new PropertyRemovedConvention(terminate: false); + var convention2 = new PropertyRemovedConvention(terminate: true); + var convention3 = new PropertyRemovedConvention(terminate: false); + conventions.PropertyRemovedConventions.Add(convention1); + conventions.PropertyRemovedConventions.Add(convention2); + conventions.PropertyRemovedConventions.Add(convention3); + + var builder = new InternalModelBuilder(new Model(conventions)); + var entityBuilder = builder.Entity(typeof(Order), ConfigurationSource.Convention); + var shadowPropertyName = "ShadowProperty"; + var property = entityBuilder.Metadata.AddProperty( + shadowPropertyName, typeof(int), ConfigurationSource.Convention, ConfigurationSource.Convention); + + var scope = useScope ? builder.Metadata.ConventionDispatcher.DelayConventions() : null; + + var result = entityBuilder.Metadata.RemoveProperty(property); + + if (useScope) + { + Assert.Same(property, result); + } + else + { + Assert.Null(result); + } + + if (useScope) + { + Assert.Empty(convention1.Calls); + Assert.Empty(convention2.Calls); + scope.Dispose(); + } + + Assert.Equal(new[] { property }, convention1.Calls); + Assert.Equal(new[] { property }, convention2.Calls); + Assert.Empty(convention3.Calls); + } + + private class PropertyRemovedConvention : IPropertyRemovedConvention + { + private readonly bool _terminate; + public readonly List Calls = new List(); + + public PropertyRemovedConvention(bool terminate) + { + _terminate = terminate; + } + + public void ProcessPropertyRemoved( + IConventionEntityTypeBuilder entityTypeBuilder, + IConventionProperty property, + IConventionContext context) + { + Assert.NotNull(entityTypeBuilder.Metadata.Builder); + + Calls.Add(property); + + if (_terminate) + { + context.StopProcessing(); + } + } + } + private class Order { public static readonly PropertyInfo OrderIdProperty = typeof(Order).GetProperty(nameof(OrderId)); @@ -2784,6 +3336,7 @@ private class Order public virtual OrderDetails OrderDetails { get; set; } public virtual OrderDetails OtherOrderDetails { get; set; } + public virtual ICollection Products { get; set; } } private class SpecialOrder : Order @@ -2797,5 +3350,22 @@ private class OrderDetails public int Id { get; set; } public virtual Order Order { get; set; } } + + private class OrderProduct + { + public static readonly PropertyInfo OrderIdProperty = typeof(OrderProduct).GetProperty(nameof(OrderId)); + public static readonly PropertyInfo ProductIdProperty = typeof(OrderProduct).GetProperty(nameof(ProductId)); + + public int OrderId { get; set; } + public int ProductId { get; set; } + public virtual Order Order { get; set; } + public virtual Product Product { get; set; } + } + + private class Product + { + public int Id { get; set; } + public virtual ICollection Orders { get; set; } + } } } diff --git a/test/EFCore.Tests/Metadata/Internal/ClrCollectionAccessorFactoryTest.cs b/test/EFCore.Tests/Metadata/Internal/ClrCollectionAccessorFactoryTest.cs index 6fccdb0b852..f2154f8b675 100644 --- a/test/EFCore.Tests/Metadata/Internal/ClrCollectionAccessorFactoryTest.cs +++ b/test/EFCore.Tests/Metadata/Internal/ClrCollectionAccessorFactoryTest.cs @@ -33,6 +33,10 @@ public void Navigation_is_returned_if_it_implements_IClrCollectionAccessor() { var navigation = new FakeNavigation(); + var fk = new FakeForeignKey() { PrincipalToDependent = navigation }; + navigation.ForeignKey = fk; + navigation.PropertyInfo = MyEntity.AsICollectionProperty; + Assert.Same(navigation, new ClrCollectionAccessorFactory().Create(navigation)); } @@ -44,10 +48,10 @@ private class FakeNavigation : INavigation, IClrCollectionAccessor public string Name { get; } public ITypeBase DeclaringType { get; } public Type ClrType { get; } - public PropertyInfo PropertyInfo { get; } + public PropertyInfo PropertyInfo { get; set; } public FieldInfo FieldInfo { get; } public IEntityType DeclaringEntityType { get; } - public IForeignKey ForeignKey { get; } + public IForeignKey ForeignKey { get; set; } public bool Add(object entity, object value, bool forMaterialization) => throw new NotImplementedException(); public bool Contains(object entity, object value) => throw new NotImplementedException(); public bool Remove(object entity, object value) => throw new NotImplementedException(); @@ -56,6 +60,23 @@ private class FakeNavigation : INavigation, IClrCollectionAccessor public Type CollectionType { get; } } + private class FakeForeignKey : IForeignKey + { + public object this[string name] => throw new NotImplementedException(); + public IAnnotation FindAnnotation(string name) => throw new NotImplementedException(); + public IEnumerable GetAnnotations() => throw new NotImplementedException(); + public IEntityType DeclaringEntityType { get; } + public IReadOnlyList Properties { get; } + public IEntityType PrincipalEntityType { get; } + public IKey PrincipalKey { get; } + public INavigation DependentToPrincipal { get; set; } + public INavigation PrincipalToDependent { get; set; } + public bool IsUnique { get; } + public bool IsRequired { get; } + public bool IsOwnership { get; } + public DeleteBehavior DeleteBehavior { get; } + } + [ConditionalFact] public void Delegate_accessor_is_returned_for_IEnumerable_navigation() { @@ -427,6 +448,8 @@ private ProviderConventionSetBuilderDependencies CreateDependencies() private class MyEntity { + public static readonly PropertyInfo AsICollectionProperty = typeof(MyEntity).GetProperty(nameof(AsICollection), BindingFlags.NonPublic | BindingFlags.Instance); + private ICollection _asICollection; private ICollection _asICollectionOfEntitiesWithCustomComparer; private IList _asIList; diff --git a/test/EFCore.Tests/Metadata/Internal/EntityTypeTest.cs b/test/EFCore.Tests/Metadata/Internal/EntityTypeTest.cs index 5bfc7a91ffc..c027fe169eb 100644 --- a/test/EFCore.Tests/Metadata/Internal/EntityTypeTest.cs +++ b/test/EFCore.Tests/Metadata/Internal/EntityTypeTest.cs @@ -8,6 +8,7 @@ using System.Linq; using System.Linq.Expressions; using System.Reflection; +using JetBrains.Annotations; using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Internal; @@ -83,6 +84,8 @@ public IForeignKey FindForeignKey(IReadOnlyList properties, IKey prin public IServiceProperty FindServiceProperty(string name) => throw new NotImplementedException(); public IEnumerable GetServiceProperties() => throw new NotImplementedException(); public IEnumerable> GetSeedData() => throw new NotImplementedException(); + public ISkipNavigation FindSkipNavigation([NotNull] string name) => throw new NotImplementedException(); + public IEnumerable GetSkipNavigations() => throw new NotImplementedException(); } [ConditionalFact] @@ -419,7 +422,10 @@ public void Removing_a_key_throws_if_it_referenced_from_a_foreign_key_in_the_mod orderType.AddForeignKey(customerFk, customerKey, customerType); Assert.Equal( - CoreStrings.KeyInUse("{'" + Customer.IdProperty.Name + "'}", nameof(Customer), nameof(Order)), + CoreStrings.KeyInUse("{'" + Customer.IdProperty.Name + "'}", + nameof(Customer), + "{'" + Order.CustomerIdProperty.Name + "'}", + nameof(Order)), Assert.Throws(() => customerType.RemoveKey(customerKey.Properties)).Message); } @@ -795,6 +801,27 @@ public void Can_remove_a_foreign_key_if_it_is_referenced_from_a_navigation_in_th Assert.Empty(customerType.GetNavigations()); } + [ConditionalFact] + public void Removing_a_foreign_key_throws_if_referenced_from_skip_navigation() + { + var model = CreateModel(); + var firstEntity = model.AddEntityType(typeof(Order)); + var firstId = firstEntity.AddProperty(Order.IdProperty); + var firstKey = firstEntity.AddKey(firstId); + var secondEntity = model.AddEntityType(typeof(Product)); + var associationEntity = model.AddEntityType(typeof(OrderProduct)); + var orderIdProperty = associationEntity.AddProperty(OrderProduct.OrderIdProperty); + var foreignKey = associationEntity + .AddForeignKey(new[] { orderIdProperty }, firstKey, firstEntity); + + var navigation = firstEntity.AddSkipNavigation( + nameof(Order.Products), null, secondEntity, foreignKey, true, true); + + Assert.Equal(CoreStrings.ForeignKeyInUseSkipNavigation( + "{'" + nameof(OrderProduct.OrderId) + "'}", nameof(OrderProduct), nameof(Order.Products), nameof(Order)), + Assert.Throws(() => associationEntity.RemoveForeignKey(foreignKey)).Message); + } + [ConditionalFact] public void Foreign_keys_are_ordered_by_property_count_then_property_names() { @@ -1048,9 +1075,10 @@ public void Reference_navigation_properties_must_be_of_the_target_type() var customerForeignKey = orderType.AddForeignKey(foreignKeyProperty, customerKey, customerType); Assert.Equal( - CoreStrings.NavigationSingleWrongClrType("OrderCustomer", typeof(Order).Name, typeof(Order).Name, typeof(Customer).Name), + CoreStrings.NavigationSingleWrongClrType( + nameof(Order.RelatedOrder), typeof(Order).Name, typeof(Order).Name, typeof(Customer).Name), Assert.Throws( - () => customerForeignKey.HasDependentToPrincipal(Order.OrderCustomerProperty)).Message); + () => customerForeignKey.HasDependentToPrincipal(Order.RelatedOrderProperty)).Message); } [ConditionalFact] @@ -1149,6 +1177,295 @@ public void Navigations_are_ordered_by_name() Assert.True(new[] { navigation1, navigation2 }.SequenceEqual(((IEntityType)customerType).GetNavigations())); } + [ConditionalFact] + public void Can_add_and_remove_skip_navigation() + { + var model = CreateModel(); + var orderEntity = model.AddEntityType(typeof(Order)); + var orderIdProperty = orderEntity.AddProperty(Order.IdProperty); + var orderKey = orderEntity.AddKey(orderIdProperty); + + var customerEntity = model.AddEntityType(typeof(Customer)); + var customerIdProperty = customerEntity.AddProperty(Order.IdProperty); + var customerKey = customerEntity.AddKey(customerIdProperty); + var customerFkProperty = orderEntity.AddProperty(Order.CustomerIdProperty); + var customerForeignKey = orderEntity + .AddForeignKey(customerFkProperty, customerKey, customerEntity); + var relatedNavigation = orderEntity.AddSkipNavigation( + nameof(Order.RelatedOrder), null, orderEntity, customerForeignKey, false, false); + + var productEntity = model.AddEntityType(typeof(Product)); + var orderProductEntity = model.AddEntityType(typeof(OrderProduct)); + var orderProductFkProperty = orderProductEntity.AddProperty(OrderProduct.OrderIdProperty); + var orderProductForeignKey = orderProductEntity + .AddForeignKey(new[] { orderProductFkProperty }, orderKey, orderEntity); + + var productsNavigation = orderEntity.AddSkipNavigation( + nameof(Order.Products), null, productEntity, orderProductForeignKey, true, true); + + Assert.Equal(new[] { productsNavigation, relatedNavigation }, orderEntity.GetSkipNavigations()); + Assert.Empty(customerEntity.GetSkipNavigations()); + + Assert.Equal(new[] { relatedNavigation }, customerForeignKey.GetReferencingSkipNavigations()); + Assert.Equal(new[] { productsNavigation }, orderProductForeignKey.GetReferencingSkipNavigations()); + + orderEntity.RemoveSkipNavigation(productsNavigation); + orderEntity.RemoveSkipNavigation(relatedNavigation); + Assert.Empty(orderEntity.GetSkipNavigations()); + } + + [ConditionalFact] + public void Adding_skip_navigation_with_a_name_that_conflicts_with_another_skip_navigation_throws() + { + var model = CreateModel(); + var orderEntity = model.AddEntityType(typeof(Order)); + var orderIdProperty = orderEntity.AddProperty(Order.IdProperty); + var orderKey = orderEntity.AddKey(orderIdProperty); + var productEntity = model.AddEntityType(typeof(Product)); + var orderProductEntity = model.AddEntityType(typeof(OrderProduct)); + var orderProductFkProperty = orderProductEntity.AddProperty(OrderProduct.OrderIdProperty); + var orderProductForeignKey = orderProductEntity + .AddForeignKey(new[] { orderProductFkProperty }, orderKey, orderEntity); + + var navigation = orderEntity.AddSkipNavigation( + nameof(Order.Products), null, productEntity, orderProductForeignKey, true, true); + + Assert.Equal( + CoreStrings.ConflictingPropertyOrNavigation(nameof(Order.Products), typeof(Order).Name, typeof(Order).Name), + Assert.Throws(() => + orderEntity.AddSkipNavigation( + nameof(Order.Products), null, productEntity, orderProductForeignKey, true, true)).Message); + } + + [ConditionalFact] + public void Adding_skip_navigation_with_a_name_that_conflicts_with_a_navigation_throws() + { + var model = CreateModel(); + var orderEntity = model.AddEntityType(typeof(Order)); + var orderIdProperty = orderEntity.AddProperty(Order.IdProperty); + var orderKey = orderEntity.AddKey(orderIdProperty); + var productEntity = model.AddEntityType(typeof(Product)); + var productIdProperty = productEntity.AddProperty(Product.IdProperty); + var orderProductEntity = model.AddEntityType(typeof(OrderProduct)); + var orderProductFkProperty = orderProductEntity.AddProperty(OrderProduct.OrderIdProperty); + var orderProductForeignKey = orderProductEntity + .AddForeignKey(new[] { orderProductFkProperty }, orderKey, orderEntity); + + var customerForeignKey = productEntity.AddForeignKey(productIdProperty, orderKey, orderEntity); + + customerForeignKey.HasPrincipalToDependent(nameof(Order.Products)); + + Assert.Equal( + CoreStrings.ConflictingPropertyOrNavigation(nameof(Order.Products), typeof(Order).Name, typeof(Order).Name), + Assert.Throws(() => + orderEntity.AddSkipNavigation( + nameof(Order.Products), null, productEntity, orderProductForeignKey, true, true)).Message); + } + + [ConditionalFact] + public void Adding_skip_navigation_with_a_name_that_conflicts_with_a_property_throws() + { + var model = CreateModel(); + var orderEntity = model.AddEntityType(typeof(Order)); + var orderIdProperty = orderEntity.AddProperty(Order.IdProperty); + var orderKey = orderEntity.AddKey(orderIdProperty); + var productEntity = model.AddEntityType(typeof(Product)); + var orderProductEntity = model.AddEntityType(typeof(OrderProduct)); + var orderProductFkProperty = orderProductEntity.AddProperty(OrderProduct.OrderIdProperty); + var orderProductForeignKey = orderProductEntity + .AddForeignKey(new[] { orderProductFkProperty }, orderKey, orderEntity); + + orderEntity.AddProperty(nameof(Order.Products)); + + Assert.Equal( + CoreStrings.ConflictingPropertyOrNavigation(nameof(Order.Products), typeof(Order).Name, typeof(Order).Name), + Assert.Throws(() => + orderEntity.AddSkipNavigation( + nameof(Order.Products), null, productEntity, orderProductForeignKey, true, true)).Message); + } + + [ConditionalFact] + public void Adding_skip_navigation_with_a_name_that_conflicts_with_a_service_property_throws() + { + var model = CreateModel(); + var orderEntity = model.AddEntityType(typeof(Order)); + var orderIdProperty = orderEntity.AddProperty(Order.IdProperty); + var orderKey = orderEntity.AddKey(orderIdProperty); + var productEntity = model.AddEntityType(typeof(Product)); + var orderProductEntity = model.AddEntityType(typeof(OrderProduct)); + var orderProductFkProperty = orderProductEntity.AddProperty(OrderProduct.OrderIdProperty); + var orderProductForeignKey = orderProductEntity + .AddForeignKey(new[] { orderProductFkProperty }, orderKey, orderEntity); + + orderEntity.AddServiceProperty(Order.ProductsProperty); + + Assert.Equal( + CoreStrings.ConflictingPropertyOrNavigation(nameof(Order.Products), typeof(Order).Name, typeof(Order).Name), + Assert.Throws(() => + orderEntity.AddSkipNavigation( + nameof(Order.Products), null, productEntity, orderProductForeignKey, true, true)).Message); + } + + [ConditionalFact] + public void Adding_CLR_skip_navigation_to_shadow_entity_type_throws() + { + var model = CreateModel(); + var orderEntity = model.AddEntityType(nameof(Order)); + var orderIdProperty = orderEntity.AddProperty(nameof(Order.Id), typeof(int)); + var orderKey = orderEntity.AddKey(orderIdProperty); + var productEntity = model.AddEntityType(nameof(Product)); + var orderProductEntity = model.AddEntityType(nameof(OrderProduct)); + var orderProductFkProperty = orderProductEntity.AddProperty(nameof(OrderProduct.OrderId), typeof(int)); + var orderProductForeignKey = orderProductEntity + .AddForeignKey(new[] { orderProductFkProperty }, orderKey, orderEntity); + + Assert.Equal( + CoreStrings.ClrPropertyOnShadowEntity(nameof(Order.Products), nameof(Order)), + Assert.Throws( + () => orderEntity.AddSkipNavigation( + nameof(Order.Products), Order.ProductsProperty, productEntity, orderProductForeignKey, true, true)).Message); + } + + [ConditionalFact] + public void Adding_CLR_skip_navigation_targetting_a_shadow_entity_type_throws() + { + var model = CreateModel(); + var orderEntity = model.AddEntityType(typeof(Order)); + var orderIdProperty = orderEntity.AddProperty(nameof(Order.Id)); + var orderKey = orderEntity.AddKey(orderIdProperty); + var productEntity = model.AddEntityType(nameof(Product)); + var orderProductEntity = model.AddEntityType(nameof(OrderProduct)); + var orderProductFkProperty = orderProductEntity.AddProperty(nameof(OrderProduct.OrderId), typeof(int)); + var orderProductForeignKey = orderProductEntity + .AddForeignKey(new[] { orderProductFkProperty }, orderKey, orderEntity); + + Assert.Equal( + CoreStrings.NavigationToShadowEntity(nameof(Order.Products), nameof(Order), nameof(Product)), + Assert.Throws( + () => orderEntity.AddSkipNavigation( + nameof(Order.Products), Order.ProductsProperty, productEntity, orderProductForeignKey, true, true)).Message); + } + + [ConditionalFact] + public void Adding_CLR_skip_navigation_to_a_mismatched_entity_type_throws() + { + var model = CreateModel(); + var orderEntity = model.AddEntityType(typeof(Order)); + var orderIdProperty = orderEntity.AddProperty(Order.IdProperty); + var orderKey = orderEntity.AddKey(orderIdProperty); + var productEntity = model.AddEntityType(typeof(Product)); + var orderProductEntity = model.AddEntityType(typeof(OrderProduct)); + var orderProductFkProperty = orderProductEntity.AddProperty(nameof(OrderProduct.OrderId), typeof(int)); + var orderProductForeignKey = orderProductEntity + .AddForeignKey(new[] { orderProductFkProperty }, orderKey, orderEntity); + + Assert.Equal( + CoreStrings.NoClrNavigation(nameof(Order.Products), nameof(Product)), + Assert.Throws( + () => productEntity.AddSkipNavigation( + nameof(Order.Products), Order.ProductsProperty, productEntity, orderProductForeignKey, true, true)).Message); + } + + [ConditionalFact] + public void Adding_CLR_collection_skip_navigation_with_mismatched_target_entity_type_throws() + { + var model = CreateModel(); + var orderEntity = model.AddEntityType(typeof(Order)); + var orderIdProperty = orderEntity.AddProperty(Order.IdProperty); + var orderKey = orderEntity.AddKey(orderIdProperty); + var productEntity = model.AddEntityType(typeof(Product)); + var orderProductEntity = model.AddEntityType(typeof(OrderProduct)); + var orderProductFkProperty = orderProductEntity.AddProperty(nameof(OrderProduct.OrderId), typeof(int)); + var orderProductForeignKey = orderProductEntity + .AddForeignKey(new[] { orderProductFkProperty }, orderKey, orderEntity); + + Assert.Equal( + CoreStrings.NavigationCollectionWrongClrType(nameof(Order.Products), nameof(Order), "ICollection", nameof(Order)), + Assert.Throws( + () => orderEntity.AddSkipNavigation( + nameof(Order.Products), Order.ProductsProperty, orderEntity, orderProductForeignKey, true, true)).Message); + } + + [ConditionalFact] + public void Adding_CLR_reference_skip_navigation_with_mismatched_target_entity_type_throws() + { + var model = CreateModel(); + var orderEntity = model.AddEntityType(typeof(Order)); + var orderIdProperty = orderEntity.AddProperty(Order.IdProperty); + var orderKey = orderEntity.AddKey(orderIdProperty); + var productEntity = model.AddEntityType(typeof(Product)); + var orderProductEntity = model.AddEntityType(typeof(OrderProduct)); + var orderProductFkProperty = orderProductEntity.AddProperty(nameof(OrderProduct.OrderId), typeof(int)); + var orderProductForeignKey = orderProductEntity + .AddForeignKey(new[] { orderProductFkProperty }, orderKey, orderEntity); + + Assert.Equal( + CoreStrings.NavigationSingleWrongClrType(nameof(Order.Products), nameof(Order), "ICollection", nameof(Order)), + Assert.Throws( + () => orderEntity.AddSkipNavigation( + nameof(Order.Products), Order.ProductsProperty, orderEntity, orderProductForeignKey, false, true)).Message); + } + + [ConditionalFact] + public void Adding_skip_navigation_with_a_mismatched_memberinfo_throws() + { + var model = CreateModel(); + var orderEntity = model.AddEntityType(typeof(Order)); + var orderIdProperty = orderEntity.AddProperty(Order.IdProperty); + var orderKey = orderEntity.AddKey(orderIdProperty); + var productEntity = model.AddEntityType(typeof(Product)); + var orderProductEntity = model.AddEntityType(typeof(OrderProduct)); + var orderProductFkProperty = orderProductEntity.AddProperty(OrderProduct.OrderIdProperty); + var orderProductForeignKey = orderProductEntity + .AddForeignKey(new[] { orderProductFkProperty }, orderKey, orderEntity); + + Assert.Equal( + CoreStrings.PropertyWrongName(nameof(Order.Products), typeof(Order).Name, nameof(Order.RelatedOrder)), + Assert.Throws(() => + orderEntity.AddSkipNavigation( + nameof(Order.Products), Order.RelatedOrderProperty, productEntity, orderProductForeignKey, true, true)).Message); + } + + [ConditionalFact] + public void Adding_dependent_skip_navigation_with_mismatched_foreign_key_throws() + { + var model = CreateModel(); + var orderEntity = model.AddEntityType(typeof(Order)); + var orderIdProperty = orderEntity.AddProperty(Order.IdProperty); + var orderKey = orderEntity.AddKey(orderIdProperty); + var productEntity = model.AddEntityType(typeof(Product)); + var orderProductEntity = model.AddEntityType(typeof(OrderProduct)); + var orderProductFkProperty = orderProductEntity.AddProperty(nameof(OrderProduct.OrderId), typeof(int)); + var orderProductForeignKey = orderProductEntity.AddForeignKey(orderProductFkProperty, orderKey, orderEntity); + + Assert.Equal( + CoreStrings.SkipNavigationWrongDependentType( + nameof(Order.Products), nameof(Order), nameof(OrderProduct), "{'" + nameof(OrderProduct.OrderId) + "'}"), + Assert.Throws( + () => orderEntity.AddSkipNavigation( + nameof(Order.Products), Order.ProductsProperty, productEntity, orderProductForeignKey, true, false)).Message); + } + + [ConditionalFact] + public void Adding_principal_skip_navigation_with_mismatched_foreign_key_throws() + { + var model = CreateModel(); + var orderEntity = model.AddEntityType(typeof(Order)); + var orderIdProperty = orderEntity.AddProperty(Order.IdProperty); + var orderKey = orderEntity.AddKey(orderIdProperty); + var productEntity = model.AddEntityType(typeof(Product)); + var orderProductEntity = model.AddEntityType(typeof(OrderProduct)); + var orderProductFkProperty = orderProductEntity.AddProperty(nameof(OrderProduct.OrderId), typeof(int)); + var orderProductForeignKey = orderProductEntity.AddForeignKey(orderProductFkProperty, orderKey, orderEntity); + + Assert.Equal( + CoreStrings.SkipNavigationWrongPrincipalType( + nameof(OrderProduct.Order), nameof(OrderProduct), nameof(Order), "{'" + nameof(OrderProduct.OrderId) + "'}"), + Assert.Throws( + () => orderProductEntity.AddSkipNavigation( + nameof(OrderProduct.Order), null, orderEntity, orderProductForeignKey, false, true)).Message); + } + [ConditionalFact] public void Can_add_retrieve_and_remove_indexes() { @@ -1359,8 +1676,7 @@ public void AddProperty_throws_if_shadow_entity_type() Assert.Equal( CoreStrings.ClrPropertyOnShadowEntity(nameof(Customer.Name), "Customer"), Assert.Throws( - () => - entityType.AddProperty(Customer.NameProperty)).Message); + () => entityType.AddProperty(Customer.NameProperty)).Message); } [ConditionalFact] @@ -1477,7 +1793,7 @@ public void Properties_are_ordered_by_name() var property2 = entityType.AddProperty(Customer.NameProperty); var property1 = entityType.AddProperty(Customer.IdProperty); - Assert.True(new[] { property1, property2 }.SequenceEqual(entityType.GetProperties())); + Assert.Equal(new[] { property1, property2 }, entityType.GetProperties()); } [ConditionalFact] @@ -1491,7 +1807,7 @@ public void Primary_key_properties_precede_others() entityType.SetPrimaryKey(pkProperty); - Assert.True(new[] { pkProperty, aProperty }.SequenceEqual(entityType.GetProperties())); + Assert.Equal(new[] { pkProperty, aProperty }, entityType.GetProperties()); } [ConditionalFact] @@ -1506,7 +1822,7 @@ public void Composite_primary_key_properties_are_listed_in_key_order() entityType.SetPrimaryKey(new[] { pkProperty1, pkProperty2 }); - Assert.True(new[] { pkProperty1, pkProperty2, aProperty }.SequenceEqual(entityType.GetProperties())); + Assert.Equal(new[] { pkProperty1, pkProperty2, aProperty }, entityType.GetProperties()); } [ConditionalFact] @@ -1523,7 +1839,7 @@ public void Properties_on_base_type_are_listed_before_derived_properties() var property3 = childType.AddProperty("A", typeof(int)); childType.BaseType = parentType; - Assert.True(new[] { property1, property2, property3, property4 }.SequenceEqual(childType.GetProperties())); + Assert.Equal(new[] { property1, property2, property3, property4 }, childType.GetProperties()); } [ConditionalFact] @@ -1537,11 +1853,11 @@ public void Properties_are_properly_ordered_when_primary_key_changes() entityType.SetPrimaryKey(bProperty); - Assert.True(new[] { bProperty, aProperty }.SequenceEqual(entityType.GetProperties())); + Assert.Equal(new[] { bProperty, aProperty }, entityType.GetProperties()); entityType.SetPrimaryKey(aProperty); - Assert.True(new[] { aProperty, bProperty }.SequenceEqual(entityType.GetProperties())); + Assert.Equal(new[] { aProperty, bProperty }, entityType.GetProperties()); } [ConditionalFact] @@ -1682,25 +1998,14 @@ public void Adding_a_new_service_property_with_a_type_that_already_exists_throws } [ConditionalFact] - public void Adding_a_CLR_property_from_wrong_CLR_type_throws() - { - var model = CreateModel(); - var entityType = model.AddEntityType(typeof(Customer)); - - Assert.Equal( - CoreStrings.PropertyWrongEntityClrType(Order.CustomerIdProperty.Name, typeof(Customer).Name, typeof(Order).Name), - Assert.Throws(() => entityType.AddProperty(Order.CustomerIdProperty)).Message); - } - - [ConditionalFact] - public void Adding_a_CLR_property_to_shadow_type_throws() + public void Adding_a_CLR_service_property_to_shadow_type_throws() { var model = CreateModel(); var entityType = model.AddEntityType(typeof(Customer).Name); Assert.Equal( CoreStrings.ClrPropertyOnShadowEntity(Order.CustomerIdProperty.Name, typeof(Customer).Name), - Assert.Throws(() => entityType.AddProperty(Order.CustomerIdProperty)).Message); + Assert.Throws(() => entityType.AddServiceProperty(Order.CustomerIdProperty)).Message); } [ConditionalFact] @@ -2508,13 +2813,15 @@ private class Order : BaseType public static readonly PropertyInfo CustomerProperty = typeof(Order).GetProperty(nameof(Customer)); public static readonly PropertyInfo CustomerIdProperty = typeof(Order).GetProperty(nameof(CustomerId)); public static readonly PropertyInfo CustomerUniqueProperty = typeof(Order).GetProperty(nameof(CustomerUnique)); - public static readonly PropertyInfo OrderCustomerProperty = typeof(Order).GetProperty(nameof(OrderCustomer)); + public static readonly PropertyInfo RelatedOrderProperty = typeof(Order).GetProperty(nameof(RelatedOrder)); + public static readonly PropertyInfo ProductsProperty = typeof(Order).GetProperty(nameof(Products)); public int CustomerId { get; set; } public Guid CustomerUnique { get; set; } public Customer Customer { get; set; } - public Order OrderCustomer { get; set; } + public Order RelatedOrder { get; set; } + public virtual ICollection Products { get; set; } } private class SpecialOrder : Order @@ -2528,6 +2835,25 @@ private class VerySpecialOrder : SpecialOrder { } + private class OrderProduct + { + public static readonly PropertyInfo OrderIdProperty = typeof(OrderProduct).GetProperty(nameof(OrderId)); + public static readonly PropertyInfo ProductIdProperty = typeof(OrderProduct).GetProperty(nameof(ProductId)); + + public int OrderId { get; set; } + public int ProductId { get; set; } + public virtual Order Order { get; set; } + public virtual Product Product { get; set; } + } + + private class Product + { + public static readonly PropertyInfo IdProperty = typeof(Product).GetProperty(nameof(Id)); + + public int Id { get; set; } + public virtual ICollection Orders { get; set; } + } + private static IMutableModel BuildFullNotificationEntityModel() { var builder = InMemoryTestHelpers.Instance.CreateConventionBuilder(); diff --git a/test/EFCore.Tests/Metadata/Internal/IndexTest.cs b/test/EFCore.Tests/Metadata/Internal/IndexTest.cs index 38cbafb5f1e..7b9afbf19ec 100644 --- a/test/EFCore.Tests/Metadata/Internal/IndexTest.cs +++ b/test/EFCore.Tests/Metadata/Internal/IndexTest.cs @@ -12,7 +12,7 @@ namespace Microsoft.EntityFrameworkCore.Metadata.Internal public class IndexTest { [ConditionalFact] - public void Can_create_index_from_properties() + public void Gets_expected_default_values() { var entityType = ((IConventionModel)CreateModel()).AddEntityType(typeof(Customer)); var property1 = entityType.AddProperty(Customer.IdProperty); @@ -26,7 +26,7 @@ public void Can_create_index_from_properties() } [ConditionalFact] - public void Can_create_unique_index_from_properties() + public void Can_set_unique() { var entityType = CreateModel().AddEntityType(typeof(Customer)); var property1 = entityType.AddProperty(Customer.IdProperty); @@ -39,19 +39,6 @@ public void Can_create_unique_index_from_properties() Assert.True(index.IsUnique); } - [ConditionalFact] - public void Constructor_validates_properties_from_same_entity() - { - var model = CreateModel(); - var property1 = model.AddEntityType(typeof(Customer)).AddProperty(Customer.IdProperty); - var property2 = model.AddEntityType(typeof(Order)).AddProperty(Order.IdProperty); - - Assert.Equal( - CoreStrings.IndexPropertiesWrongEntity($"{{'{property1.Name}', '{property2.Name}'}}", typeof(Customer).Name), - Assert.Throws( - () => property1.DeclaringEntityType.AddIndex(new[] { property1, property2 })).Message); - } - private static IMutableModel CreateModel() => new Model(); private class Customer diff --git a/test/EFCore.Tests/Metadata/Internal/SkipNavigationTest.cs b/test/EFCore.Tests/Metadata/Internal/SkipNavigationTest.cs new file mode 100644 index 00000000000..04c88b7bed4 --- /dev/null +++ b/test/EFCore.Tests/Metadata/Internal/SkipNavigationTest.cs @@ -0,0 +1,171 @@ +// 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.Reflection; +using Microsoft.EntityFrameworkCore.Diagnostics; +using Xunit; + +namespace Microsoft.EntityFrameworkCore.Metadata.Internal +{ + public class SkipNavigationTest + { + [ConditionalFact] + public void Gets_expected_default_values() + { + var model = (IConventionModel)CreateModel(); + var firstEntity = model.AddEntityType(typeof(Order)); + var firstIdProperty = firstEntity.AddProperty(Order.IdProperty); + var firstKey = firstEntity.AddKey(firstIdProperty); + var secondEntity = model.AddEntityType(typeof(Product)); + var associationEntityBuilder = model.AddEntityType(typeof(OrderProduct)); + var orderIdProperty = associationEntityBuilder.AddProperty(OrderProduct.OrderIdProperty); + var firstFk = associationEntityBuilder + .AddForeignKey(new[] { orderIdProperty }, firstKey, firstEntity); + + var navigation = firstEntity.AddSkipNavigation( + nameof(Order.Products), null, secondEntity, firstFk, true, true); + + Assert.True(navigation.IsCollection); + Assert.True(navigation.IsOnPrincipal); + Assert.False(navigation.IsEagerLoaded); + Assert.Null(navigation.Inverse); + Assert.Equal(firstFk, navigation.ForeignKey); + Assert.Equal(nameof(Order.Products), navigation.Name); + Assert.Null(navigation.FieldInfo); + Assert.NotNull(navigation.PropertyInfo); + Assert.Equal(ConfigurationSource.Convention, navigation.GetConfigurationSource()); + } + + [ConditionalFact] + public void Can_set_inverse() + { + var model = CreateModel(); + var orderEntity = model.AddEntityType(typeof(Order)); + var orderIdProperty = orderEntity.AddProperty(Order.IdProperty); + var orderKey = orderEntity.AddKey(orderIdProperty); + var productEntity = model.AddEntityType(typeof(Product)); + var productIdProperty = productEntity.AddProperty(Product.IdProperty); + var productKey = productEntity.AddKey(productIdProperty); + var orderProductEntity = model.AddEntityType(typeof(OrderProduct)); + var orderProductFkProperty = orderProductEntity.AddProperty(OrderProduct.OrderIdProperty); + var orderProductForeignKey = orderProductEntity + .AddForeignKey(new[] { orderProductFkProperty }, orderKey, orderEntity); + var productOrderFkProperty = orderProductEntity.AddProperty(OrderProduct.ProductIdProperty); + var productOrderForeignKey = orderProductEntity + .AddForeignKey(new[] { productOrderFkProperty }, productKey, productEntity); + + var productsNavigation = orderEntity.AddSkipNavigation( + nameof(Order.Products), null, productEntity, orderProductForeignKey, true, true); + + var ordersNavigation = productEntity.AddSkipNavigation( + nameof(Product.Orders), null, orderEntity, productOrderForeignKey, true, true); + + productsNavigation.SetInverse(ordersNavigation); + ordersNavigation.SetInverse(productsNavigation); + + Assert.Same(ordersNavigation, productsNavigation.Inverse); + Assert.Same(productsNavigation, ordersNavigation.Inverse); + Assert.Equal(ConfigurationSource.Explicit, ((IConventionSkipNavigation)productsNavigation).GetConfigurationSource()); + Assert.Equal(ConfigurationSource.Explicit, ((IConventionSkipNavigation)ordersNavigation).GetConfigurationSource()); + Assert.Equal(ConfigurationSource.Explicit, ((IConventionSkipNavigation)productsNavigation).GetInverseConfigurationSource()); + Assert.Equal(ConfigurationSource.Explicit, ((IConventionSkipNavigation)ordersNavigation).GetInverseConfigurationSource()); + + productsNavigation.SetInverse(null); + ordersNavigation.SetInverse(null); + + Assert.Null(((IConventionSkipNavigation)productsNavigation).GetInverseConfigurationSource()); + Assert.Null(((IConventionSkipNavigation)ordersNavigation).GetInverseConfigurationSource()); + } + + [ConditionalFact] + public void Setting_inverse_targetting_wrong_type_throws() + { + var model = CreateModel(); + var orderEntity = model.AddEntityType(typeof(Order)); + var orderIdProperty = orderEntity.AddProperty(Order.IdProperty); + var orderKey = orderEntity.AddKey(orderIdProperty); + var productEntity = model.AddEntityType(typeof(Product)); + var productIdProperty = productEntity.AddProperty(Product.IdProperty); + var productKey = productEntity.AddKey(productIdProperty); + var orderProductEntity = model.AddEntityType(typeof(OrderProduct)); + var orderProductFkProperty = orderProductEntity.AddProperty(OrderProduct.OrderIdProperty); + var orderProductForeignKey = orderProductEntity + .AddForeignKey(new[] { orderProductFkProperty }, orderKey, orderEntity); + var productOrderFkProperty = orderProductEntity.AddProperty(OrderProduct.ProductIdProperty); + var productOrderForeignKey = orderProductEntity + .AddForeignKey(new[] { productOrderFkProperty }, productKey, productEntity); + + var productsNavigation = orderEntity.AddSkipNavigation( + nameof(Order.Products), null, productEntity, orderProductForeignKey, true, true); + + var ordersNavigation = orderProductEntity.AddSkipNavigation( + nameof(OrderProduct.Product), null, productEntity, productOrderForeignKey, false, false); + + Assert.Equal(CoreStrings.SkipNavigationWrongInverse( + nameof(OrderProduct.Product), nameof(OrderProduct), nameof(Order.Products), nameof(Product)), + Assert.Throws(() => productsNavigation.SetInverse(ordersNavigation)).Message); + } + + [ConditionalFact] + public void Setting_inverse_with_wrong_association_type_throws() + { + var model = CreateModel(); + var orderEntity = model.AddEntityType(typeof(Order)); + var orderIdProperty = orderEntity.AddProperty(Order.IdProperty); + var orderKey = orderEntity.AddKey(orderIdProperty); + var productEntity = model.AddEntityType(typeof(Product)); + var productIdProperty = productEntity.AddProperty(Product.IdProperty); + var productKey = productEntity.AddKey(productIdProperty); + var orderProductEntity = model.AddEntityType(typeof(OrderProduct)); + var orderProductFkProperty = orderProductEntity.AddProperty(OrderProduct.OrderIdProperty); + var orderProductForeignKey = orderProductEntity + .AddForeignKey(new[] { orderProductFkProperty }, orderKey, orderEntity); + var productFkProperty = productEntity.AddProperty("Fk", typeof(int)); + var productOrderForeignKey = productEntity + .AddForeignKey(new[] { productFkProperty }, productKey, productEntity); + + var productsNavigation = orderEntity.AddSkipNavigation( + nameof(Order.Products), null, productEntity, orderProductForeignKey, true, true); + + var ordersNavigation = productEntity.AddSkipNavigation( + nameof(Product.Orders), null, orderEntity, productOrderForeignKey, true, true); + + Assert.Equal(CoreStrings.SkipInverseMismatchedAssociationType( + nameof(Product.Orders), nameof(Product), nameof(Order.Products), nameof(OrderProduct)), + Assert.Throws(() => productsNavigation.SetInverse(ordersNavigation)).Message); + } + + private static IMutableModel CreateModel() => new Model(); + + private class Order + { + public static readonly PropertyInfo IdProperty = typeof(Order).GetProperty(nameof(Id)); + + public int Id { get; set; } + + public virtual ICollection Products { get; set; } + } + + private class OrderProduct + { + public static readonly PropertyInfo OrderIdProperty = typeof(OrderProduct).GetProperty(nameof(OrderId)); + public static readonly PropertyInfo ProductIdProperty = typeof(OrderProduct).GetProperty(nameof(ProductId)); + + public int OrderId { get; set; } + public int ProductId { get; set; } + public virtual Order Order { get; set; } + public virtual Product Product { get; set; } + } + + private class Product + { + public static readonly PropertyInfo IdProperty = typeof(Product).GetProperty(nameof(Id)); + + public int Id { get; set; } + + public virtual ICollection Orders { get; set; } + } + } +}