diff --git a/src/EFCore.Relational/Extensions/PropertiesConfigurationBuilderExtensions.cs b/src/EFCore.Relational/Extensions/PropertiesConfigurationBuilderExtensions.cs new file mode 100644 index 00000000000..49223aab30a --- /dev/null +++ b/src/EFCore.Relational/Extensions/PropertiesConfigurationBuilderExtensions.cs @@ -0,0 +1,106 @@ +// 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; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Microsoft.EntityFrameworkCore.Utilities; + +// ReSharper disable once CheckNamespace +namespace Microsoft.EntityFrameworkCore +{ + /// + /// Relational database specific extension methods for . + /// + public static class PropertiesConfigurationBuilderExtensions + { + /// + /// Configures the data type of the column that the property maps to when targeting a relational database. + /// This should be the complete type name, including precision, scale, length, etc. + /// + /// The builder for the property being configured. + /// The name of the data type of the column. + /// The same builder instance so that multiple calls can be chained. + public static PropertiesConfigurationBuilder HaveColumnType( + this PropertiesConfigurationBuilder propertyBuilder, + string typeName) + { + Check.NotNull(propertyBuilder, nameof(propertyBuilder)); + Check.NotEmpty(typeName, nameof(typeName)); + + propertyBuilder.HaveAnnotation(RelationalAnnotationNames.ColumnType, typeName); + + return propertyBuilder; + } + + /// + /// Configures the data type of the column that the property maps to when targeting a relational database. + /// This should be the complete type name, including precision, scale, length, etc. + /// + /// The type of the property being configured. + /// The builder for the property being configured. + /// The name of the data type of the column. + /// The same builder instance so that multiple calls can be chained. + public static PropertiesConfigurationBuilder HaveColumnType( + this PropertiesConfigurationBuilder propertyBuilder, + string typeName) + => (PropertiesConfigurationBuilder)HaveColumnType((PropertiesConfigurationBuilder)propertyBuilder, typeName); + + /// + /// Configures the property as capable of storing only fixed-length data, such as strings. + /// + /// The builder for the property being configured. + /// A value indicating whether the property is constrained to fixed length values. + /// The same builder instance so that multiple configuration calls can be chained. + public static PropertiesConfigurationBuilder AreFixedLength( + this PropertiesConfigurationBuilder propertyBuilder, + bool fixedLength = true) + { + Check.NotNull(propertyBuilder, nameof(propertyBuilder)); + + propertyBuilder.HaveAnnotation(RelationalAnnotationNames.IsFixedLength, fixedLength); + + return propertyBuilder; + } + + /// + /// Configures the property as capable of storing only fixed-length data, such as strings. + /// + /// The type of the property being configured. + /// The builder for the property being configured. + /// A value indicating whether the property is constrained to fixed length values. + /// The same builder instance so that multiple configuration calls can be chained. + public static PropertiesConfigurationBuilder AreFixedLength( + this PropertiesConfigurationBuilder propertyBuilder, + bool fixedLength = true) + => (PropertiesConfigurationBuilder)AreFixedLength((PropertiesConfigurationBuilder)propertyBuilder, fixedLength); + + /// + /// Configures the property to use the given collation. The database column will be created with the given + /// collation, and it will be used implicitly in all collation-sensitive operations. + /// + /// The builder for the property being configured. + /// The collation for the column. + /// The same builder instance so that multiple calls can be chained. + public static PropertiesConfigurationBuilder UseCollation(this PropertiesConfigurationBuilder propertyBuilder, string collation) + { + Check.NotNull(propertyBuilder, nameof(propertyBuilder)); + Check.NotEmpty(collation, nameof(collation)); + + propertyBuilder.HaveAnnotation(RelationalAnnotationNames.Collation, collation); + + return propertyBuilder; + } + + /// + /// Configures the property to use the given collation. The database column will be created with the given + /// collation, and it will be used implicitly in all collation-sensitive operations. + /// + /// The builder for the property being configured. + /// The collation for the column. + /// The same builder instance so that multiple calls can be chained. + public static PropertiesConfigurationBuilder UseCollation( + this PropertiesConfigurationBuilder propertyBuilder, + string collation) + => (PropertiesConfigurationBuilder)UseCollation((PropertiesConfigurationBuilder)propertyBuilder, collation); + } +} diff --git a/src/EFCore/Infrastructure/Internal/MemberInfoNameComparer.cs b/src/EFCore/Infrastructure/Internal/MemberInfoNameComparer.cs new file mode 100644 index 00000000000..7ff9c6301df --- /dev/null +++ b/src/EFCore/Infrastructure/Internal/MemberInfoNameComparer.cs @@ -0,0 +1,57 @@ +// 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; + +namespace Microsoft.EntityFrameworkCore.Infrastructure.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. + /// + // Sealed for perf + public sealed class MemberInfoNameComparer : IComparer + { + /// + /// 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 MemberInfoNameComparer Instance = new(); + + private MemberInfoNameComparer() + { + } + + /// + /// 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 int Compare(MemberInfo? x, MemberInfo? y) + { + if (ReferenceEquals(x, y)) + { + return 0; + } + + if (x is null) + { + return -1; + } + + if (y is null) + { + return 1; + } + + return StringComparer.Ordinal.Compare(x.Name, y.Name); + } + } +} diff --git a/src/EFCore/Infrastructure/ModelValidator.cs b/src/EFCore/Infrastructure/ModelValidator.cs index f1da4b2ae74..8324b755e4e 100644 --- a/src/EFCore/Infrastructure/ModelValidator.cs +++ b/src/EFCore/Infrastructure/ModelValidator.cs @@ -158,7 +158,8 @@ protected virtual void ValidatePropertyMapping( { throw new InvalidOperationException( CoreStrings.PropertyNotMapped( - entityType.DisplayName(), unmappedProperty.Name, unmappedProperty.ClrType.ShortDisplayName())); + entityType.DisplayName(), unmappedProperty.Name, + (unmappedProperty.GetValueConverter()?.ProviderClrType ?? unmappedProperty.ClrType).ShortDisplayName())); } if (entityType.ClrType == Model.DefaultPropertyBagType) @@ -175,45 +176,52 @@ protected virtual void ValidatePropertyMapping( .Where(pi => pi.IsCandidateProperty(needsWrite: false)) .Select(pi => pi.GetSimpleMemberName())); - 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.ExceptWith( + ((IEnumerable)entityType.GetProperties()) + .Concat(entityType.GetNavigations()) + .Concat(entityType.GetSkipNavigations()) + .Concat(entityType.GetServiceProperties()).Select(p => p.Name)); if (entityType.IsPropertyBag) { clrProperties.ExceptWith(_dictionaryProperties); } - clrProperties.RemoveWhere(p => entityType.FindIgnoredConfigurationSource(p) != null); - if (clrProperties.Count <= 0) { continue; } - foreach (var clrProperty in clrProperties) + var configuration = ((Model)entityType.Model).Configuration; + foreach (var clrPropertyName in clrProperties) { - var actualProperty = runtimeProperties[clrProperty]; - var propertyType = actualProperty.PropertyType; + if (entityType.FindIgnoredConfigurationSource(clrPropertyName) != null) + { + continue; + } + + var clrProperty = runtimeProperties[clrPropertyName]; + var propertyType = clrProperty.PropertyType; var targetSequenceType = propertyType.TryGetSequenceType(); if (conventionModel.FindIgnoredConfigurationSource(propertyType) != null - || targetSequenceType != null - && conventionModel.FindIgnoredConfigurationSource(targetSequenceType) != null) + || conventionModel.IsIgnoredType(propertyType) + || (targetSequenceType != null + && (conventionModel.FindIgnoredConfigurationSource(targetSequenceType) != null + || conventionModel.IsIgnoredType(targetSequenceType)))) { continue; } - var targetType = FindCandidateNavigationPropertyType(actualProperty); + Dependencies.MemberClassifier.GetNavigationCandidates(entityType).TryGetValue(clrProperty, out var targetType); if (targetType == null - || targetSequenceType == null) + || targetSequenceType == null) { - if (actualProperty.FindSetterProperty() == null) + if (clrProperty.FindSetterProperty() == null) { continue; } - var sharedType = actualProperty.GetMemberType(); + var sharedType = clrProperty.GetMemberType(); if (conventionModel.IsShared(sharedType)) { targetType = sharedType; @@ -234,7 +242,7 @@ protected virtual void ValidatePropertyMapping( if ((!entityType.IsKeyless || targetSequenceType == null) && entityType.GetDerivedTypes().All( - dt => dt.GetDeclaredNavigations().FirstOrDefault(n => n.Name == actualProperty.GetSimpleMemberName()) + dt => dt.GetDeclaredNavigations().FirstOrDefault(n => n.Name == clrProperty.GetSimpleMemberName()) == null) && (!isTargetSharedOrOwned || (!targetType.Equals(entityType.ClrType) @@ -247,18 +255,18 @@ protected virtual void ValidatePropertyMapping( { throw new InvalidOperationException( CoreStrings.AmbiguousOwnedNavigation( - entityType.DisplayName() + "." + actualProperty.Name, targetType.ShortDisplayName())); + entityType.DisplayName() + "." + clrProperty.Name, targetType.ShortDisplayName())); } if (model.IsShared(targetType)) { throw new InvalidOperationException( - CoreStrings.NonConfiguredNavigationToSharedType(actualProperty.Name, entityType.DisplayName())); + CoreStrings.NonConfiguredNavigationToSharedType(clrProperty.Name, entityType.DisplayName())); } throw new InvalidOperationException( CoreStrings.NavigationNotAdded( - entityType.DisplayName(), actualProperty.Name, propertyType.ShortDisplayName())); + entityType.DisplayName(), clrProperty.Name, propertyType.ShortDisplayName())); } // ReSharper restore CheckForReferenceEqualityInstead.3 @@ -269,21 +277,18 @@ protected virtual void ValidatePropertyMapping( { throw new InvalidOperationException( CoreStrings.InterfacePropertyNotAdded( - entityType.DisplayName(), actualProperty.Name, propertyType.ShortDisplayName())); + entityType.DisplayName(), clrProperty.Name, propertyType.ShortDisplayName())); } else { throw new InvalidOperationException( CoreStrings.PropertyNotAdded( - entityType.DisplayName(), actualProperty.Name, propertyType.ShortDisplayName())); + entityType.DisplayName(), clrProperty.Name, propertyType.ShortDisplayName())); } } } } - private Type? FindCandidateNavigationPropertyType(PropertyInfo propertyInfo) - => Dependencies.MemberClassifier.FindCandidateNavigationPropertyType(propertyInfo); - /// /// Validates that no attempt is made to ignore inherited properties. /// diff --git a/src/EFCore/Metadata/Builders/IConventionEntityTypeBuilder.cs b/src/EFCore/Metadata/Builders/IConventionEntityTypeBuilder.cs index 6ed57fcc2cd..cf13faf79f4 100644 --- a/src/EFCore/Metadata/Builders/IConventionEntityTypeBuilder.cs +++ b/src/EFCore/Metadata/Builders/IConventionEntityTypeBuilder.cs @@ -75,6 +75,54 @@ public interface IConventionEntityTypeBuilder : IConventionAnnotatableBuilder /// IConventionPropertyBuilder? Property(MemberInfo memberInfo, bool fromDataAnnotation = false); + /// + /// Returns a value indicating whether the given property can be added to this entity type. + /// + /// The type of value the property will hold. + /// The name of the property to be configured. + /// Indicates whether the configuration was specified using a data annotation. + /// if the property can be added. + bool CanHaveProperty( + Type? propertyType, + string propertyName, + bool fromDataAnnotation = false); + + /// + /// Returns a value indicating whether the given property can be added to this entity type. + /// + /// The or of the property. + /// Indicates whether the configuration was specified using a data annotation. + /// if the property can be added. + bool CanHaveProperty(MemberInfo memberInfo, bool fromDataAnnotation = false); + + /// + /// Returns an object that can be used to configure the indexer property with the given name. + /// If no matching property exists, then a new property will be added. + /// + /// The type of value the property will hold. + /// The name of the property to be configured. + /// Indicates whether the configuration was specified using a data annotation. + /// + /// An object that can be used to configure the property if it exists on the entity type, + /// otherwise. + /// + IConventionPropertyBuilder? IndexerProperty( + Type propertyType, + string propertyName, + bool fromDataAnnotation = false); + + /// + /// Returns a value indicating whether the given indexer property can be added to this entity type. + /// + /// The type of value the property will hold. + /// The name of the property to be configured. + /// Indicates whether the configuration was specified using a data annotation. + /// if the property can be added. + bool CanHaveIndexerProperty( + Type propertyType, + string propertyName, + bool fromDataAnnotation = false); + /// /// Creates a property with a name that's different from any existing properties. /// @@ -138,6 +186,14 @@ IConventionEntityTypeBuilder RemoveUnusedShadowProperties( MemberInfo memberInfo, bool fromDataAnnotation = false); + /// + /// Returns a value indicating whether the given service property can be added to this entity type. + /// + /// The or of the property. + /// Indicates whether the configuration was specified using a data annotation. + /// if the service property can be added. + bool CanHaveServiceProperty(MemberInfo memberInfo, bool fromDataAnnotation = false); + /// /// Indicates whether the given member name is ignored for the given configuration source. /// @@ -624,16 +680,54 @@ bool CanAddNavigation(string navigationName, bool fromDataAnnotation = false) /// The name of the navigation. /// Indicates whether the configuration was specified using a data annotation. /// if the configuration can be applied. + [Obsolete("Use CanHaveNavigation with Type parameter")] bool CanHaveNavigation(string navigationName, bool fromDataAnnotation = false); + /// + /// Returns a value indicating whether the given navigation can be added to this entity type. + /// + /// The name of the navigation. + /// The type of the navigation target. + /// Indicates whether the configuration was specified using a data annotation. + /// if the skip navigation can be added. + bool CanHaveNavigation(string navigationName, Type? type, bool fromDataAnnotation = false); + + /// + /// Returns a value indicating whether the given navigation can be added to this entity type. + /// + /// The navigation member. + /// Indicates whether the configuration was specified using a data annotation. + /// if the configuration can be applied. + bool CanHaveNavigation(MemberInfo navigation, bool fromDataAnnotation = false) + => CanHaveNavigation(navigation.Name, navigation.GetMemberType(), fromDataAnnotation); + /// /// Returns a value indicating whether the given skip navigation can be added to this entity type. /// /// The name of the skip navigation. /// Indicates whether the configuration was specified using a data annotation. - /// if the configuration can be applied. + /// if the skip navigation can be added. + [Obsolete("Use CanHaveSkipNavigation with Type parameter")] bool CanHaveSkipNavigation(string skipNavigationName, bool fromDataAnnotation = false); + /// + /// Returns a value indicating whether the given skip navigation can be added to this entity type. + /// + /// The name of the skip navigation. + /// The type of the navigation target. + /// Indicates whether the configuration was specified using a data annotation. + /// if the skip navigation can be added. + bool CanHaveSkipNavigation(string skipNavigationName, Type? type, bool fromDataAnnotation = false); + + /// + /// Returns a value indicating whether the given skip navigation can be added to this entity type. + /// + /// The navigation member. + /// Indicates whether the configuration was specified using a data annotation. + /// if the skip navigation can be added. + bool CanHaveSkipNavigation(MemberInfo navigation, bool fromDataAnnotation = false) + => CanHaveSkipNavigation(navigation.Name, navigation.GetMemberType(), fromDataAnnotation); + /// /// Configures a skip navigation and the inverse between this and the target entity type. /// diff --git a/src/EFCore/Metadata/Builders/PropertiesConfigurationBuilder.cs b/src/EFCore/Metadata/Builders/PropertiesConfigurationBuilder.cs new file mode 100644 index 00000000000..0c730609abc --- /dev/null +++ b/src/EFCore/Metadata/Builders/PropertiesConfigurationBuilder.cs @@ -0,0 +1,201 @@ +// 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.ComponentModel; +using Microsoft.EntityFrameworkCore.ChangeTracking; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata.Internal; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Microsoft.EntityFrameworkCore.Utilities; + +namespace Microsoft.EntityFrameworkCore.Metadata.Builders +{ + /// + /// + /// Provides a simple API surface for setting property defaults before conventions run. + /// + /// + /// Instances of this class are returned from methods when using the API + /// and it is not designed to be directly constructed in your application code. + /// + /// + public class PropertiesConfigurationBuilder + { + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + [EntityFrameworkInternal] + public PropertiesConfigurationBuilder(PropertyConfiguration property) + { + Check.NotNull(property, nameof(property)); + + Property = 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. + /// + [EntityFrameworkInternal] + protected virtual PropertyConfiguration Property { get; } + + /// + /// Adds or updates an annotation on the property. + /// + /// The key of the annotation to be added or updated. + /// The value to be stored in the annotation. + /// The same builder instance so that multiple configuration calls can be chained. + public virtual PropertiesConfigurationBuilder HaveAnnotation(string annotation, object value) + { + Check.NotEmpty(annotation, nameof(annotation)); + + Property[annotation] = value; + + return this; + } + + /// + /// Configures the maximum length of data that can be stored in this property. + /// Maximum length can only be set on array properties (including properties). + /// + /// The maximum length of data allowed in the property. + /// The same builder instance so that multiple configuration calls can be chained. + public virtual PropertiesConfigurationBuilder HaveMaxLength(int maxLength) + { + Property.SetMaxLength(maxLength); + + return this; + } + + /// + /// Configures the precision and scale of the property. + /// + /// The precision of the property. + /// The scale of the property. + /// The same builder instance so that multiple configuration calls can be chained. + public virtual PropertiesConfigurationBuilder HavePrecision(int precision, int scale) + { + Property.SetPrecision(precision); + Property.SetScale(scale); + + return this; + } + + /// + /// + /// Configures the precision of the property. + /// + /// + /// The precision of the property. + /// The same builder instance so that multiple configuration calls can be chained. + public virtual PropertiesConfigurationBuilder HavePrecision(int precision) + { + Property.SetPrecision(precision); + + return this; + } + + /// + /// Configures whether the property as capable of persisting unicode characters. + /// Can only be set on properties. + /// + /// A value indicating whether the property can contain unicode characters. + /// The same builder instance so that multiple configuration calls can be chained. + public virtual PropertiesConfigurationBuilder AreUnicode(bool unicode = true) + { + Property.SetIsUnicode(unicode); + + return this; + } + + /// + /// Configures the property so that the property value is converted to the given type before + /// writing to the database and converted back when reading from the database. + /// + /// The type to convert to and from. + /// The same builder instance so that multiple configuration calls can be chained. + public virtual PropertiesConfigurationBuilder HaveConversion() + => HaveConversion(typeof(TProvider)); + + /// + /// Configures the property so that the property value is converted to the given type before + /// writing to the database and converted back when reading from the database. + /// + /// The type to convert to and from. + /// The same builder instance so that multiple configuration calls can be chained. + public virtual PropertiesConfigurationBuilder HaveConversion(Type providerClrType) + { + Check.NotNull(providerClrType, nameof(providerClrType)); + + Property.SetProviderClrType(providerClrType); + + return this; + } + + /// + /// Configures the property so that the property value is converted to and from the database + /// using the given . + /// + /// A type that derives from . + /// A type that derives from . + /// The same builder instance so that multiple configuration calls can be chained. + public virtual PropertiesConfigurationBuilder HaveConversion() + where TConverter : ValueConverter + where TComparer : ValueComparer + => HaveConversion(typeof(TConverter), typeof(TComparer)); + + /// + /// Configures the property so that the property value is converted to and from the database + /// using the given . + /// + /// A type that derives from . + /// A type that derives from . + /// The same builder instance so that multiple configuration calls can be chained. + public virtual PropertiesConfigurationBuilder HaveConversion(Type converterType, Type? comparerType) + { + Check.NotNull(converterType, nameof(converterType)); + + Property.SetValueConverter(converterType); + Property.SetValueComparer(comparerType); + + return this; + } + + #region Hidden System.Object members + + /// + /// Returns a string that represents the current object. + /// + /// A string that represents the current object. + [EditorBrowsable(EditorBrowsableState.Never)] + public override string? ToString() + => base.ToString(); + + /// + /// Determines whether the specified object is equal to the current object. + /// + /// The object to compare with the current object. + /// if the specified object is equal to the current object; otherwise, . + [EditorBrowsable(EditorBrowsableState.Never)] + // ReSharper disable once BaseObjectEqualsIsObjectEquals + public override bool Equals(object? obj) + => base.Equals(obj); + + /// + /// Serves as the default hash function. + /// + /// A hash code for the current object. + [EditorBrowsable(EditorBrowsableState.Never)] + // ReSharper disable once BaseObjectGetHashCodeCallInGetHashCode + public override int GetHashCode() + => base.GetHashCode(); + + #endregion + } +} diff --git a/src/EFCore/Metadata/Builders/PropertiesConfigurationBuilder`.cs b/src/EFCore/Metadata/Builders/PropertiesConfigurationBuilder`.cs new file mode 100644 index 00000000000..9c448fa83a6 --- /dev/null +++ b/src/EFCore/Metadata/Builders/PropertiesConfigurationBuilder`.cs @@ -0,0 +1,122 @@ +// 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 Microsoft.EntityFrameworkCore.ChangeTracking; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata.Internal; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +namespace Microsoft.EntityFrameworkCore.Metadata.Builders +{ + /// + /// + /// Provides a simple API surface for setting property defaults before conventions run. + /// + /// + /// Instances of this class are returned from methods when using the API + /// and it is not designed to be directly constructed in your application code. + /// + /// + public class PropertiesConfigurationBuilder : PropertiesConfigurationBuilder + { + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + [EntityFrameworkInternal] + public PropertiesConfigurationBuilder(PropertyConfiguration property) + : base(property) + { + } + + /// + /// Adds or updates an annotation on the property. If an annotation with the key specified in + /// already exists its value will be updated. + /// + /// The key of the annotation to be added or updated. + /// The value to be stored in the annotation. + /// The same builder instance so that multiple configuration calls can be chained. + public new virtual PropertiesConfigurationBuilder HaveAnnotation(string annotation, object value) + => (PropertiesConfigurationBuilder)base.HaveAnnotation(annotation, value); + + /// + /// Configures the maximum length of data that can be stored in this property. + /// Maximum length can only be set on array properties (including properties). + /// + /// The maximum length of data allowed in the property. + /// The same builder instance so that multiple configuration calls can be chained. + public new virtual PropertiesConfigurationBuilder HaveMaxLength(int maxLength) + => (PropertiesConfigurationBuilder)base.HaveMaxLength(maxLength); + + /// + /// Configures the precision and scale of the property. + /// + /// The precision of the property. + /// The scale of the property. + /// The same builder instance so that multiple configuration calls can be chained. + public new virtual PropertiesConfigurationBuilder HavePrecision(int precision, int scale) + => (PropertiesConfigurationBuilder)base.HavePrecision(precision, scale); + + /// + /// + /// Configures the precision of the property. + /// + /// + /// The precision of the property. + /// The same builder instance so that multiple configuration calls can be chained. + public new virtual PropertiesConfigurationBuilder HavePrecision(int precision) + => (PropertiesConfigurationBuilder)base.HavePrecision(precision); + + /// + /// Configures the property as capable of persisting unicode characters. + /// Can only be set on properties. + /// + /// A value indicating whether the property can contain unicode characters. + /// The same builder instance so that multiple configuration calls can be chained. + public new virtual PropertiesConfigurationBuilder AreUnicode(bool unicode = true) + => (PropertiesConfigurationBuilder)base.AreUnicode(unicode); + + /// + /// Configures the property so that the property value is converted to the given type before + /// writing to the database and converted back when reading from the database. + /// + /// The type to convert to and from. + /// The same builder instance so that multiple configuration calls can be chained. + public new virtual PropertiesConfigurationBuilder HaveConversion() + => (PropertiesConfigurationBuilder)base.HaveConversion(); + + /// + /// Configures the property so that the property value is converted to the given type before + /// writing to the database and converted back when reading from the database. + /// + /// The type to convert to and from. + /// The same builder instance so that multiple configuration calls can be chained. + public new virtual PropertiesConfigurationBuilder HaveConversion(Type providerClrType) + => (PropertiesConfigurationBuilder)base.HaveConversion(providerClrType); + + /// + /// Configures the property so that the property value is converted to and from the database + /// using the given . + /// + /// A type that derives from . + /// A type that derives from . + /// The same builder instance so that multiple configuration calls can be chained. + public new virtual PropertiesConfigurationBuilder HaveConversion() + where TConverter : ValueConverter + where TComparer : ValueComparer + => (PropertiesConfigurationBuilder)base.HaveConversion(); + + /// + /// Configures the property so that the property value is converted to and from the database + /// using the given . + /// + /// A type that derives from . + /// A type that derives from . + /// The same builder instance so that multiple configuration calls can be chained. + public new virtual PropertiesConfigurationBuilder HaveConversion(Type converterType, Type? comparerType) + => (PropertiesConfigurationBuilder)base.HaveConversion(converterType, comparerType); + } +} diff --git a/src/EFCore/Metadata/Builders/PropertyBuilder.cs b/src/EFCore/Metadata/Builders/PropertyBuilder.cs index d6815d84100..1d1571f8fbc 100644 --- a/src/EFCore/Metadata/Builders/PropertyBuilder.cs +++ b/src/EFCore/Metadata/Builders/PropertyBuilder.cs @@ -491,8 +491,10 @@ public virtual PropertyBuilder HasConversion(ValueComparer? valueComp /// The type to convert to and from. /// The comparer to use for values before conversion. /// The same builder instance so that multiple configuration calls can be chained. - public virtual PropertyBuilder HasConversion(Type? providerClrType, ValueComparer? valueComparer) + public virtual PropertyBuilder HasConversion(Type providerClrType, ValueComparer? valueComparer) { + Check.NotNull(providerClrType, nameof(providerClrType)); + Builder.HasConversion(providerClrType, ConfigurationSource.Explicit); Builder.HasValueComparer(valueComparer, ConfigurationSource.Explicit); @@ -533,8 +535,10 @@ public virtual PropertyBuilder HasConversion() /// A type that derives from . /// A type that derives from . /// The same builder instance so that multiple configuration calls can be chained. - public virtual PropertyBuilder HasConversion(Type? converterType, Type? comparerType) + public virtual PropertyBuilder HasConversion(Type converterType, Type? comparerType) { + Check.NotNull(converterType, nameof(converterType)); + Builder.HasConverter(converterType, ConfigurationSource.Explicit); Builder.HasValueComparer(comparerType, ConfigurationSource.Explicit); diff --git a/src/EFCore/Metadata/Builders/PropertyBuilder`.cs b/src/EFCore/Metadata/Builders/PropertyBuilder`.cs index 7f4557dbf09..9e5b97e14bf 100644 --- a/src/EFCore/Metadata/Builders/PropertyBuilder`.cs +++ b/src/EFCore/Metadata/Builders/PropertyBuilder`.cs @@ -410,7 +410,7 @@ public virtual PropertyBuilder HasConversion(ValueConverte /// The comparer to use for values before conversion. /// The same builder instance so that multiple configuration calls can be chained. public new virtual PropertyBuilder HasConversion( - Type? providerClrType, + Type providerClrType, ValueComparer? valueComparer) => (PropertyBuilder)base.HasConversion(providerClrType, valueComparer); @@ -477,7 +477,7 @@ public virtual PropertyBuilder HasConversion( /// A type that derives from . /// A type that derives from . /// The same builder instance so that multiple configuration calls can be chained. - public new virtual PropertyBuilder HasConversion(Type? converterType, Type? comparerType) + public new virtual PropertyBuilder HasConversion(Type converterType, Type? comparerType) => (PropertyBuilder)base.HasConversion(converterType, comparerType); } } diff --git a/src/EFCore/Metadata/Conventions/ForeignKeyAttributeConvention.cs b/src/EFCore/Metadata/Conventions/ForeignKeyAttributeConvention.cs index a3008b1675c..541c8258b82 100644 --- a/src/EFCore/Metadata/Conventions/ForeignKeyAttributeConvention.cs +++ b/src/EFCore/Metadata/Conventions/ForeignKeyAttributeConvention.cs @@ -332,18 +332,16 @@ var fkPropertiesOnDependentToPrincipal foreach (var memberInfo in entityType.GetRuntimeProperties().Values.Cast() .Concat(entityType.GetRuntimeFields().Values)) { - if (entityType.Builder.IsIgnored(memberInfo.GetSimpleMemberName()) - || !Attribute.IsDefined(memberInfo, typeof(ForeignKeyAttribute), inherit: true)) + if (!Attribute.IsDefined(memberInfo, typeof(ForeignKeyAttribute), inherit: true) + || !entityType.Builder.CanHaveProperty(memberInfo, fromDataAnnotation: true)) { continue; } var attribute = memberInfo.GetCustomAttribute(inherit: true)!; - if (attribute.Name != navigationName || (memberInfo is PropertyInfo propertyInfo - && (FindCandidateNavigationPropertyType(propertyInfo) != null - || IsNavigationToSharedType(entityType.Model, propertyInfo)))) + && IsNavigationCandidate(propertyInfo, entityType))) { continue; } @@ -372,13 +370,8 @@ var fkPropertiesOnDependentToPrincipal return candidateProperty; } - private Type? FindCandidateNavigationPropertyType(PropertyInfo propertyInfo) - => Dependencies.MemberClassifier.FindCandidateNavigationPropertyType(propertyInfo); - - private bool IsNavigationToSharedType(IConventionModel model, PropertyInfo propertyInfo) - => model.IsShared(propertyInfo.PropertyType) - || (propertyInfo.PropertyType.TryGetSequenceType() is Type elementType - && model.IsShared(elementType)); + private bool IsNavigationCandidate(PropertyInfo propertyInfo, IConventionEntityType entityType) + => Dependencies.MemberClassifier.GetNavigationCandidates(entityType).TryGetValue(propertyInfo, out var _); private static IReadOnlyList? FindCandidateDependentPropertiesThroughNavigation( IConventionForeignKeyBuilder relationshipBuilder, diff --git a/src/EFCore/Metadata/Conventions/InversePropertyAttributeConvention.cs b/src/EFCore/Metadata/Conventions/InversePropertyAttributeConvention.cs index d66bc3cd80c..78a5be4ae80 100644 --- a/src/EFCore/Metadata/Conventions/InversePropertyAttributeConvention.cs +++ b/src/EFCore/Metadata/Conventions/InversePropertyAttributeConvention.cs @@ -54,12 +54,7 @@ private void Process( Type targetClrType, InversePropertyAttribute attribute) { - var entityType = (EntityType)entityTypeBuilder.Metadata; - var navigationName = navigationMemberInfo.GetSimpleMemberName(); - if (entityTypeBuilder.IsIgnored(navigationName, fromDataAnnotation: true) - || entityType.FindPropertiesInHierarchy(navigationName).Cast() - .Concat(entityType.FindServicePropertiesInHierarchy(navigationName)) - .Any(m => !ConfigurationSource.DataAnnotation.Overrides(m.GetConfigurationSource()))) + if (!entityTypeBuilder.CanHaveNavigation(navigationMemberInfo, fromDataAnnotation: true)) { return; } @@ -87,7 +82,7 @@ private void Process( .FirstOrDefault(p => string.Equals(p.GetSimpleMemberName(), attribute.Property, StringComparison.OrdinalIgnoreCase)); if (inverseNavigationPropertyInfo == null - || !Dependencies.MemberClassifier.FindCandidateNavigationPropertyType(inverseNavigationPropertyInfo)! + || !Dependencies.MemberClassifier.GetNavigationCandidates(targetEntityTypeBuilder.Metadata)[inverseNavigationPropertyInfo] .IsAssignableFrom(entityType.ClrType)) { throw new InvalidOperationException( @@ -306,10 +301,11 @@ public override void ProcessEntityTypeRemoved( return; } + var navigationName = navigationMemberInfo.GetSimpleMemberName(); var leastDerivedEntityTypes = modelBuilder.Metadata.FindLeastDerivedEntityTypes(declaringType); foreach (var leastDerivedEntityType in leastDerivedEntityTypes) { - if (leastDerivedEntityType.Builder.IsIgnored(navigationMemberInfo.GetSimpleMemberName(), fromDataAnnotation: true)) + if (leastDerivedEntityType.Builder.IsIgnored(navigationName, fromDataAnnotation: true)) { continue; } diff --git a/src/EFCore/Metadata/Conventions/NavigationAttributeConventionBase.cs b/src/EFCore/Metadata/Conventions/NavigationAttributeConventionBase.cs index 7c9e92a262a..d8105873114 100644 --- a/src/EFCore/Metadata/Conventions/NavigationAttributeConventionBase.cs +++ b/src/EFCore/Metadata/Conventions/NavigationAttributeConventionBase.cs @@ -83,7 +83,7 @@ public virtual void ProcessEntityTypeIgnored( var navigations = new List<(PropertyInfo, Type)>(); foreach (var navigationPropertyInfo in type.GetRuntimeProperties()) { - var targetClrType = FindCandidateNavigationWithAttributePropertyType(navigationPropertyInfo); + var targetClrType = FindCandidateNavigationWithAttributePropertyType(navigationPropertyInfo, modelBuilder.Metadata); if (targetClrType == null) { continue; @@ -183,7 +183,7 @@ public virtual void ProcessEntityTypeBaseTypeChanged( var navigations = new List<(PropertyInfo, Type)>(); foreach (var navigationPropertyInfo in entityType.GetRuntimeProperties().Values) { - var targetClrType = FindCandidateNavigationWithAttributePropertyType(navigationPropertyInfo); + var targetClrType = FindCandidateNavigationWithAttributePropertyType(navigationPropertyInfo, entityType); if (targetClrType == null) { continue; @@ -267,7 +267,7 @@ public virtual void ProcessEntityTypeMemberIgnored( return; } - var targetClrType = FindCandidateNavigationWithAttributePropertyType(navigationPropertyInfo); + var targetClrType = FindCandidateNavigationWithAttributePropertyType(navigationPropertyInfo, entityTypeBuilder.Metadata); if (targetClrType == null) { return; @@ -284,15 +284,23 @@ public virtual void ProcessEntityTypeMemberIgnored( } } - private Type? FindCandidateNavigationWithAttributePropertyType(PropertyInfo propertyInfo) + private Type? FindCandidateNavigationWithAttributePropertyType(PropertyInfo propertyInfo, IConventionModel model) { - var targetClrType = Dependencies.MemberClassifier.FindCandidateNavigationPropertyType(propertyInfo); - return targetClrType == null - || !Attribute.IsDefined(propertyInfo, typeof(TAttribute), inherit: true) - ? null - : targetClrType; + var targetClrType = Dependencies.MemberClassifier.FindCandidateNavigationPropertyType( + propertyInfo, ((Model)model).Configuration); + return targetClrType != null + && Attribute.IsDefined(propertyInfo, typeof(TAttribute), inherit: true) + ? targetClrType + : null; } + private Type? FindCandidateNavigationWithAttributePropertyType(PropertyInfo propertyInfo, IConventionEntityType entityType) + => Dependencies.MemberClassifier.GetNavigationCandidates(entityType) + .TryGetValue(propertyInfo, out var targetClrType) + && Attribute.IsDefined(propertyInfo, typeof(TAttribute), inherit: true) + ? targetClrType + : null; + /// /// Returns the attributes applied to the given navigation. /// diff --git a/src/EFCore/Metadata/Conventions/PropertyDiscoveryConvention.cs b/src/EFCore/Metadata/Conventions/PropertyDiscoveryConvention.cs index 0b516a55b43..43ed2505483 100644 --- a/src/EFCore/Metadata/Conventions/PropertyDiscoveryConvention.cs +++ b/src/EFCore/Metadata/Conventions/PropertyDiscoveryConvention.cs @@ -11,7 +11,9 @@ namespace Microsoft.EntityFrameworkCore.Metadata.Conventions /// /// A convention that adds properties to entity types corresponding to scalar public properties on the CLR type. /// - public class PropertyDiscoveryConvention : IEntityTypeAddedConvention, IEntityTypeBaseTypeChangedConvention + public class PropertyDiscoveryConvention : + IEntityTypeAddedConvention, + IEntityTypeBaseTypeChangedConvention { /// /// Creates a new instance of . @@ -62,22 +64,17 @@ public virtual void ProcessEntityTypeBaseTypeChanged( private void Process(IConventionEntityTypeBuilder entityTypeBuilder) { - foreach (var propertyInfo in entityTypeBuilder.Metadata.GetRuntimeProperties().Values) + var entityType = entityTypeBuilder.Metadata; + var configuration = ((Model)entityType.Model).Configuration; + foreach (var propertyInfo in entityType.GetRuntimeProperties().Values) { - if (IsCandidatePrimitiveProperty(propertyInfo)) + if (!Dependencies.MemberClassifier.IsCandidatePrimitiveProperty(propertyInfo, configuration)) { - entityTypeBuilder.Property(propertyInfo); + continue; } + + entityTypeBuilder.Property(propertyInfo); } } - - /// - /// Returns a value indicating whether the given CLR property should be mapped as an entity type property. - /// - /// The property. - /// if the property should be mapped. - protected virtual bool IsCandidatePrimitiveProperty(PropertyInfo propertyInfo) - => propertyInfo.IsCandidateProperty() - && Dependencies.TypeMappingSource.FindMapping(propertyInfo) != null; } } diff --git a/src/EFCore/Metadata/Conventions/RelationshipDiscoveryConvention.cs b/src/EFCore/Metadata/Conventions/RelationshipDiscoveryConvention.cs index d514f0444da..94a531e34ab 100644 --- a/src/EFCore/Metadata/Conventions/RelationshipDiscoveryConvention.cs +++ b/src/EFCore/Metadata/Conventions/RelationshipDiscoveryConvention.cs @@ -10,6 +10,7 @@ using JetBrains.Annotations; using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Infrastructure.Internal; using Microsoft.EntityFrameworkCore.Metadata.Builders; using Microsoft.EntityFrameworkCore.Metadata.Conventions.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata.Internal; @@ -46,11 +47,6 @@ public RelationshipDiscoveryConvention(ProviderConventionSetBuilderDependencies private void DiscoverRelationships(IConventionEntityTypeBuilder entityTypeBuilder, IConventionContext context) { - if (entityTypeBuilder.ModelBuilder.IsIgnored(entityTypeBuilder.Metadata.ClrType)) - { - return; - } - var relationshipCandidates = FindRelationshipCandidates(entityTypeBuilder); relationshipCandidates = RemoveIncompatibleWithExistingRelationships(relationshipCandidates, entityTypeBuilder); relationshipCandidates = RemoveInheritedInverseNavigations(relationshipCandidates); @@ -74,7 +70,7 @@ private IReadOnlyList FindRelationshipCandidates(IConvent return relationshipCandidates.Values.ToList(); } - foreach (var candidateTuple in GetNavigationCandidates(entityType)) + foreach (var candidateTuple in Dependencies.MemberClassifier.GetNavigationCandidates(entityType)) { var navigationPropertyInfo = candidateTuple.Key; var targetClrType = candidateTuple.Value; @@ -147,7 +143,7 @@ private IReadOnlyList FindRelationshipCandidates(IConvent if (!entityType.IsKeyless) { - var inverseCandidates = GetNavigationCandidates(candidateTargetEntityType); + var inverseCandidates = Dependencies.MemberClassifier.GetNavigationCandidates(candidateTargetEntityType); foreach (var inverseCandidateTuple in inverseCandidates) { var inversePropertyInfo = inverseCandidateTuple.Key; @@ -618,10 +614,10 @@ private void CreateRelationships( continue; } - var targetOwned = (!entityType.IsInOwnershipPath(targetEntityType) + var targetOwned = !entityType.IsInOwnershipPath(targetEntityType) && (targetEntityType.Model.IsOwned(targetEntityType.ClrType) || (targetEntityType.HasSharedClrType - && targetEntityType.Model.FindEntityTypes(targetEntityType.ClrType).Any(e => e.IsOwned())))); + && targetEntityType.Model.FindEntityTypes(targetEntityType.ClrType).Any(e => e.IsOwned()))); var inverse = relationshipCandidate.InverseProperties.SingleOrDefault(); if (inverse == null) @@ -967,37 +963,6 @@ public virtual void ProcessForeignKeyOwnershipChanged( IConventionContext context) => DiscoverRelationships(relationshipBuilder.Metadata.DeclaringEntityType.Builder, context); - private Type? FindCandidateNavigationPropertyType(PropertyInfo propertyInfo) - => Dependencies.MemberClassifier.FindCandidateNavigationPropertyType(propertyInfo); - - private ImmutableSortedDictionary GetNavigationCandidates(IConventionEntityType entityType) - { - if (entityType.FindAnnotation(CoreAnnotationNames.NavigationCandidates)?.Value - is ImmutableSortedDictionary navigationCandidates) - { - return navigationCandidates; - } - - var dictionaryBuilder = ImmutableSortedDictionary.CreateBuilder(MemberInfoNameComparer.Instance); - foreach (var propertyInfo in entityType.GetRuntimeProperties().Values.OrderBy(p => p.Name)) - { - var targetType = FindCandidateNavigationPropertyType(propertyInfo); - if (targetType != null) - { - dictionaryBuilder[propertyInfo] = targetType; - } - } - - navigationCandidates = dictionaryBuilder.ToImmutable(); - SetNavigationCandidates(entityType.Builder, navigationCandidates); - return navigationCandidates; - } - - private static void SetNavigationCandidates( - IConventionEntityTypeBuilder entityTypeBuilder, - ImmutableSortedDictionary navigationCandidates) - => entityTypeBuilder.HasAnnotation(CoreAnnotationNames.NavigationCandidates, navigationCandidates); - private static bool IsImplicitlyCreatedUnusedSharedType(IConventionEntityType entityType) => entityType.HasSharedClrType && entityType.GetConfigurationSource() == ConfigurationSource.Convention @@ -1097,35 +1062,6 @@ private static void SetAmbiguousNavigations( ImmutableSortedDictionary ambiguousNavigations) => entityTypeBuilder.HasAnnotation(CoreAnnotationNames.AmbiguousNavigations, ambiguousNavigations); - private sealed class MemberInfoNameComparer : IComparer - { - public static readonly MemberInfoNameComparer Instance = new(); - - private MemberInfoNameComparer() - { - } - - public int Compare(MemberInfo? x, MemberInfo? y) - { - if (ReferenceEquals(x, y)) - { - return 0; - } - - if (x is null) - { - return -1; - } - - if (y is null) - { - return 1; - } - - return StringComparer.Ordinal.Compare(x.Name, y.Name); - } - } - [DebuggerDisplay("{DebuggerDisplay(),nq}")] private sealed class RelationshipCandidate { diff --git a/src/EFCore/Metadata/Conventions/ServicePropertyDiscoveryConvention.cs b/src/EFCore/Metadata/Conventions/ServicePropertyDiscoveryConvention.cs index a2593a01b3e..690fbc3840c 100644 --- a/src/EFCore/Metadata/Conventions/ServicePropertyDiscoveryConvention.cs +++ b/src/EFCore/Metadata/Conventions/ServicePropertyDiscoveryConvention.cs @@ -61,29 +61,22 @@ public virtual void ProcessEntityTypeBaseTypeChanged( private void Process(IConventionEntityTypeBuilder entityTypeBuilder) { var entityType = entityTypeBuilder.Metadata; - var candidates = entityType.GetRuntimeProperties().Values; - - foreach (var propertyInfo in candidates) + var configuration = ((Model)entityType.Model).Configuration; + foreach (var propertyInfo in entityType.GetRuntimeProperties().Values) { - var name = propertyInfo.GetSimpleMemberName(); - if (entityTypeBuilder.IsIgnored(name) - || entityType.FindProperty(propertyInfo) != null - || entityType.FindNavigation(propertyInfo) != null - || !propertyInfo.IsCandidateProperty(publicOnly: false) - || (propertyInfo.IsCandidateProperty() - && Dependencies.TypeMappingSource.FindMapping(propertyInfo) != null)) + if (!entityTypeBuilder.CanHaveServiceProperty(propertyInfo)) { continue; } - var factory = Dependencies.ParameterBindingFactories.FindFactory(propertyInfo.PropertyType, name); + var factory = Dependencies.MemberClassifier.FindServicePropertyCandidateBindingFactory(propertyInfo, configuration); if (factory == null) { continue; } entityTypeBuilder.ServiceProperty(propertyInfo)?.HasParameterBinding( - (ServiceParameterBinding)factory.Bind(entityType, propertyInfo.PropertyType, name)); + (ServiceParameterBinding)factory.Bind(entityType, propertyInfo.PropertyType, propertyInfo.GetSimpleMemberName())); } } } diff --git a/src/EFCore/Metadata/IConventionModel.cs b/src/EFCore/Metadata/IConventionModel.cs index 1bdd7e817e4..1314cb6e158 100644 --- a/src/EFCore/Metadata/IConventionModel.cs +++ b/src/EFCore/Metadata/IConventionModel.cs @@ -333,8 +333,7 @@ public interface IConventionModel : IReadOnlyModel, IConventionAnnotatable /// /// The name of the entity type that could be ignored. /// if the given entity type name is ignored. - bool IsIgnored(string typeName) - => FindIgnoredConfigurationSource(typeName) != null; + bool IsIgnored(string typeName); /// /// Indicates whether the given entity type is ignored. @@ -343,6 +342,14 @@ bool IsIgnored(string typeName) /// if the given entity type is ignored. bool IsIgnored(Type type); + /// + /// Indicates whether entity types and properties with the given type should be ignored. + /// This configuration is independent from + /// + /// The entity type that might be ignored. + /// if the given entity type is ignored. + bool IsIgnoredType(Type type); + /// /// Indicates whether the given entity type name is ignored. /// diff --git a/src/EFCore/Metadata/Internal/EntityType.cs b/src/EFCore/Metadata/Internal/EntityType.cs index ade139c5dec..04be8d4d8bc 100644 --- a/src/EFCore/Metadata/Internal/EntityType.cs +++ b/src/EFCore/Metadata/Internal/EntityType.cs @@ -2374,6 +2374,16 @@ public virtual IEnumerable GetIndexes() _properties.Add(property.Name, property); + if (Model.Configuration != null) + { + using (Model.ConventionDispatcher.DelayConventions()) + { + Model.ConventionDispatcher.OnPropertyAdded(property.Builder); + Model.Configuration.ConfigureProperty(property); + return property; + } + } + return (Property?)Model.ConventionDispatcher.OnPropertyAdded(property.Builder)?.Metadata; } diff --git a/src/EFCore/Metadata/Internal/IMemberClassifier.cs b/src/EFCore/Metadata/Internal/IMemberClassifier.cs index b06f2827cdb..e52acba52ef 100644 --- a/src/EFCore/Metadata/Internal/IMemberClassifier.cs +++ b/src/EFCore/Metadata/Internal/IMemberClassifier.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Collections.Immutable; using System.Reflection; using Microsoft.Extensions.DependencyInjection; @@ -28,6 +29,30 @@ public interface IMemberClassifier /// 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. /// - Type? FindCandidateNavigationPropertyType(MemberInfo memberInfo); + ImmutableSortedDictionary GetNavigationCandidates(IConventionEntityType entityType); + + /// + /// 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. + /// + Type? FindCandidateNavigationPropertyType(MemberInfo memberInfo, ModelConfiguration? configuration); + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + bool IsCandidatePrimitiveProperty(PropertyInfo propertyInfo, ModelConfiguration? configuration); + + /// + /// 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. + /// + IParameterBindingFactory? FindServicePropertyCandidateBindingFactory(PropertyInfo propertyInfo, ModelConfiguration? configuration); } } diff --git a/src/EFCore/Metadata/Internal/InternalEntityTypeBuilder.cs b/src/EFCore/Metadata/Internal/InternalEntityTypeBuilder.cs index ae6aef950af..d4fb2d720aa 100644 --- a/src/EFCore/Metadata/Internal/InternalEntityTypeBuilder.cs +++ b/src/EFCore/Metadata/Internal/InternalEntityTypeBuilder.cs @@ -554,49 +554,14 @@ public virtual bool CanRemoveKey(ConfigurationSource configurationSource) } else { - if (!configurationSource.HasValue - || IsIgnored(propertyName, configurationSource)) + if (configurationSource != ConfigurationSource.Explicit + && (!configurationSource.HasValue + || !CanAddProperty(propertyType ?? memberInfo?.GetMemberType(), propertyName, configurationSource.Value))) { return null; } - foreach (var conflictingServiceProperty in Metadata.FindServicePropertiesInHierarchy(propertyName)) - { - if (!configurationSource.Overrides(conflictingServiceProperty.GetConfigurationSource())) - { - return null; - } - } - - foreach (var conflictingNavigation in Metadata.FindNavigationsInHierarchy(propertyName)) - { - var foreignKey = conflictingNavigation.ForeignKey; - - var navigationConfigurationSource = conflictingNavigation.GetConfigurationSource(); - if (!configurationSource.Overrides(navigationConfigurationSource)) - { - return null; - } - - if (navigationConfigurationSource == ConfigurationSource.Explicit) - { - throw new InvalidOperationException( - CoreStrings.PropertyCalledOnNavigation(propertyName, Metadata.DisplayName())); - } - } - - foreach (var conflictingSkipNavigation in Metadata.FindSkipNavigationsInHierarchy(propertyName)) - { - if (!configurationSource.Overrides(conflictingSkipNavigation.GetConfigurationSource())) - { - return null; - } - } - - if (memberInfo == null) - { - memberInfo = Metadata.ClrType.GetMembersInHierarchy(propertyName).FirstOrDefault(); - } + memberInfo ??= Metadata.ClrType.GetMembersInHierarchy(propertyName).FirstOrDefault(); if (propertyType == null) { @@ -608,13 +573,6 @@ public virtual bool CanRemoveKey(ConfigurationSource configurationSource) propertyType = memberInfo.GetMemberType(); typeConfigurationSource = ConfigurationSource.Explicit; } - else if (memberInfo != null - && propertyType != memberInfo.GetMemberType() - && memberInfo != Metadata.FindIndexerPropertyInfo() - && typeConfigurationSource != null) - { - return null; - } foreach (var derivedType in Metadata.GetDerivedTypes()) { @@ -654,7 +612,8 @@ public virtual bool CanRemoveKey(ConfigurationSource configurationSource) { if (conflictingNavigation.GetConfigurationSource() == ConfigurationSource.Explicit) { - continue; + throw new InvalidOperationException( + CoreStrings.PropertyCalledOnNavigation(propertyName, Metadata.DisplayName())); } var foreignKey = conflictingNavigation.ForeignKey; @@ -702,6 +661,45 @@ public virtual bool CanRemoveKey(ConfigurationSource configurationSource) : Metadata.FindProperty(propertyName)?.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 virtual bool CanHaveProperty( + Type? propertyType, + string propertyName, + MemberInfo? memberInfo, + ConfigurationSource? typeConfigurationSource, + ConfigurationSource? configurationSource) + { + var existingProperty = Metadata.FindProperty(propertyName); + return existingProperty != null + ? (IsCompatible(memberInfo, existingProperty) + && (propertyType == null || propertyType == existingProperty.ClrType)) + || ((memberInfo == null + || (memberInfo is PropertyInfo propertyInfo && propertyInfo.IsIndexerProperty())) + && (existingProperty.GetTypeConfigurationSource() is not ConfigurationSource existingTypeConfigurationSource + || typeConfigurationSource.Overrides(existingTypeConfigurationSource))) + || configurationSource.Overrides(existingProperty.GetConfigurationSource()) + : configurationSource.HasValue + && CanAddProperty(propertyType ?? memberInfo?.GetMemberType(), propertyName, configurationSource.Value); + } + + private bool CanAddProperty( + Type? propertyType, + string propertyName, + ConfigurationSource configurationSource) + => !IsIgnored(propertyName, configurationSource) + && (propertyType == null + || Metadata.Model.Builder.CanBeConfigured(propertyType, TypeConfigurationType.Property, configurationSource)) + && Metadata.FindServicePropertiesInHierarchy(propertyName).Cast() + .Concat(Metadata.FindNavigationsInHierarchy(propertyName)) + .Concat(Metadata.FindSkipNavigationsInHierarchy(propertyName)) + .All(m => configurationSource.Overrides(m.GetConfigurationSource()) + && m.GetConfigurationSource() != ConfigurationSource.Explicit); + private bool IsCompatible(MemberInfo? newMemberInfo, Property existingProperty) { if (newMemberInfo == null) @@ -868,7 +866,9 @@ public virtual IMutableNavigationBase Navigation(string navigationName) propertiesToDetach = new List { existingProperty }; } - else if (!CanAddServiceProperty(memberInfo, configurationSource)) + else if (configurationSource != ConfigurationSource.Explicit + && (!configurationSource.HasValue + || !CanAddServiceProperty(memberInfo, configurationSource.Value))) { return null; } @@ -983,31 +983,25 @@ public virtual bool CanHaveServiceProperty(MemberInfo memberInfo, ConfigurationS var existingProperty = Metadata.FindServiceProperty(memberInfo); return existingProperty != null ? existingProperty.DeclaringEntityType == Metadata - || (configurationSource.HasValue - && configurationSource.Value.Overrides(existingProperty.GetConfigurationSource())) - : CanAddServiceProperty(memberInfo, configurationSource); + || configurationSource.Overrides(existingProperty.GetConfigurationSource()) + : configurationSource.HasValue + && CanAddServiceProperty(memberInfo, configurationSource.Value); } - private bool CanAddServiceProperty(MemberInfo memberInfo, ConfigurationSource? configurationSource) + private bool CanAddServiceProperty(MemberInfo memberInfo, ConfigurationSource configurationSource) { var propertyName = memberInfo.GetSimpleMemberName(); - if (!configurationSource.HasValue - || IsIgnored(propertyName, configurationSource)) - { - return false; - } - - foreach (var conflictingProperty in Metadata.FindMembersInHierarchy(propertyName)) - { - if (!configurationSource.Overrides(conflictingProperty.GetConfigurationSource()) - && (conflictingProperty is not ServiceProperty derivedServiceProperty - || !memberInfo.IsOverridenBy(derivedServiceProperty.GetIdentifyingMemberInfo()))) - { - return false; - } - } - - return true; + return !IsIgnored(propertyName, configurationSource) + && Metadata.Model.Builder.CanBeConfigured(memberInfo.GetMemberType(), TypeConfigurationType.ServiceProperty, configurationSource) + && Metadata.FindPropertiesInHierarchy(propertyName).Cast() + .Concat(Metadata.FindNavigationsInHierarchy(propertyName)) + .Concat(Metadata.FindSkipNavigationsInHierarchy(propertyName)) + .All(m => configurationSource.Overrides(m.GetConfigurationSource()) + && m.GetConfigurationSource() != ConfigurationSource.Explicit) + && Metadata.FindServicePropertiesInHierarchy(propertyName) + .All(m => (configurationSource.Overrides(m.GetConfigurationSource()) + && m.GetConfigurationSource() != ConfigurationSource.Explicit) + || memberInfo.IsOverridenBy(m.GetIdentifyingMemberInfo())); } private static InternalServicePropertyBuilder? DetachServiceProperty(ServiceProperty? serviceProperty) @@ -1028,12 +1022,48 @@ private bool CanAddServiceProperty(MemberInfo memberInfo, ConfigurationSource? 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. /// - public virtual bool CanHaveNavigation(string navigationName, ConfigurationSource? configurationSource) + public virtual bool CanHaveNavigation(string navigationName, Type? type, ConfigurationSource? configurationSource) + { + var existingNavigation = Metadata.FindNavigation(navigationName); + return existingNavigation != null + ? type == null + || existingNavigation.ClrType == type + || configurationSource.Overrides(existingNavigation.GetConfigurationSource()) + : configurationSource.HasValue + && CanAddNavigation(navigationName, type, configurationSource.Value); + } + + /// + /// 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 CanAddNavigation(string navigationName, Type? type, ConfigurationSource configurationSource) => !IsIgnored(navigationName, configurationSource) + && (type == null || CanBeNavigation(type, configurationSource)) && Metadata.FindPropertiesInHierarchy(navigationName).Cast() .Concat(Metadata.FindServicePropertiesInHierarchy(navigationName)) .Concat(Metadata.FindSkipNavigationsInHierarchy(navigationName)) - .All(m => configurationSource.Overrides(m.GetConfigurationSource())); + .All(m => configurationSource.Overrides(m.GetConfigurationSource()) + && m.GetConfigurationSource() != ConfigurationSource.Explicit); + + private bool CanBeNavigation(Type type, ConfigurationSource configurationSource) + { + if (configurationSource == ConfigurationSource.Explicit) + { + return true; + } + + if (ModelBuilder.Metadata.Configuration?.GetConfigurationType(type).IsEntityType() == false + || (type?.TryGetSequenceType() is Type sequenceType + && ModelBuilder.Metadata.Configuration?.GetConfigurationType(sequenceType).IsEntityType() == false)) + { + return false; + } + + return true; + } /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -1041,12 +1071,25 @@ public virtual bool CanHaveNavigation(string navigationName, ConfigurationSource /// 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 CanHaveSkipNavigation(string skipNavigationName, ConfigurationSource? configurationSource) + public virtual bool CanHaveSkipNavigation(string skipNavigationName, Type? type, ConfigurationSource? configurationSource) + { + var existingNavigation = Metadata.FindSkipNavigation(skipNavigationName); + return existingNavigation != null + ? type == null + || existingNavigation.ClrType == type + || configurationSource.Overrides(existingNavigation.GetConfigurationSource()) + : configurationSource.HasValue + && CanAddSkipNavigation(skipNavigationName, type, configurationSource.Value); + } + + private bool CanAddSkipNavigation(string skipNavigationName, Type? type, ConfigurationSource configurationSource) => !IsIgnored(skipNavigationName, configurationSource) + && (type == null || CanBeNavigation(type, configurationSource)) && Metadata.FindPropertiesInHierarchy(skipNavigationName).Cast() .Concat(Metadata.FindServicePropertiesInHierarchy(skipNavigationName)) .Concat(Metadata.FindNavigationsInHierarchy(skipNavigationName)) - .All(m => configurationSource.Overrides(m.GetConfigurationSource())); + .All(m => configurationSource.Overrides(m.GetConfigurationSource()) + && m.GetConfigurationSource() != ConfigurationSource.Explicit); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -3377,6 +3420,21 @@ private bool Contains(IReadOnlyForeignKey? inheritedFk, IReadOnlyForeignKey deri targetType = Model.DefaultPropertyBagType; } + switch (ModelBuilder.Metadata.Configuration?.GetConfigurationType(targetType)) + { + case null: + break; + case TypeConfigurationType.EntityType: + case TypeConfigurationType.SharedTypeEntityType: + targetShouldBeOwned ??= false; + break; + case TypeConfigurationType.OwnedEntityType: + targetShouldBeOwned ??= true; + break; + default: + return null; + } + if (targetShouldBeOwned != true) { var ownership = Metadata.FindOwnership(); @@ -3593,6 +3651,11 @@ private bool Contains(IReadOnlyForeignKey? inheritedFk, IReadOnlyForeignKey deri dependentProperties.Count, null, dependentProperties.Select(p => p.ClrType), Enumerable.Repeat("", dependentProperties.Count), isRequired: true, baseName: "TempId").Item2; + if (principalKeyProperties == null) + { + return null; + } + principalKey = principalBaseEntityTypeBuilder.HasKeyInternal(principalKeyProperties, ConfigurationSource.Convention)! .Metadata; } @@ -3611,6 +3674,11 @@ private bool Contains(IReadOnlyForeignKey? inheritedFk, IReadOnlyForeignKey deri var principalKeyProperties = principalBaseEntityTypeBuilder.TryCreateUniqueProperties( 1, null, new[] { typeof(int) }, new[] { "TempId" }, isRequired: true, baseName: "").Item2; + if (principalKeyProperties == null) + { + return null; + } + principalKey = principalBaseEntityTypeBuilder.HasKeyInternal( principalKeyProperties, ConfigurationSource.Convention)?.Metadata; @@ -3624,7 +3692,12 @@ private bool Contains(IReadOnlyForeignKey? inheritedFk, IReadOnlyForeignKey deri { var oldProperties = foreignKey.Properties; var oldKey = foreignKey.PrincipalKey; - var temporaryProperties = CreateUniqueProperties(null, principalKey.Properties, isRequired ?? false, "TempFk")!; + var temporaryProperties = CreateUniqueProperties(principalKey.Properties, isRequired ?? false, "TempFk"); + if (temporaryProperties == null) + { + return null; + } + foreignKey.SetProperties(temporaryProperties, principalKey, configurationSource); foreignKey.DeclaringEntityType.Builder.RemoveUnusedImplicitProperties(oldProperties); @@ -3639,7 +3712,11 @@ private bool Contains(IReadOnlyForeignKey? inheritedFk, IReadOnlyForeignKey deri var baseName = string.IsNullOrEmpty(propertyBaseName) ? principalType.ShortName() : propertyBaseName; - dependentProperties = CreateUniqueProperties(null, principalKey.Properties, isRequired ?? false, baseName)!; + dependentProperties = CreateUniqueProperties(principalKey.Properties, isRequired ?? false, baseName); + if (dependentProperties == null) + { + return null; + } } if (foreignKey == null) @@ -3745,22 +3822,13 @@ private bool Contains(IReadOnlyForeignKey? inheritedFk, IReadOnlyForeignKey deri return existingNavigation.Builder; } - if (!configurationSource.HasValue - || IsIgnored(navigationName, configurationSource)) + if (configurationSource != ConfigurationSource.Explicit + && (!configurationSource.HasValue + || !CanAddSkipNavigation(navigationName, memberInfo?.GetMemberType(), configurationSource.Value))) { return null; } - foreach (var conflictingMember in Metadata.FindPropertiesInHierarchy(navigationName).Cast() - .Concat(Metadata.FindNavigationsInHierarchy(navigationName)) - .Concat(Metadata.FindServicePropertiesInHierarchy(navigationName))) - { - if (!configurationSource.Overrides(conflictingMember.GetConfigurationSource())) - { - return null; - } - } - foreach (var derivedType in Metadata.GetDerivedTypes()) { var conflictingNavigation = derivedType.FindDeclaredSkipNavigation(navigationName); @@ -3936,7 +4004,7 @@ public virtual bool ShouldReuniquifyTemporaryProperties(ForeignKey foreignKey) => CreateUniqueProperties( new[] { propertyType }, new[] { propertyName }, - required).First().Builder; + required)?.First().Builder; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -3944,7 +4012,7 @@ public virtual bool ShouldReuniquifyTemporaryProperties(ForeignKey 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 CreateUniqueProperties( + public virtual IReadOnlyList? CreateUniqueProperties( IReadOnlyList propertyTypes, IReadOnlyList propertyNames, bool isRequired) @@ -3954,16 +4022,15 @@ public virtual IReadOnlyList CreateUniqueProperties( propertyTypes, propertyNames, isRequired, - "").Item2!; + "").Item2; private IReadOnlyList? CreateUniqueProperties( - IReadOnlyList? currentProperties, IReadOnlyList principalProperties, bool isRequired, string baseName) => TryCreateUniqueProperties( principalProperties.Count, - currentProperties, + null, principalProperties.Select(p => p.ClrType), principalProperties.Select(p => p.Name), isRequired, @@ -4010,7 +4077,12 @@ public virtual IReadOnlyList CreateUniqueProperties( { var propertyBuilder = Property( clrType, propertyName, typeConfigurationSource: null, - configurationSource: ConfigurationSource.Convention)!; + configurationSource: ConfigurationSource.Convention); + + if (propertyBuilder == null) + { + return (false, null); + } if (clrType.IsNullableType()) { @@ -4019,6 +4091,11 @@ public virtual IReadOnlyList CreateUniqueProperties( newProperties![i] = propertyBuilder.Metadata; } + else if (!Metadata.Model.Builder.CanBeConfigured( + clrType, TypeConfigurationType.Property, ConfigurationSource.Convention)) + { + break; + } else { canReuniquify = true; @@ -4579,7 +4656,7 @@ bool IConventionEntityTypeBuilder.CanSetBaseType(IConventionEntityType? baseEnti propertyType, propertyName, setTypeConfigurationSource ? fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention - : (ConfigurationSource?)null, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + : null, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -4591,6 +4668,71 @@ bool IConventionEntityTypeBuilder.CanSetBaseType(IConventionEntityType? baseEnti IConventionPropertyBuilder? IConventionEntityTypeBuilder.Property(MemberInfo memberInfo, bool fromDataAnnotation) => Property(memberInfo, 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] + bool IConventionEntityTypeBuilder.CanHaveProperty( + Type? propertyType, string propertyName, bool fromDataAnnotation) + => CanHaveProperty( + propertyType, + propertyName, + null, + propertyType != null ? + fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention + : 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] + bool IConventionEntityTypeBuilder.CanHaveProperty(MemberInfo memberInfo, bool fromDataAnnotation) + => CanHaveProperty( + memberInfo.GetMemberType(), + memberInfo.Name, + memberInfo, + fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention, + 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] + IConventionPropertyBuilder? IConventionEntityTypeBuilder.IndexerProperty( + Type propertyType, string propertyName, bool fromDataAnnotation) + => Property( + propertyType, + propertyName, + Metadata.FindIndexerPropertyInfo(), + fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention, + 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] + bool IConventionEntityTypeBuilder.CanHaveIndexerProperty( + Type propertyType, string propertyName, bool fromDataAnnotation) + => CanHaveProperty( + propertyType, + propertyName, + Metadata.FindIndexerPropertyInfo(), + fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention, + 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 @@ -4637,6 +4779,16 @@ IConventionEntityTypeBuilder IConventionEntityTypeBuilder.RemoveUnusedImplicitPr IConventionServicePropertyBuilder? IConventionEntityTypeBuilder.ServiceProperty(MemberInfo memberInfo, bool fromDataAnnotation) => ServiceProperty(memberInfo, 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] + bool IConventionEntityTypeBuilder.CanHaveServiceProperty(MemberInfo memberInfo, bool fromDataAnnotation) + => CanHaveServiceProperty(memberInfo, 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 @@ -5136,6 +5288,20 @@ bool IConventionEntityTypeBuilder.CanRemoveRelationship(IConventionForeignKey fo bool IConventionEntityTypeBuilder.CanHaveNavigation(string navigationName, bool fromDataAnnotation) => CanHaveNavigation( navigationName, + 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] + bool IConventionEntityTypeBuilder.CanHaveNavigation(string navigationName, Type? type, bool fromDataAnnotation) + => CanHaveNavigation( + navigationName, + type, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); /// @@ -5148,6 +5314,20 @@ bool IConventionEntityTypeBuilder.CanHaveNavigation(string navigationName, bool bool IConventionEntityTypeBuilder.CanHaveSkipNavigation(string skipNavigationName, bool fromDataAnnotation) => CanHaveSkipNavigation( skipNavigationName, + 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] + bool IConventionEntityTypeBuilder.CanHaveSkipNavigation(string skipNavigationName, Type? type, bool fromDataAnnotation) + => CanHaveSkipNavigation( + skipNavigationName, + type, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); /// diff --git a/src/EFCore/Metadata/Internal/InternalForeignKeyBuilder.cs b/src/EFCore/Metadata/Internal/InternalForeignKeyBuilder.cs index 1f8856b9aa4..1bdd97ec1c7 100644 --- a/src/EFCore/Metadata/Internal/InternalForeignKeyBuilder.cs +++ b/src/EFCore/Metadata/Internal/InternalForeignKeyBuilder.cs @@ -672,7 +672,10 @@ private bool CanSetNavigations( removeOppositeNavigation = true; } - else if (!dependentEntityType.Builder.CanHaveNavigation(navigationToPrincipalName, configurationSource)) + else if ((configurationSource != ConfigurationSource.Explicit || !shouldThrow) + && (!configurationSource.HasValue + || !dependentEntityType.Builder.CanAddNavigation( + navigationToPrincipalName, navigationToPrincipal.Value.MemberInfo?.GetMemberType(), configurationSource.Value))) { return false; } @@ -702,7 +705,10 @@ private bool CanSetNavigations( removeOppositeNavigation = true; } - else if (!principalEntityType.Builder.CanHaveNavigation(navigationToDependentName, configurationSource)) + else if ((configurationSource != ConfigurationSource.Explicit || !shouldThrow) + && (!configurationSource.HasValue + || !principalEntityType.Builder.CanAddNavigation( + navigationToDependentName, navigationToDependent.Value.MemberInfo?.GetMemberType(), configurationSource.Value))) { return false; } @@ -804,8 +810,7 @@ private bool CanSetNavigations( conflictingNavigationsFound = compatibleRelationship != null || resolvableRelationships.Any( - r => - (r.Resolution & (Resolution.ResetToDependent | Resolution.ResetToPrincipal)) != 0); + r => (r.Resolution & (Resolution.ResetToDependent | Resolution.ResetToPrincipal)) != 0); if (shouldBeUnique == null && (Metadata.IsUnique || configurationSource.OverridesStrictly(Metadata.GetIsUniqueConfigurationSource())) diff --git a/src/EFCore/Metadata/Internal/InternalModelBuilder.cs b/src/EFCore/Metadata/Internal/InternalModelBuilder.cs index 2735a25c682..41775199960 100644 --- a/src/EFCore/Metadata/Internal/InternalModelBuilder.cs +++ b/src/EFCore/Metadata/Internal/InternalModelBuilder.cs @@ -86,6 +86,31 @@ public override InternalModelBuilder ModelBuilder return null; } + if (type.Type != null) + { + if (shouldBeOwned == null) + { + if (configurationSource != ConfigurationSource.Explicit + && Metadata.Configuration?.GetConfigurationType(type.Type).IsEntityType() == false) + { + return null; + } + } + else + { + var configurationType = shouldBeOwned.Value + ? TypeConfigurationType.OwnedEntityType + : type.IsNamed + ? TypeConfigurationType.SharedTypeEntityType + : TypeConfigurationType.EntityType; + + if (!CanBeConfigured(type.Type, configurationType, configurationSource)) + { + return null; + } + } + } + using var batch = Metadata.DelayConventions(); var clrType = type.Type; EntityType? entityType; @@ -139,8 +164,7 @@ public override InternalModelBuilder ModelBuilder { // We always throw as configuring a type as owned always comes from user (through Explicit/DataAnnotation) throw new InvalidOperationException( - CoreStrings.ClashingOwnedEntityType( - clrType == null ? type.Name : clrType.ShortDisplayName())); + CoreStrings.ClashingOwnedEntityType(clrType == null ? type.Name : clrType.ShortDisplayName())); } if (shouldBeOwned == true @@ -271,7 +295,8 @@ public override InternalModelBuilder ModelBuilder Type type, ConfigurationSource configurationSource) { - if (IsIgnored(type, configurationSource)) + if (IsIgnored(type, configurationSource) + || !CanBeConfigured(type, TypeConfigurationType.OwnedEntityType, configurationSource)) { return null; } @@ -370,10 +395,42 @@ private bool IsIgnored(in TypeIdentity type, ConfigurationSource configurationSo } var ignoredConfigurationSource = Metadata.FindIgnoredConfigurationSource(type.Name); + if (type.Type != null + && Metadata.IsIgnoredType(type.Type)) + { + ignoredConfigurationSource = ConfigurationSource.Explicit; + } + return ignoredConfigurationSource.HasValue && ignoredConfigurationSource.Value.Overrides(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 CanBeConfigured(Type type, TypeConfigurationType configurationType, ConfigurationSource configurationSource) + { + if (configurationSource == ConfigurationSource.Explicit) + { + return true; + } + + if (!configurationType.IsEntityType() + && (!configurationSource.Overrides(Metadata.FindEntityType(type)?.GetConfigurationSource()) + || !configurationSource.Overrides(Metadata.FindIsOwnedConfigurationSource(type)) + || Metadata.IsShared(type))) + { + return false; + } + + var configuredType = ModelBuilder.Metadata.Configuration?.GetConfigurationType(type); + return configuredType == null + || configuredType == configurationType; + } + /// /// 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/InternalPropertyBuilder.cs b/src/EFCore/Metadata/Internal/InternalPropertyBuilder.cs index 83476c710cf..fb27c385773 100644 --- a/src/EFCore/Metadata/Internal/InternalPropertyBuilder.cs +++ b/src/EFCore/Metadata/Internal/InternalPropertyBuilder.cs @@ -459,6 +459,7 @@ public virtual bool CanSetValueGeneratorFactory( { if (CanSetConversion(converter, configurationSource)) { + Metadata.SetProviderClrType(null, configurationSource); Metadata.SetValueConverter(converter, configurationSource); return this; @@ -476,11 +477,12 @@ public virtual bool CanSetValueGeneratorFactory( public virtual bool CanSetConversion( ValueConverter? converter, ConfigurationSource? configurationSource) - => configurationSource == ConfigurationSource.Explicit + => (configurationSource == ConfigurationSource.Explicit || (configurationSource.Overrides(Metadata.GetValueConverterConfigurationSource()) && Metadata.CheckValueConverter(converter) == null) || (Metadata[CoreAnnotationNames.ValueConverterType] == null - && (ValueConverter?)Metadata[CoreAnnotationNames.ValueConverter] == converter); + && (ValueConverter?)Metadata[CoreAnnotationNames.ValueConverter] == converter)) + && configurationSource.Overrides(Metadata.GetProviderClrTypeConfigurationSource()); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -492,6 +494,7 @@ public virtual bool CanSetConversion( { if (CanSetConversion(providerClrType, configurationSource)) { + Metadata.SetValueConverter((ValueConverter?)null, configurationSource); Metadata.SetProviderClrType(providerClrType, configurationSource); return this; @@ -507,8 +510,9 @@ public virtual bool CanSetConversion( /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public virtual bool CanSetConversion(Type? providerClrType, ConfigurationSource? configurationSource) - => configurationSource.Overrides(Metadata.GetProviderClrTypeConfigurationSource()) - || Metadata.GetProviderClrType() == providerClrType; + => (configurationSource.Overrides(Metadata.GetProviderClrTypeConfigurationSource()) + || Metadata.GetProviderClrType() == providerClrType) + && configurationSource.Overrides(Metadata.GetValueConverterConfigurationSource()); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -520,6 +524,7 @@ public virtual bool CanSetConversion(Type? providerClrType, ConfigurationSource? { if (CanSetConverter(converterType, configurationSource)) { + Metadata.SetProviderClrType(null, configurationSource); Metadata.SetValueConverter(converterType, configurationSource); return this; diff --git a/src/EFCore/Metadata/Internal/MemberClassifier.cs b/src/EFCore/Metadata/Internal/MemberClassifier.cs index d896a27407f..5f03e435d08 100644 --- a/src/EFCore/Metadata/Internal/MemberClassifier.cs +++ b/src/EFCore/Metadata/Internal/MemberClassifier.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; +using System.Collections.Immutable; using System.Reflection; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Infrastructure.Internal; using Microsoft.EntityFrameworkCore.Storage; using Microsoft.EntityFrameworkCore.Utilities; using Microsoft.Extensions.DependencyInjection; @@ -50,10 +53,44 @@ public MemberClassifier( /// 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 Type? FindCandidateNavigationPropertyType(MemberInfo memberInfo) + public virtual ImmutableSortedDictionary GetNavigationCandidates(IConventionEntityType entityType) { - Check.NotNull(memberInfo, nameof(memberInfo)); + if (entityType.FindAnnotation(CoreAnnotationNames.NavigationCandidates)?.Value + is ImmutableSortedDictionary navigationCandidates) + { + return navigationCandidates; + } + + var dictionaryBuilder = ImmutableSortedDictionary.CreateBuilder(MemberInfoNameComparer.Instance); + + var configuration = ((Model)entityType.Model).Configuration; + foreach (var propertyInfo in entityType.GetRuntimeProperties().Values) + { + var targetType = FindCandidateNavigationPropertyType(propertyInfo, configuration); + if (targetType != null) + { + dictionaryBuilder[propertyInfo] = targetType; + } + } + navigationCandidates = dictionaryBuilder.ToImmutable(); + + if (!((Annotatable)entityType).IsReadOnly + && entityType.IsInModel) + { + entityType.Builder.HasAnnotation(CoreAnnotationNames.NavigationCandidates, navigationCandidates); + } + return navigationCandidates; + } + + /// + /// 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 Type? FindCandidateNavigationPropertyType(MemberInfo memberInfo, ModelConfiguration? configuration) + { var targetType = memberInfo.GetMemberType(); var targetSequenceType = targetType.TryGetSequenceType(); if (!(memberInfo is PropertyInfo propertyInfo) @@ -62,17 +99,85 @@ public MemberClassifier( return null; } + var isConfiguredAsEntityType = configuration?.GetConfigurationType(targetType).IsEntityType(); + if (isConfiguredAsEntityType == false) + { + return null; + } + + if (targetSequenceType != null) + { + isConfiguredAsEntityType ??= configuration?.GetConfigurationType(targetSequenceType).IsEntityType(); + if (isConfiguredAsEntityType == false) + { + return null; + } + } + targetType = targetSequenceType ?? targetType; - targetType = targetType.UnwrapNullableType(); + if (!targetType.IsValidEntityType()) + { + return null; + } - return targetType.IsInterface - || targetType.IsValueType - || targetType == typeof(object) - || _parameterBindingFactories.FindFactory(targetType, memberInfo.GetSimpleMemberName()) != null - || _typeMappingSource.FindMapping(targetType) != null - || targetType.IsArray + targetType = targetType.UnwrapNullableType(); + return isConfiguredAsEntityType == null + && (targetType == typeof(object) + || _parameterBindingFactories.FindFactory(targetType, memberInfo.GetSimpleMemberName()) != null + || _typeMappingSource.FindMapping(targetType) != null) ? null : targetType; } + + /// + /// 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 IsCandidatePrimitiveProperty(PropertyInfo propertyInfo, ModelConfiguration? configuration) + { + if (!propertyInfo.IsCandidateProperty()) + { + return false; + } + + var configurationType = configuration?.GetConfigurationType(propertyInfo.PropertyType); + return configurationType == TypeConfigurationType.Property + || (configurationType == null && _typeMappingSource.FindMapping(propertyInfo) != 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 IParameterBindingFactory? FindServicePropertyCandidateBindingFactory( + PropertyInfo propertyInfo, ModelConfiguration? configuration) + { + if (!propertyInfo.IsCandidateProperty(publicOnly: false)) + { + return null; + } + + var type = propertyInfo.PropertyType; + var configurationType = configuration?.GetConfigurationType(type); + if (configurationType != TypeConfigurationType.ServiceProperty) + { + if (configurationType != null) + { + return null; + } + + if (propertyInfo.IsCandidateProperty() + && _typeMappingSource.FindMapping(propertyInfo) != null) + { + return null; + } + } + + return _parameterBindingFactories.FindFactory(type, propertyInfo.GetSimpleMemberName()); + } } } diff --git a/src/EFCore/Metadata/Internal/Model.cs b/src/EFCore/Metadata/Internal/Model.cs index d0128457e0c..48343d0ee9b 100644 --- a/src/EFCore/Metadata/Internal/Model.cs +++ b/src/EFCore/Metadata/Internal/Model.cs @@ -608,6 +608,15 @@ public virtual bool IsIgnored(string name) public virtual bool IsIgnored(Type type) => FindIgnoredConfigurationSource(GetDisplayName(type)) != 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 bool IsIgnoredType(Type type) + => Configuration?.GetConfigurationType(type) == TypeConfigurationType.Ignored; + /// /// 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 @@ -871,7 +880,6 @@ public virtual IModel FinalizeModel() if (finalizedModel is Model model) { - model.Configuration = null; finalizedModel = model.MakeReadonly(); } diff --git a/src/EFCore/Metadata/Internal/ModelConfiguration.cs b/src/EFCore/Metadata/Internal/ModelConfiguration.cs index 544acfe0838..8d44a11c77c 100644 --- a/src/EFCore/Metadata/Internal/ModelConfiguration.cs +++ b/src/EFCore/Metadata/Internal/ModelConfiguration.cs @@ -1,6 +1,11 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.EntityFrameworkCore.Diagnostics; + namespace Microsoft.EntityFrameworkCore.Metadata.Internal { /// @@ -11,6 +16,10 @@ namespace Microsoft.EntityFrameworkCore.Metadata.Internal /// public partial class ModelConfiguration { + private readonly Dictionary _properties = new(); + private readonly HashSet _ignoredTypes = new(); + private readonly Dictionary _configurationTypes = new(); + /// /// 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 @@ -28,6 +37,245 @@ public ModelConfiguration() /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public virtual bool IsEmpty() - => true; + => _properties.Count == 0 && _ignoredTypes.Count == 0; + + /// + /// 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 TypeConfigurationType? GetConfigurationType(Type type) + { + Type? configuredType = null; + return GetConfigurationType(type, null, ref configuredType); + } + + private TypeConfigurationType? GetConfigurationType( + Type type, TypeConfigurationType? previousConfiguration, ref Type? previousType, bool getBaseTypes = true) + { + if (_configurationTypes.TryGetValue(type, out var configurationType)) + { + if (configurationType.HasValue) + { + EnsureCompatible(configurationType.Value, type, previousConfiguration, previousType); + previousType = type; + } + + return configurationType ?? previousConfiguration; + } + + Type? configuredType = null; + if (getBaseTypes) + { + if (type.BaseType != null) + { + configurationType = GetConfigurationType( + type.BaseType, configurationType, ref configuredType); + } + + if (type.IsConstructedGenericType) + { + configurationType = GetConfigurationType( + type.GetGenericTypeDefinition(), configurationType, ref configuredType, getBaseTypes: false); + } + + foreach (var @interface in GetDeclaredInterfaces(type)) + { + configurationType = GetConfigurationType( + @interface, configurationType, ref configuredType, getBaseTypes: false); + } + } + + if (_ignoredTypes.Contains(type)) + { + EnsureCompatible(TypeConfigurationType.Ignored, type, configurationType, configuredType); + configurationType = TypeConfigurationType.Ignored; + configuredType = type; + } + + if (_properties.ContainsKey(type)) + { + EnsureCompatible(TypeConfigurationType.Property, type, configurationType, configuredType); + configurationType = TypeConfigurationType.Property; + configuredType = type; + } + + if (configurationType.HasValue) + { + EnsureCompatible(configurationType.Value, configuredType!, previousConfiguration, previousType); + previousType = configuredType; + } + + _configurationTypes[type] = configurationType; + return configurationType ?? previousConfiguration; + } + + private IEnumerable GetDeclaredInterfaces(Type type) + { + var interfaces = type.GetInterfaces(); + if (type.BaseType == typeof(object) + || type.BaseType == null) + { + return interfaces; + } + + return interfaces.Except(type.BaseType.GetInterfaces()); + } + + private static void EnsureCompatible( + TypeConfigurationType configurationType, Type type, + TypeConfigurationType? previousConfiguration, Type? previousType) + { + if (previousConfiguration != null + && previousConfiguration.Value != configurationType) + { + throw new InvalidOperationException(CoreStrings.TypeConfigurationConflict( + type.DisplayName(fullName: false), configurationType, + previousType?.DisplayName(fullName: false), previousConfiguration.Value)); + } + } + + private IList GetBaseTypesAndInterfacesInclusive(Type type) + { + var baseTypes = new List(); + var typesToProcess = new Queue(); + typesToProcess.Enqueue(type); + + while (typesToProcess.Count > 0) + { + type = typesToProcess.Dequeue(); + baseTypes.Add(type); + + if (!type.IsGenericTypeDefinition + && !type.IsInterface) + { + if (type.BaseType != null) + { + typesToProcess.Enqueue(type.BaseType); + } + + if (type.IsConstructedGenericType) + { + typesToProcess.Enqueue(type.GetGenericTypeDefinition()); + } + + foreach (var @interface in GetDeclaredInterfaces(type)) + { + typesToProcess.Enqueue(@interface); + } + } + } + + return baseTypes; + } + + /// + /// 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 ConfigureProperty(IMutableProperty property) + { + var types = GetBaseTypesAndInterfacesInclusive(property.ClrType); + for (var i = types.Count - 1; i >= 0; i--) + { + var type = types[i]; + + if (_properties.TryGetValue(type, out var configuration)) + { + configuration.Apply(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 PropertyConfiguration GetOrAddProperty(Type type) + { + if (type.UnwrapNullableType() == typeof(object) + || type == Model.DefaultPropertyBagType) + { + throw new InvalidOperationException( + CoreStrings.UnconfigurableType(type.DisplayName(fullName: false), TypeConfigurationType.Property)); + } + + var property = FindProperty(type); + + if (property == null) + { + RemoveIgnored(type); + + property = new PropertyConfiguration(type); + _properties.Add(type, property); + } + + return 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 PropertyConfiguration? FindProperty(Type type) + => _properties.TryGetValue(type, out var property) + ? property + : 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 bool RemoveProperty(Type type) + => _properties.Remove(type); + + /// + /// 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 AddIgnored(Type type) + { + if (type.UnwrapNullableType() == typeof(int) + || type.UnwrapNullableType() == typeof(int?) + || type.UnwrapNullableType() == typeof(string) + || type.UnwrapNullableType() == typeof(object) + || type == Model.DefaultPropertyBagType) + { + throw new InvalidOperationException( + CoreStrings.UnconfigurableType(type.DisplayName(fullName: false), TypeConfigurationType.Ignored)); + } + + RemoveProperty(type); + _ignoredTypes.Add(type); + } + + /// + /// 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 IsIgnored(Type type) + => _ignoredTypes.Contains(type); + + /// + /// 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 RemoveIgnored(Type type) + => _ignoredTypes.Remove(type); } } diff --git a/src/EFCore/Metadata/Internal/Property.cs b/src/EFCore/Metadata/Internal/Property.cs index 13cea781f7e..e6b9861a4c1 100644 --- a/src/EFCore/Metadata/Internal/Property.cs +++ b/src/EFCore/Metadata/Internal/Property.cs @@ -978,9 +978,9 @@ public static bool AreCompatible(IReadOnlyList properties, EntityType property => property.IsShadowProperty() || ((property.PropertyInfo != null - && entityType.GetRuntimeProperties().ContainsKey(property.Name)) - || (property.FieldInfo != null - && entityType.GetRuntimeFields().ContainsKey(property.Name)))); + && entityType.GetRuntimeProperties().ContainsKey(property.Name)) + || (property.FieldInfo != null + && entityType.GetRuntimeFields().ContainsKey(property.Name)))); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to diff --git a/src/EFCore/Metadata/Internal/PropertyConfiguration.cs b/src/EFCore/Metadata/Internal/PropertyConfiguration.cs new file mode 100644 index 00000000000..cea60465e1b --- /dev/null +++ b/src/EFCore/Metadata/Internal/PropertyConfiguration.cs @@ -0,0 +1,199 @@ +// 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 Microsoft.EntityFrameworkCore.ChangeTracking; +using Microsoft.EntityFrameworkCore.Diagnostics; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +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 PropertyConfiguration : AnnotatableBase + { + /// + /// 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 PropertyConfiguration(Type clrType) + { + Check.NotNull(clrType, nameof(clrType)); + + ClrType = clrType; + } + + /// + /// 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 Type ClrType { 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 void Apply(IMutableProperty property) + { + foreach (var annotation in GetAnnotations()) + { + switch (annotation.Name) + { + case CoreAnnotationNames.MaxLength: + property.SetMaxLength((int?)annotation.Value); + + break; + case CoreAnnotationNames.Unicode: + property.SetIsUnicode((bool?)annotation.Value); + + break; + case CoreAnnotationNames.Precision: + property.SetPrecision((int?)annotation.Value); + + break; + case CoreAnnotationNames.Scale: + property.SetScale((int?)annotation.Value); + + break; + case CoreAnnotationNames.ProviderClrType: + property.SetProviderClrType((Type?)annotation.Value); + + break; + case CoreAnnotationNames.ValueConverterType: + property.SetValueConverter((Type?)annotation.Value); + + break; + case CoreAnnotationNames.ValueComparerType: + property.SetValueComparer((Type?)annotation.Value); + + break; + default: + if (!CoreAnnotationNames.AllNames.Contains(annotation.Name)) + { + property.SetAnnotation(annotation.Name, annotation.Value); + } + break; + } + } + } + + /// + /// 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 SetMaxLength(int? maxLength) + { + if (maxLength != null + && maxLength < 0) + { + throw new ArgumentOutOfRangeException(nameof(maxLength)); + } + + this[CoreAnnotationNames.MaxLength] = maxLength; + } + + /// + /// 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 SetIsUnicode(bool? unicode) + => this[CoreAnnotationNames.Unicode] = unicode; + + /// + /// 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 SetPrecision(int? precision) + { + if (precision != null && precision < 0) + { + throw new ArgumentOutOfRangeException(nameof(precision)); + } + + this[CoreAnnotationNames.Precision] = precision; + } + + /// + /// 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 SetScale(int? scale) + { + if (scale != null && scale < 0) + { + throw new ArgumentOutOfRangeException(nameof(scale)); + } + + this[CoreAnnotationNames.Scale] = scale; + } + + /// + /// 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 SetProviderClrType(Type? providerClrType) + => this[CoreAnnotationNames.ProviderClrType] = providerClrType; + + /// + /// 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 SetValueConverter(Type? converterType) + { + if (converterType != null) + { + if (!typeof(ValueConverter).IsAssignableFrom(converterType)) + { + throw new InvalidOperationException( + CoreStrings.BadValueConverterType(converterType.ShortDisplayName(), typeof(ValueConverter).ShortDisplayName())); + } + } + + this[CoreAnnotationNames.ValueConverterType] = converterType; + } + + /// + /// 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 SetValueComparer(Type? comparerType) + { + if (comparerType != null) + { + if (!typeof(ValueComparer).IsAssignableFrom(comparerType)) + { + throw new InvalidOperationException( + CoreStrings.BadValueComparerType(comparerType.ShortDisplayName(), typeof(ValueComparer).ShortDisplayName())); + } + } + + this[CoreAnnotationNames.ValueComparerType] = comparerType; + } + } +} diff --git a/src/EFCore/Metadata/Internal/TypeBase.cs b/src/EFCore/Metadata/Internal/TypeBase.cs index 641b17930b8..e7109d16791 100644 --- a/src/EFCore/Metadata/Internal/TypeBase.cs +++ b/src/EFCore/Metadata/Internal/TypeBase.cs @@ -27,8 +27,8 @@ private readonly Dictionary _ignoredMembers private bool _indexerPropertyInitialized; private PropertyInfo? _indexerPropertyInfo; - private Dictionary? _runtimeProperties; - private Dictionary? _runtimeFields; + private SortedDictionary? _runtimeProperties; + private SortedDictionary? _runtimeFields; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -145,7 +145,7 @@ public virtual IReadOnlyDictionary GetRuntimeProperties() { if (_runtimeProperties == null) { - var runtimeProperties = new Dictionary(StringComparer.Ordinal); + var runtimeProperties = new SortedDictionary(StringComparer.Ordinal); foreach (var property in ClrType.GetRuntimeProperties()) { if (!property.IsStatic() @@ -171,7 +171,7 @@ public virtual IReadOnlyDictionary GetRuntimeFields() { if (_runtimeFields == null) { - var runtimeFields = new Dictionary(StringComparer.Ordinal); + var runtimeFields = new SortedDictionary(StringComparer.Ordinal); foreach (var field in ClrType.GetRuntimeFields()) { if (!field.IsStatic diff --git a/src/EFCore/Metadata/Internal/TypeBaseExtensions.cs b/src/EFCore/Metadata/Internal/TypeBaseExtensions.cs index c26b371f1e5..61423f57074 100644 --- a/src/EFCore/Metadata/Internal/TypeBaseExtensions.cs +++ b/src/EFCore/Metadata/Internal/TypeBaseExtensions.cs @@ -20,7 +20,7 @@ public static class TypeBaseExtensions /// 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 IReadOnlyDictionary GetRuntimeProperties(this IConventionTypeBase type) + public static IReadOnlyDictionary GetRuntimeProperties(this IReadOnlyTypeBase type) => ((TypeBase)type).GetRuntimeProperties(); /// @@ -29,7 +29,7 @@ public static IReadOnlyDictionary GetRuntimeProperties(thi /// 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 IReadOnlyDictionary GetRuntimeFields(this IConventionTypeBase type) + public static IReadOnlyDictionary GetRuntimeFields(this IReadOnlyTypeBase type) => ((TypeBase)type).GetRuntimeFields(); } } diff --git a/src/EFCore/Metadata/Internal/TypeConfigurationType.cs b/src/EFCore/Metadata/Internal/TypeConfigurationType.cs new file mode 100644 index 00000000000..eb31a441918 --- /dev/null +++ b/src/EFCore/Metadata/Internal/TypeConfigurationType.cs @@ -0,0 +1,62 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +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 enum TypeConfigurationType + { + /// + /// 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. + /// + Ignored, + + /// + /// 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. + /// + EntityType, + + /// + /// 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. + /// + SharedTypeEntityType, + + /// + /// 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. + /// + OwnedEntityType, + + /// + /// 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. + /// + 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. + /// + ServiceProperty + } +} diff --git a/src/EFCore/Metadata/Internal/TypeConfigurationTypeExtensions.cs b/src/EFCore/Metadata/Internal/TypeConfigurationTypeExtensions.cs new file mode 100644 index 00000000000..15a2dbc9506 --- /dev/null +++ b/src/EFCore/Metadata/Internal/TypeConfigurationTypeExtensions.cs @@ -0,0 +1,36 @@ +// 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.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 TypeConfigurationTypeExtensions + { + /// + /// 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 bool? IsEntityType(this TypeConfigurationType? configurationType) + => configurationType == null + ? null + : configurationType.Value.IsEntityType(); + + /// + /// 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 bool IsEntityType(this TypeConfigurationType configurationType) + => configurationType == TypeConfigurationType.EntityType + || configurationType == TypeConfigurationType.SharedTypeEntityType + || configurationType == TypeConfigurationType.OwnedEntityType; + } +} diff --git a/src/EFCore/ModelConfigurationBuilder.cs b/src/EFCore/ModelConfigurationBuilder.cs index 56f52bb45b9..5d1395f62d5 100644 --- a/src/EFCore/ModelConfigurationBuilder.cs +++ b/src/EFCore/ModelConfigurationBuilder.cs @@ -1,8 +1,10 @@ // 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.ComponentModel; using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata.Builders; using Microsoft.EntityFrameworkCore.Metadata.Conventions; using Microsoft.EntityFrameworkCore.Metadata.Internal; using Microsoft.EntityFrameworkCore.Utilities; @@ -45,6 +47,98 @@ public ModelConfigurationBuilder(ConventionSet conventions) [EntityFrameworkInternal] protected virtual ModelConfiguration ModelConfiguration => _modelConfiguration; + /// + /// Prevents the conventions from the given type from discovering properties of the given or derived types. + /// + /// The type to be ignored. + /// + /// The same instance so that additional configuration calls can be chained. + /// + public virtual ModelConfigurationBuilder IgnoreAny() + => IgnoreAny(typeof(TEntity)); + + /// + /// Prevents the conventions from the given type from discovering properties of the given or derived types. + /// + /// The type to be ignored. + /// + /// The same instance so that additional configuration calls can be chained. + /// + public virtual ModelConfigurationBuilder IgnoreAny(Type type) + { + Check.NotNull(type, nameof(type)); + + _modelConfiguration.AddIgnored(type); + + return this; + } + + /// + /// Marks the given and derived types as corresponding to entity type properties. + /// + /// The property type to be configured. + /// An object that can be used to provide the configuration defaults for the properties. + public virtual PropertiesConfigurationBuilder Properties() + { + var property = _modelConfiguration.GetOrAddProperty(typeof(TProperty)); + + return new PropertiesConfigurationBuilder(property); + } + + /// + /// Marks the given and derived types as corresponding to entity type properties. + /// + /// The property type to be configured. + /// An action that performs configuration of the property. + /// + /// The same instance so that additional configuration calls can be chained. + /// + public virtual ModelConfigurationBuilder Properties( + Action> buildAction) + { + Check.NotNull(buildAction, nameof(buildAction)); + + var propertyBuilder = Properties(); + buildAction(propertyBuilder); + + return this; + } + + /// + /// Marks the given and derived types as corresponding to entity type properties. + /// + /// The property type to be configured. + /// An object that can be used to configure the property. + public virtual PropertiesConfigurationBuilder Properties(Type propertyType) + { + Check.NotNull(propertyType, nameof(propertyType)); + + var property = _modelConfiguration.GetOrAddProperty(propertyType); + + return new PropertiesConfigurationBuilder(property); + } + + /// + /// Marks the given and derived types as corresponding to entity type properties. + /// + /// The property type to be configured. + /// An action that performs configuration of the property. + /// + /// The same instance so that additional configuration calls can be chained. + /// + public virtual ModelConfigurationBuilder Properties( + Type propertyType, + Action buildAction) + { + Check.NotNull(propertyType, nameof(propertyType)); + Check.NotNull(buildAction, nameof(buildAction)); + + var propertyBuilder = Properties(propertyType); + buildAction(propertyBuilder); + + return this; + } + /// /// Creates the configured used to create the model. This is done automatically when using /// ; this method allows it to be run diff --git a/src/EFCore/Properties/CoreStrings.Designer.cs b/src/EFCore/Properties/CoreStrings.Designer.cs index 2c9c141ffaa..3d94e5ceb95 100644 --- a/src/EFCore/Properties/CoreStrings.Designer.cs +++ b/src/EFCore/Properties/CoreStrings.Designer.cs @@ -2669,6 +2669,14 @@ public static string TranslationFailedWithDetails(object? expression, object? de GetString("TranslationFailedWithDetails", nameof(expression), nameof(details)), expression, details); + /// + /// The type '{type}' has been configured as '{typeConfiguration}', this conflicts with type '{otherType}' configured as '{otherTypeConfiguration}'. All base types and implemented interfaces must have the same configuration type. + /// + public static string TypeConfigurationConflict(object? type, object? typeConfiguration, object? otherType, object? otherTypeConfiguration) + => string.Format( + GetString("TypeConfigurationConflict", nameof(type), nameof(typeConfiguration), nameof(otherType), nameof(otherTypeConfiguration)), + type, typeConfiguration, otherType, otherTypeConfiguration); + /// /// The type '{type}' has not been configured as a shared type in the model. Before calling 'UsingEntity' add the entity type in the model as a shared entity. /// @@ -2693,6 +2701,14 @@ public static string UnableToSetIsUnique(object? isUnique, object? navigationNam GetString("UnableToSetIsUnique", nameof(isUnique), "1_navigationName", "2_entityType"), isUnique, navigationName, entityType); + /// + /// The type '{type}' cannot be configured as '{configuration}'. The current model building logic is unable to honor this configuration. + /// + public static string UnconfigurableType(object? type, object? configuration) + => string.Format( + GetString("UnconfigurableType", nameof(type), nameof(configuration)), + type, configuration); + /// /// Unhandled expression node type '{nodeType}'. /// diff --git a/src/EFCore/Properties/CoreStrings.resx b/src/EFCore/Properties/CoreStrings.resx index 6a703d51ccb..150a14f0b98 100644 --- a/src/EFCore/Properties/CoreStrings.resx +++ b/src/EFCore/Properties/CoreStrings.resx @@ -1457,6 +1457,9 @@ The LINQ expression '{expression}' could not be translated. Additional information: {details} Either rewrite the query in a form that can be translated, or switch to client evaluation explicitly by inserting a call to 'AsEnumerable', 'AsAsyncEnumerable', 'ToList', or 'ToListAsync'. See https://go.microsoft.com/fwlink/?linkid=2101038 for more information. + + The type '{type}' has been configured as '{typeConfiguration}', this conflicts with type '{otherType}' configured as '{otherTypeConfiguration}'. All base types and implemented interfaces must have the same configuration type. + The type '{type}' has not been configured as a shared type in the model. Before calling 'UsingEntity' add the entity type in the model as a shared entity. @@ -1466,6 +1469,9 @@ Unable to set 'IsUnique' to '{isUnique}' on the relationship associated with the navigation '{2_entityType}.{1_navigationName}' because the navigation has the opposite multiplicity. + + The type '{type}' cannot be configured as '{configuration}'. The current model building logic is unable to honor this configuration. + Unhandled expression node type '{nodeType}'. diff --git a/src/EFCore/Storage/TypeMappingSource.cs b/src/EFCore/Storage/TypeMappingSource.cs index 073d8c32f17..6564cdd58f5 100644 --- a/src/EFCore/Storage/TypeMappingSource.cs +++ b/src/EFCore/Storage/TypeMappingSource.cs @@ -84,7 +84,6 @@ protected TypeMappingSource(TypeMappingSourceDependencies dependencies) if (mapping == null) { var sourceType = info.ClrType; - if (sourceType != null) { foreach (var converterInfo in Dependencies diff --git a/src/Shared/SharedTypeExtensions.cs b/src/Shared/SharedTypeExtensions.cs index ff2409c9b6e..467584d0270 100644 --- a/src/Shared/SharedTypeExtensions.cs +++ b/src/Shared/SharedTypeExtensions.cs @@ -47,7 +47,8 @@ public static bool IsNullableType(this Type type) => !type.IsValueType || type.IsNullableValueType(); public static bool IsValidEntityType(this Type type) - => type.IsClass; + => type.IsClass + && !type.IsArray; public static bool IsPropertyBagType(this Type type) { diff --git a/test/EFCore.Cosmos.Tests/ModelBuilding/CosmosModelBuilderGenericTest.cs b/test/EFCore.Cosmos.Tests/ModelBuilding/CosmosModelBuilderGenericTest.cs index 99d3b08bfbd..3cf86eb58c3 100644 --- a/test/EFCore.Cosmos.Tests/ModelBuilding/CosmosModelBuilderGenericTest.cs +++ b/test/EFCore.Cosmos.Tests/ModelBuilding/CosmosModelBuilderGenericTest.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; +using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Metadata.Conventions; using Microsoft.EntityFrameworkCore.TestUtilities; using Xunit; @@ -30,6 +31,30 @@ public override void Properties_specified_by_string_are_shadow_properties_unless // Fails due to extra shadow properties } + [ConditionalFact] + public override void Properties_can_have_provider_type_set_for_type() + { + var modelBuilder = CreateModelBuilder(c => c.Properties().HaveConversion()); + + modelBuilder.Entity( + b => + { + b.Property(e => e.Up); + b.Property(e => e.Down); + b.Property("Charm"); + b.Property("Strange"); + b.Property("__id").HasConversion((Type)null); + }); + + var model = modelBuilder.FinalizeModel(); + var entityType = (IReadOnlyEntityType)model.FindEntityType(typeof(Quarks)); + + Assert.Null(entityType.FindProperty("Up").GetProviderClrType()); + Assert.Same(typeof(byte[]), entityType.FindProperty("Down").GetProviderClrType()); + Assert.Null(entityType.FindProperty("Charm").GetProviderClrType()); + Assert.Same(typeof(byte[]), entityType.FindProperty("Strange").GetProviderClrType()); + } + [ConditionalFact] public virtual void Partition_key_is_added_to_the_keys() { diff --git a/test/EFCore.Relational.Tests/Metadata/Conventions/Internal/RelationalPropertyMappingValidationConventionTest.cs b/test/EFCore.Relational.Tests/Metadata/Conventions/Internal/RelationalPropertyMappingValidationConventionTest.cs deleted file mode 100644 index 56ae9e337f3..00000000000 --- a/test/EFCore.Relational.Tests/Metadata/Conventions/Internal/RelationalPropertyMappingValidationConventionTest.cs +++ /dev/null @@ -1,64 +0,0 @@ -// 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 Microsoft.EntityFrameworkCore.Diagnostics; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Metadata.Internal; -using Microsoft.EntityFrameworkCore.Storage; -using Microsoft.EntityFrameworkCore.TestUtilities; -using Xunit; - -// ReSharper disable InconsistentNaming -namespace Microsoft.EntityFrameworkCore.Metadata.Conventions.Internal -{ - public class RelationalPropertyMappingValidationConventionTest : PropertyMappingValidationConventionTest - { - [ConditionalFact] - public void Throws_when_added_property_is_not_mapped_to_store() - { - var modelBuilder = CreateConventionlessInternalModelBuilder(); - var entityTypeBuilder = modelBuilder.Entity(typeof(NonPrimitiveAsPropertyEntity), ConfigurationSource.Convention); - entityTypeBuilder.Property(typeof(Tuple), "LongProperty", ConfigurationSource.Explicit); - entityTypeBuilder.Ignore(nameof(NonPrimitiveAsPropertyEntity.Property), ConfigurationSource.Explicit); - - Assert.Equal( - CoreStrings.PropertyNotMapped( - typeof(NonPrimitiveAsPropertyEntity).ShortDisplayName(), "LongProperty", typeof(Tuple).ShortDisplayName()), - Assert.Throws(() => CreatePropertyMappingValidator()(modelBuilder.Metadata)).Message); - } - - [ConditionalFact] - public void Throws_when_added_property_is_not_mapped_to_store_even_if_configured_to_use_column_type() - { - var modelBuilder = CreateConventionlessInternalModelBuilder(); - var entityTypeBuilder = modelBuilder.Entity(typeof(NonPrimitiveNonNavigationAsPropertyEntity), ConfigurationSource.Convention); - entityTypeBuilder.Property(typeof(Tuple), "LongProperty", ConfigurationSource.Explicit) - .HasColumnType("some_int_mapping"); - - Assert.Equal( - CoreStrings.PropertyNotMapped( - typeof(NonPrimitiveNonNavigationAsPropertyEntity).ShortDisplayName(), "LongProperty", - typeof(Tuple).ShortDisplayName()), - Assert.Throws(() => CreatePropertyMappingValidator()(modelBuilder.Metadata)).Message); - } - - protected override TestHelpers TestHelpers - => RelationalTestHelpers.Instance; - - protected override IModelValidator CreateModelValidator() - { - var typeMappingSource = new TestRelationalTypeMappingSource( - TestServiceFactory.Instance.Create(), - TestServiceFactory.Instance.Create()); - - return new RelationalModelValidator( - new ModelValidatorDependencies( - typeMappingSource, - new MemberClassifier( - typeMappingSource, - TestServiceFactory.Instance.Create())), - new RelationalModelValidatorDependencies(typeMappingSource)); - } - } -} diff --git a/test/EFCore.SqlServer.Tests/ModelBuilding/SqlServerModelBuilderGenericTest.cs b/test/EFCore.SqlServer.Tests/ModelBuilding/SqlServerModelBuilderGenericTest.cs index 8531d6afb74..328a3eae132 100644 --- a/test/EFCore.SqlServer.Tests/ModelBuilding/SqlServerModelBuilderGenericTest.cs +++ b/test/EFCore.SqlServer.Tests/ModelBuilding/SqlServerModelBuilderGenericTest.cs @@ -76,6 +76,96 @@ public virtual void Index_has_a_filter_if_nonclustered_unique_with_nullable_prop Assert.Null(index.GetFilter()); } + [ConditionalFact] + public virtual void Can_set_store_type_for_property_type() + { + var modelBuilder = CreateModelBuilder(c => + { + c.Properties().HaveColumnType("smallint"); + c.Properties().HaveColumnType("nchar(max)"); + }); + + modelBuilder.Entity( + b => + { + b.Property("Charm"); + b.Property("Strange"); + b.Property("Top"); + b.Property("Bottom"); + }); + + var model = modelBuilder.FinalizeModel(); + var entityType = model.FindEntityType(typeof(Quarks)); + + Assert.Equal("smallint", entityType.FindProperty(Customer.IdProperty.Name).GetColumnType()); + Assert.Equal("smallint", entityType.FindProperty("Up").GetColumnType()); + Assert.Equal("nchar(max)", entityType.FindProperty("Down").GetColumnType()); + Assert.Equal("smallint", entityType.FindProperty("Charm").GetColumnType()); + Assert.Equal("nchar(max)", entityType.FindProperty("Strange").GetColumnType()); + Assert.Equal("smallint", entityType.FindProperty("Top").GetColumnType()); + Assert.Equal("nchar(max)", entityType.FindProperty("Bottom").GetColumnType()); + } + + [ConditionalFact] + public virtual void Can_set_fixed_length_for_property_type() + { + var modelBuilder = CreateModelBuilder(c => + { + c.Properties().AreFixedLength(false); + c.Properties().AreFixedLength(); + }); + + modelBuilder.Entity( + b => + { + b.Property("Charm"); + b.Property("Strange"); + b.Property("Top"); + b.Property("Bottom"); + }); + + var model = modelBuilder.FinalizeModel(); + var entityType = model.FindEntityType(typeof(Quarks)); + + Assert.False(entityType.FindProperty(Customer.IdProperty.Name).IsFixedLength()); + Assert.False(entityType.FindProperty("Up").IsFixedLength()); + Assert.True(entityType.FindProperty("Down").IsFixedLength()); + Assert.False(entityType.FindProperty("Charm").IsFixedLength()); + Assert.True(entityType.FindProperty("Strange").IsFixedLength()); + Assert.False(entityType.FindProperty("Top").IsFixedLength()); + Assert.True(entityType.FindProperty("Bottom").IsFixedLength()); + } + + [ConditionalFact] + public virtual void Can_set_collation_for_property_type() + { + var modelBuilder = CreateModelBuilder(c => + { + c.Properties().UseCollation("Latin1_General_CS_AS_KS_WS"); + c.Properties().UseCollation("Latin1_General_BIN"); + }); + + modelBuilder.Entity( + b => + { + b.Property("Charm"); + b.Property("Strange"); + b.Property("Top"); + b.Property("Bottom"); + }); + + var model = modelBuilder.FinalizeModel(); + var entityType = model.FindEntityType(typeof(Quarks)); + + Assert.Equal("Latin1_General_CS_AS_KS_WS", entityType.FindProperty(Customer.IdProperty.Name).GetCollation()); + Assert.Equal("Latin1_General_CS_AS_KS_WS", entityType.FindProperty("Up").GetCollation()); + Assert.Equal("Latin1_General_BIN", entityType.FindProperty("Down").GetCollation()); + Assert.Equal("Latin1_General_CS_AS_KS_WS", entityType.FindProperty("Charm").GetCollation()); + Assert.Equal("Latin1_General_BIN", entityType.FindProperty("Strange").GetCollation()); + Assert.Equal("Latin1_General_CS_AS_KS_WS", entityType.FindProperty("Top").GetCollation()); + Assert.Equal("Latin1_General_BIN", entityType.FindProperty("Bottom").GetCollation()); + } + protected override TestModelBuilder CreateModelBuilder(Action configure = null) => CreateTestModelBuilder(SqlServerTestHelpers.Instance, configure); } @@ -92,12 +182,12 @@ public void Can_use_shadow_FK_that_collides_with_convention_shadow_FK_on_other_d .WithOne() .HasForeignKey("ParentId"); - modelBuilder.FinalizeModel(); + var model = modelBuilder.FinalizeModel(); - var property1 = modelBuilder.Model.FindEntityType(typeof(DisjointChildSubclass1)).FindProperty("ParentId"); + var property1 = model.FindEntityType(typeof(DisjointChildSubclass1)).FindProperty("ParentId"); Assert.True(property1.IsForeignKey()); Assert.Equal("ParentId", property1.GetColumnBaseName()); - var property2 = modelBuilder.Model.FindEntityType(typeof(DisjointChildSubclass2)).FindProperty("ParentId"); + var property2 = model.FindEntityType(typeof(DisjointChildSubclass2)).FindProperty("ParentId"); Assert.True(property2.IsForeignKey()); Assert.Equal("DisjointChildSubclass2_ParentId", property2.GetColumnBaseName()); } @@ -111,11 +201,11 @@ public void Inherited_clr_properties_are_mapped_to_the_same_column() modelBuilder.Entity(); modelBuilder.Entity(); - modelBuilder.FinalizeModel(); + var model = modelBuilder.FinalizeModel(); - var property1 = modelBuilder.Model.FindEntityType(typeof(DisjointChildSubclass1)).FindProperty(nameof(Child.Name)); + var property1 = model.FindEntityType(typeof(DisjointChildSubclass1)).FindProperty(nameof(Child.Name)); Assert.Equal(nameof(Child.Name), property1.GetColumnBaseName()); - var property2 = modelBuilder.Model.FindEntityType(typeof(DisjointChildSubclass2)).FindProperty(nameof(Child.Name)); + var property2 = model.FindEntityType(typeof(DisjointChildSubclass2)).FindProperty(nameof(Child.Name)); Assert.Equal(nameof(Child.Name), property2.GetColumnBaseName()); } diff --git a/test/EFCore.Tests/ApiConsistencyTest.cs b/test/EFCore.Tests/ApiConsistencyTest.cs index e52d4bac0d3..00b8e7de83a 100644 --- a/test/EFCore.Tests/ApiConsistencyTest.cs +++ b/test/EFCore.Tests/ApiConsistencyTest.cs @@ -162,6 +162,7 @@ protected override void Initialize() typeof(IConventionAnnotatable).GetMethod(nameof(IConventionAnnotatable.SetOrRemoveAnnotation)), typeof(IConventionAnnotatable).GetMethod(nameof(IConventionAnnotatable.AddAnnotations)), typeof(IMutableAnnotatable).GetMethod(nameof(IMutableAnnotatable.AddAnnotations)), + typeof(IConventionModel).GetMethod(nameof(IConventionModel.IsIgnoredType)), typeof(IConventionModel).GetMethod(nameof(IConventionModel.IsShared)), typeof(IConventionModel).GetMethod(nameof(IConventionModel.AddOwned)), typeof(IConventionModel).GetMethod(nameof(IConventionModel.AddShared)), diff --git a/test/EFCore.Tests/Infrastructure/ModelValidatorTest.PropertyMapping.cs b/test/EFCore.Tests/Infrastructure/ModelValidatorTest.PropertyMapping.cs index 11aff80b3e8..13dfd09d990 100644 --- a/test/EFCore.Tests/Infrastructure/ModelValidatorTest.PropertyMapping.cs +++ b/test/EFCore.Tests/Infrastructure/ModelValidatorTest.PropertyMapping.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Collections; using System.Collections.Generic; using System.Threading; using Microsoft.EntityFrameworkCore.Diagnostics; @@ -58,7 +59,7 @@ public virtual void Throws_when_primitive_type_property_is_not_added_or_ignored( public virtual void Throws_when_nonprimitive_value_type_property_is_not_added_or_ignored() { var modelBuilder = CreateConventionlessModelBuilder(); - modelBuilder.Entity(typeof(NonPrimitiveValueTypePropertyEntity)); + modelBuilder.Entity(typeof(NonPrimitiveValueTypePropertyEntity)).HasNoKey(); Assert.Equal( CoreStrings.PropertyNotAdded( @@ -66,6 +67,16 @@ public virtual void Throws_when_nonprimitive_value_type_property_is_not_added_or Assert.Throws(() => Validate(modelBuilder)).Message); } + [ConditionalFact] + public virtual void Does_not_throw_when_nonprimitive_value_type_property_type_is_ignored() + { + var modelBuilder = CreateConventionlessModelBuilder( + configurationBuilder => configurationBuilder.IgnoreAny()); + modelBuilder.Entity(typeof(NonPrimitiveValueTypePropertyEntity)).HasNoKey(); + + Validate(modelBuilder); + } + [ConditionalFact] public virtual void Throws_when_keyless_type_property_is_not_added_or_ignored() { @@ -140,6 +151,16 @@ public virtual void Does_not_throw_when_navigation_is_ignored() Validate(modelBuilder); } + [ConditionalFact] + public virtual void Does_not_throw_when_navigation_type_is_ignored() + { + var modelBuilder = CreateConventionlessModelBuilder( + configurationBuilder => configurationBuilder.IgnoreAny()); + modelBuilder.Entity(typeof(NavigationEntity)).HasNoKey(); + + Validate(modelBuilder); + } + [ConditionalFact] public virtual void Does_not_throw_when_navigation_target_entity_is_ignored() { @@ -171,7 +192,7 @@ public virtual void Does_not_throw_when_explicit_navigation_is_not_added() public virtual void Throws_when_interface_type_property_is_not_added_or_ignored() { var modelBuilder = CreateConventionlessModelBuilder(); - modelBuilder.Entity(typeof(InterfaceNavigationEntity)); + modelBuilder.Entity(typeof(InterfaceNavigationEntity)).HasNoKey(); Assert.Equal( CoreStrings.InterfacePropertyNotAdded( @@ -181,6 +202,36 @@ public virtual void Throws_when_interface_type_property_is_not_added_or_ignored( Assert.Throws(() => Validate(modelBuilder)).Message); } + [ConditionalFact] + public virtual void Does_not_throw_when_interface_collection_type_property_type_is_ignored() + { + var modelBuilder = CreateConventionlessModelBuilder( + configurationBuilder => configurationBuilder.IgnoreAny()); + modelBuilder.Entity(typeof(InterfaceNavigationEntity)).HasNoKey(); + + Validate(modelBuilder); + } + + [ConditionalFact] + public virtual void Does_not_throw_when_interface_generic_type_property_type_is_ignored() + { + var modelBuilder = CreateConventionlessModelBuilder( + configurationBuilder => configurationBuilder.IgnoreAny(typeof(IList<>))); + modelBuilder.Entity(typeof(InterfaceNavigationEntity)).HasNoKey(); + + Validate(modelBuilder); + } + + [ConditionalFact] + public virtual void Does_not_throw_when_interface_base_type_property_type_is_ignored() + { + var modelBuilder = CreateConventionlessModelBuilder( + configurationBuilder => configurationBuilder.IgnoreAny()); + modelBuilder.Entity(typeof(InterfaceNavigationEntity)).HasNoKey(); + + Validate(modelBuilder); + } + [ConditionalFact] public virtual void Does_not_throw_when_non_candidate_property_is_not_added() { diff --git a/test/EFCore.Tests/Infrastructure/ModelValidatorTest.cs b/test/EFCore.Tests/Infrastructure/ModelValidatorTest.cs index f8df09899c7..b76a2ddae1e 100644 --- a/test/EFCore.Tests/Infrastructure/ModelValidatorTest.cs +++ b/test/EFCore.Tests/Infrastructure/ModelValidatorTest.cs @@ -1111,7 +1111,7 @@ public virtual void Detects_missing_signed_integer_key_values_in_seeds() [InlineData(false)] public virtual void Detects_duplicate_seeds(bool sensitiveDataLoggingEnabled) { - var modelBuilder = CreateConventionalModelBuilder(sensitiveDataLoggingEnabled); + var modelBuilder = CreateConventionalModelBuilder(sensitiveDataLoggingEnabled: sensitiveDataLoggingEnabled); modelBuilder.Entity().HasData( new A { Id = 1 }); modelBuilder.Entity().HasData( @@ -1130,7 +1130,7 @@ public virtual void Detects_duplicate_seeds(bool sensitiveDataLoggingEnabled) [InlineData(false)] public virtual void Detects_incompatible_values(bool sensitiveDataLoggingEnabled) { - var modelBuilder = CreateConventionalModelBuilder(sensitiveDataLoggingEnabled); + var modelBuilder = CreateConventionalModelBuilder(sensitiveDataLoggingEnabled: sensitiveDataLoggingEnabled); modelBuilder.Entity( e => { @@ -1151,7 +1151,7 @@ public virtual void Detects_incompatible_values(bool sensitiveDataLoggingEnabled [InlineData(false)] public virtual void Detects_reference_navigations_in_seeds(bool sensitiveDataLoggingEnabled) { - var modelBuilder = CreateConventionalModelBuilder(sensitiveDataLoggingEnabled); + var modelBuilder = CreateConventionalModelBuilder(sensitiveDataLoggingEnabled: sensitiveDataLoggingEnabled); modelBuilder.Entity( e => { @@ -1181,7 +1181,7 @@ public virtual void Detects_reference_navigations_in_seeds(bool sensitiveDataLog [InlineData(false)] public virtual void Detects_reference_navigations_in_seeds2(bool sensitiveDataLoggingEnabled) { - var modelBuilder = CreateConventionalModelBuilder(sensitiveDataLoggingEnabled); + var modelBuilder = CreateConventionalModelBuilder(sensitiveDataLoggingEnabled: sensitiveDataLoggingEnabled); modelBuilder.Entity( e => { @@ -1213,7 +1213,7 @@ public virtual void Detects_reference_navigations_in_seeds2(bool sensitiveDataLo [InlineData(false)] public virtual void Detects_collection_navigations_in_seeds(bool sensitiveDataLoggingEnabled) { - var modelBuilder = CreateConventionalModelBuilder(sensitiveDataLoggingEnabled); + var modelBuilder = CreateConventionalModelBuilder(sensitiveDataLoggingEnabled: sensitiveDataLoggingEnabled); modelBuilder.Entity( e => { diff --git a/test/EFCore.Tests/Infrastructure/ModelValidatorTestBase.cs b/test/EFCore.Tests/Infrastructure/ModelValidatorTestBase.cs index cd4ff06b5ef..340cf72987d 100644 --- a/test/EFCore.Tests/Infrastructure/ModelValidatorTestBase.cs +++ b/test/EFCore.Tests/Infrastructure/ModelValidatorTestBase.cs @@ -315,14 +315,21 @@ protected virtual IModel Validate(TestHelpers.TestModelBuilder modelBuilder, boo new NullDbContextLogger()); } - protected virtual TestHelpers.TestModelBuilder CreateConventionalModelBuilder(bool sensitiveDataLoggingEnabled = false) + protected virtual TestHelpers.TestModelBuilder CreateConventionalModelBuilder( + Action configure = null, bool sensitiveDataLoggingEnabled = false) => TestHelpers.CreateConventionBuilder( - CreateModelLogger(sensitiveDataLoggingEnabled), CreateValidationLogger(sensitiveDataLoggingEnabled)); + CreateModelLogger(sensitiveDataLoggingEnabled), CreateValidationLogger(sensitiveDataLoggingEnabled), + configurationBuilder => configure?.Invoke(configurationBuilder)); - protected virtual TestHelpers.TestModelBuilder CreateConventionlessModelBuilder(bool sensitiveDataLoggingEnabled = false) + protected virtual TestHelpers.TestModelBuilder CreateConventionlessModelBuilder( + Action configure = null, bool sensitiveDataLoggingEnabled = false) => TestHelpers.CreateConventionBuilder( CreateModelLogger(sensitiveDataLoggingEnabled), CreateValidationLogger(sensitiveDataLoggingEnabled), - configurationBuilder => configurationBuilder.RemoveAllConventions()); + configurationBuilder => + { + configure?.Invoke(configurationBuilder); + configurationBuilder.RemoveAllConventions(); + }); protected virtual TestHelpers TestHelpers => InMemoryTestHelpers.Instance; diff --git a/test/EFCore.Tests/Metadata/Conventions/PropertyMappingValidationConventionTest.cs b/test/EFCore.Tests/Metadata/Conventions/PropertyMappingValidationConventionTest.cs deleted file mode 100644 index 1473e1dabc5..00000000000 --- a/test/EFCore.Tests/Metadata/Conventions/PropertyMappingValidationConventionTest.cs +++ /dev/null @@ -1,292 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using System.Threading; -using Microsoft.EntityFrameworkCore.ChangeTracking.Internal; -using Microsoft.EntityFrameworkCore.Diagnostics; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.InMemory.Storage.Internal; -using Microsoft.EntityFrameworkCore.Metadata.Conventions.Infrastructure; -using Microsoft.EntityFrameworkCore.Metadata.Internal; -using Microsoft.EntityFrameworkCore.Query; -using Microsoft.EntityFrameworkCore.Storage; -using Microsoft.EntityFrameworkCore.TestUtilities; -using Microsoft.EntityFrameworkCore.Update; -using Microsoft.Extensions.DependencyInjection; -using Xunit; - -// ReSharper disable InconsistentNaming -namespace Microsoft.EntityFrameworkCore.Metadata.Conventions -{ - public class PropertyMappingValidationConventionTest : ModelValidatorTestBase - { - [ConditionalFact] - public virtual void Throws_when_added_property_is_not_of_primitive_type() - { - var modelBuilder = CreateConventionlessInternalModelBuilder(); - var entityTypeBuilder = modelBuilder.Entity(typeof(NonPrimitiveAsPropertyEntity), ConfigurationSource.Convention); - entityTypeBuilder.Property( - typeof(NavigationAsProperty), nameof(NonPrimitiveAsPropertyEntity.Property), ConfigurationSource.Convention); - - Assert.Equal( - CoreStrings.PropertyNotMapped( - typeof(NonPrimitiveAsPropertyEntity).ShortDisplayName(), - nameof(NonPrimitiveAsPropertyEntity.Property), - typeof(NavigationAsProperty).ShortDisplayName()), - Assert.Throws(() => CreatePropertyMappingValidator()(modelBuilder.Metadata)).Message); - } - - [ConditionalFact] - public virtual void Does_not_throw_when_added_shadow_property_by_convention_is_not_of_primitive_type() - { - var modelBuilder = CreateConventionlessInternalModelBuilder(); - var entityTypeBuilder = modelBuilder.Entity(typeof(NonPrimitiveAsPropertyEntity), ConfigurationSource.Convention); - entityTypeBuilder.Property(typeof(NavigationAsProperty), "ShadowProperty", ConfigurationSource.Convention); - entityTypeBuilder.Ignore(nameof(NonPrimitiveAsPropertyEntity.Property), ConfigurationSource.Explicit); - - CreatePropertyMappingValidator()(modelBuilder.Metadata); - } - - [ConditionalFact] - public virtual void Throws_when_primitive_type_property_is_not_added_or_ignored() - { - var modelBuilder = CreateConventionlessInternalModelBuilder(); - modelBuilder.Entity(typeof(PrimitivePropertyEntity), ConfigurationSource.Convention); - - Assert.Equal( - CoreStrings.PropertyNotAdded( - typeof(PrimitivePropertyEntity).ShortDisplayName(), "Property", typeof(int).DisplayName()), - Assert.Throws(() => CreatePropertyMappingValidator()(modelBuilder.Metadata)).Message); - } - - [ConditionalFact] - public virtual void Throws_when_nonprimitive_value_type_property_is_not_added_or_ignored() - { - var modelBuilder = CreateConventionlessInternalModelBuilder(); - modelBuilder.Entity(typeof(NonPrimitiveValueTypePropertyEntity), ConfigurationSource.Convention); - - Assert.Equal( - CoreStrings.PropertyNotAdded( - typeof(NonPrimitiveValueTypePropertyEntity).ShortDisplayName(), "Property", typeof(CancellationToken).Name), - Assert.Throws(() => CreatePropertyMappingValidator()(modelBuilder.Metadata)).Message); - } - - [ConditionalFact] - public virtual void Throws_when_keyless_type_property_is_not_added_or_ignored() - { - var modelBuilder = CreateConventionlessInternalModelBuilder(); - modelBuilder.Entity(typeof(NonPrimitiveReferenceTypePropertyEntity), ConfigurationSource.Convention); - - Assert.Equal( - CoreStrings.PropertyNotAdded( - typeof(NonPrimitiveReferenceTypePropertyEntity).ShortDisplayName(), - nameof(NonPrimitiveReferenceTypePropertyEntity.Property), - typeof(ICollection).ShortDisplayName()), - Assert.Throws(() => CreatePropertyMappingValidator()(modelBuilder.Metadata)).Message); - } - - [ConditionalFact] - public virtual void Does_not_throw_when_primitive_type_property_is_added() - { - var modelBuilder = CreateConventionlessInternalModelBuilder(); - var entityTypeBuilder = modelBuilder.Entity(typeof(PrimitivePropertyEntity), ConfigurationSource.Convention); - entityTypeBuilder.Property(typeof(int), "Property", ConfigurationSource.Convention); - - CreatePropertyMappingValidator()(modelBuilder.Metadata); - } - - [ConditionalFact] - public virtual void Does_not_throw_when_primitive_type_property_is_ignored() - { - var modelBuilder = CreateConventionlessInternalModelBuilder(); - var entityTypeBuilder = modelBuilder.Entity(typeof(PrimitivePropertyEntity), ConfigurationSource.Convention); - entityTypeBuilder.Ignore("Property", ConfigurationSource.DataAnnotation); - - CreatePropertyMappingValidator()(modelBuilder.Metadata); - } - - [ConditionalFact] - public virtual void Throws_when_navigation_is_not_added_or_ignored() - { - var modelBuilder = CreateConventionlessInternalModelBuilder(); - modelBuilder.Entity(typeof(NavigationEntity), ConfigurationSource.Convention); - modelBuilder.Entity(typeof(PrimitivePropertyEntity), ConfigurationSource.Convention); - - Assert.Equal( - CoreStrings.NavigationNotAdded( - typeof(NavigationEntity).ShortDisplayName(), "Navigation", typeof(PrimitivePropertyEntity).Name), - Assert.Throws(() => CreatePropertyMappingValidator()(modelBuilder.Metadata)).Message); - } - - [ConditionalFact] - public virtual void Does_not_throw_when_navigation_is_added() - { - var modelBuilder = CreateConventionlessInternalModelBuilder(); - var entityTypeBuilder = modelBuilder.Entity(typeof(NavigationEntity), ConfigurationSource.Convention); - var referencedEntityTypeBuilder = modelBuilder.Entity(typeof(PrimitivePropertyEntity), ConfigurationSource.Convention); - referencedEntityTypeBuilder.Ignore("Property", ConfigurationSource.DataAnnotation); - entityTypeBuilder.HasRelationship(referencedEntityTypeBuilder.Metadata, "Navigation", null, ConfigurationSource.Convention); - - CreatePropertyMappingValidator()(modelBuilder.Metadata); - } - - [ConditionalFact] - public virtual void Does_not_throw_when_navigation_is_ignored() - { - var modelBuilder = CreateConventionlessInternalModelBuilder(); - var entityTypeBuilder = modelBuilder.Entity(typeof(NavigationEntity), ConfigurationSource.Convention); - entityTypeBuilder.Ignore("Navigation", ConfigurationSource.DataAnnotation); - - CreatePropertyMappingValidator()(modelBuilder.Metadata); - } - - [ConditionalFact] - public virtual void Does_not_throw_when_navigation_target_entity_is_ignored() - { - var modelBuilder = CreateConventionlessInternalModelBuilder(); - modelBuilder.Entity(typeof(NavigationEntity), ConfigurationSource.Convention); - modelBuilder.Ignore(typeof(PrimitivePropertyEntity), ConfigurationSource.Convention); - - CreatePropertyMappingValidator()(modelBuilder.Metadata); - } - - [ConditionalFact] - public virtual void Does_not_throw_when_explicit_navigation_is_not_added() - { - var modelBuilder = CreateConventionlessInternalModelBuilder(); - var entityTypeBuilder = modelBuilder.Entity(typeof(ExplicitNavigationEntity), ConfigurationSource.Convention); - var referencedEntityTypeBuilder = modelBuilder.Entity(typeof(PrimitivePropertyEntity), ConfigurationSource.Convention); - referencedEntityTypeBuilder.Ignore("Property", ConfigurationSource.DataAnnotation); - entityTypeBuilder.HasRelationship( - referencedEntityTypeBuilder.Metadata, "Navigation", null, ConfigurationSource.Convention); - - CreatePropertyMappingValidator()(modelBuilder.Metadata); - } - - [ConditionalFact] - public virtual void Throws_when_interface_type_property_is_not_added_or_ignored() - { - var modelBuilder = CreateConventionlessInternalModelBuilder(); - modelBuilder.Entity(typeof(InterfaceNavigationEntity), ConfigurationSource.Convention); - - Assert.Equal( - CoreStrings.InterfacePropertyNotAdded( - typeof(InterfaceNavigationEntity).ShortDisplayName(), - "Navigation", - typeof(IList).ShortDisplayName()), - Assert.Throws(() => CreatePropertyMappingValidator()(modelBuilder.Metadata)).Message); - } - - [ConditionalFact] - public virtual void Does_not_throw_when_non_candidate_property_is_not_added() - { - var modelBuilder = CreateConventionlessInternalModelBuilder(); - modelBuilder.Entity(typeof(NonCandidatePropertyEntity), ConfigurationSource.Convention); - - CreatePropertyMappingValidator()(modelBuilder.Metadata); - } - - protected virtual Action CreatePropertyMappingValidator() - { - var validator = CreateModelValidator(); - var logger = new TestLogger(); - - var validatePropertyMappingMethod = typeof(ModelValidator).GetRuntimeMethods().Single(e => e.Name == "ValidatePropertyMapping"); - - var modelRuntimeInitializer = TestHelpers.CreateContextServices().GetRequiredService(); - - return m => - { - try - { - modelRuntimeInitializer.Initialize(m, designTime: false, validationLogger: null); - validatePropertyMappingMethod.Invoke( - validator, new object[] { m, logger }); - } - catch (TargetInvocationException exception) - { - throw exception.InnerException; - } - }; - } - - protected virtual IModelValidator CreateModelValidator() - { - var typeMappingSource = new InMemoryTypeMappingSource( - TestServiceFactory.Instance.Create()); - - return new ModelValidator( - new ModelValidatorDependencies( - typeMappingSource, - new MemberClassifier( - typeMappingSource, - TestServiceFactory.Instance.Create()))); - } - - protected class NonPrimitiveNonNavigationAsPropertyEntity - { - } - - protected class NonPrimitiveAsPropertyEntity - { - public NavigationAsProperty Property { get; set; } - } - - protected class NavigationAsProperty - { - } - - protected class PrimitivePropertyEntity - { - public int Property { get; set; } - } - - protected class NonPrimitiveValueTypePropertyEntity - { - public CancellationToken Property { get; set; } - } - - protected class NonPrimitiveReferenceTypePropertyEntity - { - public ICollection Property { get; set; } - } - - protected class NavigationEntity - { - public PrimitivePropertyEntity Navigation { get; set; } - } - - protected class NonCandidatePropertyEntity - { - public static int StaticProperty { get; set; } - - public int _writeOnlyField = 1; - - public int WriteOnlyProperty - { - set => _writeOnlyField = value; - } - } - - protected interface INavigationEntity - { - PrimitivePropertyEntity Navigation { get; set; } - } - - protected class ExplicitNavigationEntity : INavigationEntity - { - PrimitivePropertyEntity INavigationEntity.Navigation { get; set; } - - public PrimitivePropertyEntity Navigation { get; set; } - } - - protected class InterfaceNavigationEntity - { - public IList Navigation { get; set; } - } - } -} diff --git a/test/EFCore.Tests/Metadata/Internal/InternalEntityTypeBuilderTest.cs b/test/EFCore.Tests/Metadata/Internal/InternalEntityTypeBuilderTest.cs index 6aa694f8e6b..00211990f85 100644 --- a/test/EFCore.Tests/Metadata/Internal/InternalEntityTypeBuilderTest.cs +++ b/test/EFCore.Tests/Metadata/Internal/InternalEntityTypeBuilderTest.cs @@ -2199,22 +2199,26 @@ public void Navigation_returns_same_value() var foreignKeyBuilder = dependentEntityBuilder.HasRelationship( principalEntityBuilder.Metadata, ConfigurationSource.DataAnnotation); - Assert.True(dependentEntityBuilder.CanHaveNavigation(Order.CustomerProperty.Name, ConfigurationSource.Convention)); + Assert.True(dependentEntityBuilder.CanHaveNavigation( + Order.CustomerProperty.Name, Order.CustomerProperty.GetMemberType(), ConfigurationSource.Convention)); foreignKeyBuilder = foreignKeyBuilder.HasNavigation( Order.CustomerProperty.Name, pointsToPrincipal: true, ConfigurationSource.DataAnnotation); - Assert.True(dependentEntityBuilder.CanHaveNavigation(Order.CustomerProperty.Name, ConfigurationSource.Explicit)); - Assert.True(principalEntityBuilder.CanHaveNavigation(Customer.OrdersProperty.Name, ConfigurationSource.Convention)); + Assert.True(dependentEntityBuilder.CanHaveNavigation( + Order.CustomerProperty.Name, Order.CustomerProperty.GetMemberType(), ConfigurationSource.Explicit)); + Assert.True(principalEntityBuilder.CanHaveNavigation( + Customer.OrdersProperty.Name, Customer.OrdersProperty.GetMemberType(), ConfigurationSource.Convention)); foreignKeyBuilder = foreignKeyBuilder.HasNavigation( Customer.OrdersProperty.Name, pointsToPrincipal: false, ConfigurationSource.DataAnnotation); - Assert.True(principalEntityBuilder.CanHaveNavigation(Customer.OrdersProperty.Name, ConfigurationSource.Explicit)); + Assert.True(principalEntityBuilder.CanHaveNavigation( + Customer.OrdersProperty.Name, Customer.OrdersProperty.GetMemberType(), ConfigurationSource.Explicit)); var newForeignKeyBuilder = dependentEntityBuilder .HasRelationship(principalEntityBuilder.Metadata, ConfigurationSource.Convention).HasNavigation( diff --git a/test/EFCore.Tests/ModelBuilding/ModelBuilderGenericTest.cs b/test/EFCore.Tests/ModelBuilding/ModelBuilderGenericTest.cs index 2acab8cdf65..78ce0e36021 100644 --- a/test/EFCore.Tests/ModelBuilding/ModelBuilderGenericTest.cs +++ b/test/EFCore.Tests/ModelBuilding/ModelBuilderGenericTest.cs @@ -466,6 +466,9 @@ public override TestPropertyBuilder IsRequired(bool isRequired = true public override TestPropertyBuilder HasMaxLength(int maxLength) => new GenericTestPropertyBuilder(PropertyBuilder.HasMaxLength(maxLength)); + public override TestPropertyBuilder HasPrecision(int precision, int scale) + => new GenericTestPropertyBuilder(PropertyBuilder.HasPrecision(precision, scale)); + public override TestPropertyBuilder IsUnicode(bool unicode = true) => new GenericTestPropertyBuilder(PropertyBuilder.IsUnicode(unicode)); diff --git a/test/EFCore.Tests/ModelBuilding/ModelBuilderNonGenericTest.cs b/test/EFCore.Tests/ModelBuilding/ModelBuilderNonGenericTest.cs index fc36aed2658..5a833a1d46f 100644 --- a/test/EFCore.Tests/ModelBuilding/ModelBuilderNonGenericTest.cs +++ b/test/EFCore.Tests/ModelBuilding/ModelBuilderNonGenericTest.cs @@ -543,6 +543,9 @@ public override TestPropertyBuilder IsRequired(bool isRequired = true public override TestPropertyBuilder HasMaxLength(int maxLength) => new NonGenericTestPropertyBuilder(PropertyBuilder.HasMaxLength(maxLength)); + public override TestPropertyBuilder HasPrecision(int precision, int scale) + => new NonGenericTestPropertyBuilder(PropertyBuilder.HasPrecision(precision, scale)); + public override TestPropertyBuilder IsUnicode(bool unicode = true) => new NonGenericTestPropertyBuilder(PropertyBuilder.IsUnicode(unicode)); diff --git a/test/EFCore.Tests/ModelBuilding/ModelBuilderTestBase.cs b/test/EFCore.Tests/ModelBuilding/ModelBuilderTestBase.cs index 3535f9758b0..6ea27d822d1 100644 --- a/test/EFCore.Tests/ModelBuilding/ModelBuilderTestBase.cs +++ b/test/EFCore.Tests/ModelBuilding/ModelBuilderTestBase.cs @@ -379,6 +379,7 @@ public abstract class TestPropertyBuilder public abstract TestPropertyBuilder HasAnnotation(string annotation, object? value); public abstract TestPropertyBuilder IsRequired(bool isRequired = true); public abstract TestPropertyBuilder HasMaxLength(int maxLength); + public abstract TestPropertyBuilder HasPrecision(int precision, int scale); public abstract TestPropertyBuilder IsUnicode(bool unicode = true); public abstract TestPropertyBuilder IsRowVersion(); public abstract TestPropertyBuilder IsConcurrencyToken(bool isConcurrencyToken = true); diff --git a/test/EFCore.Tests/ModelBuilding/NonRelationshipTestBase.cs b/test/EFCore.Tests/ModelBuilding/NonRelationshipTestBase.cs index df77f975a73..01c433b42e3 100644 --- a/test/EFCore.Tests/ModelBuilding/NonRelationshipTestBase.cs +++ b/test/EFCore.Tests/ModelBuilding/NonRelationshipTestBase.cs @@ -2,7 +2,9 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Collections; using System.Collections.Generic; +using System.ComponentModel; using System.Dynamic; using System.Linq; using System.Text; @@ -311,11 +313,13 @@ public virtual void Can_set_property_annotation() { var modelBuilder = CreateModelBuilder(); - var propertyBuilder = modelBuilder + modelBuilder .Entity() .Property(c => c.Name).HasAnnotation("foo", "bar"); - Assert.Equal("bar", propertyBuilder.Metadata["foo"]); + var property = modelBuilder.FinalizeModel().FindEntityType(typeof(Customer)).FindProperty(nameof(Customer.Name)); + + Assert.Equal("bar", property["foo"]); } [ConditionalFact] @@ -323,18 +327,33 @@ public virtual void Can_set_property_annotation_when_no_clr_property() { var modelBuilder = CreateModelBuilder(); - var propertyBuilder = modelBuilder + modelBuilder .Entity() .Property(Customer.NameProperty.Name).HasAnnotation("foo", "bar"); - Assert.Equal("bar", propertyBuilder.Metadata["foo"]); + var property = modelBuilder.FinalizeModel().FindEntityType(typeof(Customer)).FindProperty(nameof(Customer.Name)); + + Assert.Equal("bar", property["foo"]); + } + + [ConditionalFact] + public virtual void Can_set_property_annotation_by_type() + { + var modelBuilder = CreateModelBuilder(c => c.Properties().HaveAnnotation("foo", "bar")); + + var propertyBuilder = modelBuilder + .Entity() + .Property(c => c.Name).HasAnnotation("foo", "bar"); + + var property = modelBuilder.FinalizeModel().FindEntityType(typeof(Customer)).FindProperty(nameof(Customer.Name)); + + Assert.Equal("bar", property["foo"]); } [ConditionalFact] public virtual void Properties_are_required_by_default_only_if_CLR_type_is_nullable() { var modelBuilder = CreateModelBuilder(); - var model = modelBuilder.Model; modelBuilder.Entity( b => @@ -347,7 +366,7 @@ public virtual void Properties_are_required_by_default_only_if_CLR_type_is_nulla b.Property("Bottom"); }); - var entityType = (IReadOnlyEntityType)model.FindEntityType(typeof(Quarks)); + var entityType = modelBuilder.FinalizeModel().FindEntityType(typeof(Quarks)); Assert.False(entityType.FindProperty("Up").IsNullable); Assert.True(entityType.FindProperty("Down").IsNullable); @@ -362,8 +381,6 @@ public virtual void Properties_can_be_ignored() { var modelBuilder = CreateModelBuilder(); - var entityType = (IReadOnlyEntityType)modelBuilder.Entity().Metadata; - modelBuilder.Entity( b => { @@ -376,11 +393,30 @@ public virtual void Properties_can_be_ignored() b.Ignore("Shadow"); }); + var entityType = modelBuilder.FinalizeModel().FindEntityType(typeof(Quarks)); Assert.Contains(nameof(Quarks.Id), entityType.GetProperties().Select(p => p.Name)); Assert.DoesNotContain(nameof(Quarks.Up), entityType.GetProperties().Select(p => p.Name)); Assert.DoesNotContain(nameof(Quarks.Down), entityType.GetProperties().Select(p => p.Name)); } + [ConditionalFact] + public virtual void Properties_can_be_ignored_by_type() + { + var modelBuilder = CreateModelBuilder(c => c.IgnoreAny()); + + modelBuilder.Entity(); + + var entityType = modelBuilder.FinalizeModel().FindEntityType(typeof(Customer)); + Assert.Null(entityType.FindProperty(nameof(Customer.AlternateKey))); + } + + [ConditionalFact] + public virtual void Int32_cannot_be_ignored() + { + Assert.Equal(CoreStrings.UnconfigurableType("int", "Ignored"), + Assert.Throws(() => CreateModelBuilder(c => c.IgnoreAny())).Message); + } + [ConditionalFact] public virtual void Can_ignore_a_property_that_is_part_of_explicit_entity_key() { @@ -417,7 +453,9 @@ public virtual void Can_add_shadow_properties_when_they_have_been_ignored() b.Property("Shadow"); }); - Assert.NotNull(modelBuilder.Model.FindEntityType(typeof(Customer)).FindProperty("Shadow")); + var model = modelBuilder.FinalizeModel(); + + Assert.NotNull(model.FindEntityType(typeof(Customer)).FindProperty("Shadow")); } [ConditionalFact] @@ -440,7 +478,7 @@ public virtual void Can_override_navigations_as_properties() public virtual void Ignoring_a_navigation_property_removes_discovered_entity_types() { var modelBuilder = CreateModelBuilder(); - var model = modelBuilder.Model; + modelBuilder.Entity( b => { @@ -448,7 +486,7 @@ public virtual void Ignoring_a_navigation_property_removes_discovered_entity_typ b.Ignore(c => c.Orders); }); - modelBuilder.FinalizeModel(); + var model = modelBuilder.FinalizeModel(); Assert.Single(model.GetEntityTypes()); } @@ -457,7 +495,7 @@ public virtual void Ignoring_a_navigation_property_removes_discovered_entity_typ public virtual void Ignoring_a_navigation_property_removes_discovered_relationship() { var modelBuilder = CreateModelBuilder(); - var model = modelBuilder.Model; + modelBuilder.Entity( b => { @@ -466,18 +504,36 @@ public virtual void Ignoring_a_navigation_property_removes_discovered_relationsh }); modelBuilder.Entity(b => b.Ignore(c => c.Customer)); - modelBuilder.FinalizeModel(); + var model = modelBuilder.FinalizeModel(); Assert.Empty(model.GetEntityTypes().First().GetForeignKeys()); Assert.Empty(model.GetEntityTypes().Last().GetForeignKeys()); Assert.Equal(2, model.GetEntityTypes().Count()); } + [ConditionalFact] + public virtual void Ignoring_a_base_type_removes_relationships() + { + var modelBuilder = CreateModelBuilder(c => c.IgnoreAny()); + + modelBuilder.Entity(); + + var model = modelBuilder.FinalizeModel(); + + Assert.Empty(model.GetEntityTypes().Single().GetForeignKeys()); + } + + [ConditionalFact] + public virtual void Object_cannot_be_ignored() + { + Assert.Equal(CoreStrings.UnconfigurableType("object", "Ignored"), + Assert.Throws(() => CreateModelBuilder(c => c.IgnoreAny())).Message); + } + [ConditionalFact] public virtual void Properties_can_be_made_required() { var modelBuilder = CreateModelBuilder(); - var model = modelBuilder.Model; modelBuilder.Entity( b => @@ -490,6 +546,7 @@ public virtual void Properties_can_be_made_required() b.Property("Bottom").IsRequired(); }); + var model = modelBuilder.FinalizeModel(); var entityType = (IReadOnlyEntityType)model.FindEntityType(typeof(Quarks)); Assert.False(entityType.FindProperty("Up").IsNullable); @@ -504,7 +561,6 @@ public virtual void Properties_can_be_made_required() public virtual void Properties_can_be_made_optional() { var modelBuilder = CreateModelBuilder(); - var model = modelBuilder.Model; modelBuilder.Entity( b => @@ -514,6 +570,7 @@ public virtual void Properties_can_be_made_optional() b.Property("Bottom").IsRequired(false); }); + var model = modelBuilder.FinalizeModel(); var entityType = (IReadOnlyEntityType)model.FindEntityType(typeof(Quarks)); Assert.True(entityType.FindProperty("Down").IsNullable); @@ -541,7 +598,6 @@ public virtual void Key_properties_cannot_be_made_optional() public virtual void Non_nullable_properties_cannot_be_made_optional() { var modelBuilder = CreateModelBuilder(); - var model = modelBuilder.Model; modelBuilder.Entity( b => @@ -559,6 +615,7 @@ public virtual void Non_nullable_properties_cannot_be_made_optional() Assert.Throws(() => b.Property("Top").IsRequired(false)).Message); }); + var model = modelBuilder.FinalizeModel(); var entityType = (IReadOnlyEntityType)model.FindEntityType(typeof(Quarks)); Assert.False(entityType.FindProperty("Up").IsNullable); @@ -570,7 +627,6 @@ public virtual void Non_nullable_properties_cannot_be_made_optional() public virtual void Properties_specified_by_string_are_shadow_properties_unless_already_known_to_be_CLR_properties() { var modelBuilder = CreateModelBuilder(); - var model = modelBuilder.Model; modelBuilder.Entity( b => @@ -581,6 +637,7 @@ public virtual void Properties_specified_by_string_are_shadow_properties_unless_ b.Property("Photon"); }); + var model = modelBuilder.FinalizeModel(); var entityType = modelBuilder.FinalizeModel().FindEntityType(typeof(Quarks)); Assert.False(entityType.FindProperty("Up").IsShadowProperty()); @@ -598,7 +655,6 @@ public virtual void Properties_specified_by_string_are_shadow_properties_unless_ public virtual void Properties_can_be_made_concurrency_tokens() { var modelBuilder = CreateModelBuilder(); - var model = modelBuilder.Model; modelBuilder.Entity( b => @@ -612,6 +668,7 @@ public virtual void Properties_can_be_made_concurrency_tokens() b.HasChangeTrackingStrategy(ChangeTrackingStrategy.ChangingAndChangedNotifications); }); + var model = modelBuilder.FinalizeModel(); var entityType = modelBuilder.FinalizeModel().FindEntityType(typeof(Quarks)); Assert.False(entityType.FindProperty(Customer.IdProperty.Name).IsConcurrencyToken); @@ -637,17 +694,17 @@ public virtual void Properties_can_be_made_concurrency_tokens() public virtual void Properties_can_have_access_mode_set() { var modelBuilder = CreateModelBuilder(); - var model = modelBuilder.Model; modelBuilder.Entity( b => { b.Property(e => e.Up); - b.Property(e => e.Down).UsePropertyAccessMode(PropertyAccessMode.Field); + b.Property(e => e.Down).HasField("_forDown").UsePropertyAccessMode(PropertyAccessMode.Field); b.Property("Charm").UsePropertyAccessMode(PropertyAccessMode.Property); b.Property("Strange").UsePropertyAccessMode(PropertyAccessMode.FieldDuringConstruction); }); + var model = modelBuilder.FinalizeModel(); var entityType = (IReadOnlyEntityType)model.FindEntityType(typeof(Quarks)); Assert.Equal(PropertyAccessMode.PreferField, entityType.FindProperty("Up").GetPropertyAccessMode()); @@ -660,19 +717,24 @@ public virtual void Properties_can_have_access_mode_set() public virtual void Access_mode_can_be_overridden_at_entity_and_property_levels() { var modelBuilder = CreateModelBuilder(); - var model = modelBuilder.Model; modelBuilder.UsePropertyAccessMode(PropertyAccessMode.Field); - modelBuilder.Entity().Property(e => e.Id1); + modelBuilder.Entity(b => + { + b.HasKey(e => e.Id1); + }); + modelBuilder.Ignore(); modelBuilder.Entity( b => { b.UsePropertyAccessMode(PropertyAccessMode.FieldDuringConstruction); b.Property(e => e.Up).UsePropertyAccessMode(PropertyAccessMode.Property); + b.Property(e => e.Down).HasField("_forDown"); }); + var model = modelBuilder.FinalizeModel(); Assert.Equal(PropertyAccessMode.Field, model.GetPropertyAccessMode()); var hobsType = (IReadOnlyEntityType)model.FindEntityType(typeof(Hob)); @@ -686,10 +748,9 @@ public virtual void Access_mode_can_be_overridden_at_entity_and_property_levels( } [ConditionalFact] - public virtual void Properties_can_have_store_type_set() + public virtual void Properties_can_have_provider_type_set() { var modelBuilder = CreateModelBuilder(); - var model = modelBuilder.Model; modelBuilder.Entity( b => @@ -701,6 +762,7 @@ public virtual void Properties_can_have_store_type_set() b.Property("Strange").HasConversion((Type)null); }); + var model = modelBuilder.FinalizeModel(); var entityType = (IReadOnlyEntityType)model.FindEntityType(typeof(Quarks)); Assert.Null(entityType.FindProperty("Up").GetProviderClrType()); @@ -709,11 +771,33 @@ public virtual void Properties_can_have_store_type_set() Assert.Null(entityType.FindProperty("Strange").GetProviderClrType()); } + [ConditionalFact] + public virtual void Properties_can_have_provider_type_set_for_type() + { + var modelBuilder = CreateModelBuilder(c => c.Properties().HaveConversion()); + + modelBuilder.Entity( + b => + { + b.Property(e => e.Up); + b.Property(e => e.Down); + b.Property("Charm"); + b.Property("Strange"); + }); + + var model = modelBuilder.FinalizeModel(); + var entityType = (IReadOnlyEntityType)model.FindEntityType(typeof(Quarks)); + + Assert.Null(entityType.FindProperty("Up").GetProviderClrType()); + Assert.Same(typeof(byte[]), entityType.FindProperty("Down").GetProviderClrType()); + Assert.Null(entityType.FindProperty("Charm").GetProviderClrType()); + Assert.Same(typeof(byte[]), entityType.FindProperty("Strange").GetProviderClrType()); + } + [ConditionalFact] public virtual void Properties_can_have_value_converter_set_non_generic() { var modelBuilder = CreateModelBuilder(); - var model = modelBuilder.Model; ValueConverter stringConverter = new StringToBytesConverter(Encoding.UTF8); ValueConverter intConverter = new CastingConverter(); @@ -728,6 +812,7 @@ public virtual void Properties_can_have_value_converter_set_non_generic() b.Property("Strange").HasConversion((ValueConverter)null); }); + var model = modelBuilder.FinalizeModel(); var entityType = (IReadOnlyEntityType)model.FindEntityType(typeof(Quarks)); Assert.Null(entityType.FindProperty("Up").GetValueConverter()); @@ -740,7 +825,6 @@ public virtual void Properties_can_have_value_converter_set_non_generic() public virtual void Properties_can_have_value_converter_type_set() { var modelBuilder = CreateModelBuilder(); - var model = modelBuilder.Model; modelBuilder.Entity( b => @@ -752,6 +836,7 @@ public virtual void Properties_can_have_value_converter_type_set() b.Property("Strange").HasConversion((ValueConverter)null, null); }); + var model = modelBuilder.FinalizeModel(); var entityType = (IReadOnlyEntityType)model.FindEntityType(typeof(Quarks)); Assert.Null(entityType.FindProperty("Up").GetValueConverter()); @@ -765,7 +850,7 @@ public virtual void Properties_can_have_value_converter_type_set() Assert.IsType>(charm.GetValueComparer()); Assert.Null(entityType.FindProperty("Strange").GetValueConverter()); - Assert.Null(entityType.FindProperty("Strange").GetValueComparer()); + Assert.IsAssignableFrom>(entityType.FindProperty("Strange").GetValueComparer()); } private class UTF8StringToBytesConverter : StringToBytesConverter @@ -788,7 +873,6 @@ public CustomValueComparer() public virtual void Properties_can_have_value_converter_set_inline() { var modelBuilder = CreateModelBuilder(); - var model = modelBuilder.Model; modelBuilder.Entity( b => @@ -798,7 +882,8 @@ public virtual void Properties_can_have_value_converter_set_inline() b.Property("Charm").HasConversion(v => (long)v, v => (int)v); }); - var entityType = (IReadOnlyEntityType)model.FindEntityType(typeof(Quarks)); + var model = (IReadOnlyModel)modelBuilder.Model; + var entityType = model.FindEntityType(typeof(Quarks)); Assert.Null(entityType.FindProperty("Up").GetValueConverter()); Assert.NotNull(entityType.FindProperty("Down").GetValueConverter()); @@ -809,7 +894,6 @@ public virtual void Properties_can_have_value_converter_set_inline() public virtual void IEnumerable_properties_with_value_converter_set_are_not_discovered_as_navigations() { var modelBuilder = CreateModelBuilder(); - var model = modelBuilder.Model; modelBuilder.Entity( b => @@ -824,10 +908,11 @@ public virtual void IEnumerable_properties_with_value_converter_set_are_not_disc b.Property(e => e.ExpandoObject).Metadata.SetValueComparer(comparer); }); - modelBuilder.FinalizeModel(); + var model = modelBuilder.FinalizeModel(); var entityType = (IReadOnlyEntityType)model.GetEntityTypes().Single(); Assert.NotNull(entityType.FindProperty(nameof(DynamicProperty.ExpandoObject)).GetValueConverter()); + Assert.NotNull(entityType.FindProperty(nameof(DynamicProperty.ExpandoObject)).GetValueComparer()); } private static ExpandoObject DeserializeExpandoObject(string value) @@ -838,11 +923,56 @@ private static ExpandoObject DeserializeExpandoObject(string value) return obj; } + [ConditionalFact] + public virtual void IEnumerable_properties_can_have_value_converter_configured_by_type() + { + var modelBuilder = CreateModelBuilder(c => + c.Properties().HaveConversion(typeof(ExpandoObjectConverter), typeof(ExpandoObjectComparer))); + + modelBuilder.Entity(); + + var model = modelBuilder.FinalizeModel(); + + var entityType = (IReadOnlyEntityType)model.GetEntityTypes().Single(); + var expandoProperty = entityType.FindProperty(nameof(DynamicProperty.ExpandoObject)); + Assert.IsType(expandoProperty.GetValueConverter()); + Assert.IsType(expandoProperty.GetValueComparer()); + } + private class ExpandoObjectConverter : ValueConverter + { + public ExpandoObjectConverter() + : base(v => (string)((IDictionary)v)["Value"], v => DeserializeExpandoObject(v)) + { + } + } + + private class ExpandoObjectComparer : ValueComparer + { + public ExpandoObjectComparer() + : base((v1, v2) => v1.SequenceEqual(v2), v => v.GetHashCode()) + { + } + } + + [ConditionalFact] + public virtual void Throws_for_conflicting_base_configurations_by_type() + { + var modelBuilder = CreateModelBuilder(c => + { + c.Properties(); + c.IgnoreAny(); + }); + + Assert.Equal(CoreStrings.TypeConfigurationConflict( + nameof(INotifyPropertyChanged), "Ignored", + nameof(IEnumerable), "Property"), + Assert.Throws(() => modelBuilder.Entity()).Message); + } + [ConditionalFact] public virtual void Value_converter_type_is_checked() { var modelBuilder = CreateModelBuilder(); - var model = modelBuilder.Model; modelBuilder.Entity( b => @@ -854,7 +984,8 @@ public virtual void Value_converter_type_is_checked() new StringToBytesConverter(Encoding.UTF8))).Message); }); - var entityType = (IReadOnlyEntityType)model.FindEntityType(typeof(Quarks)); + var model = modelBuilder.FinalizeModel(); + var entityType = model.FindEntityType(typeof(Quarks)); Assert.Null(entityType.FindProperty("Up").GetValueConverter()); } @@ -862,7 +993,6 @@ public virtual void Value_converter_type_is_checked() public virtual void Properties_can_have_field_set() { var modelBuilder = CreateModelBuilder(); - var model = modelBuilder.Model; modelBuilder.Entity( b => @@ -872,7 +1002,8 @@ public virtual void Properties_can_have_field_set() b.Property("_forWierd").HasField("_forWierd"); }); - var entityType = (IReadOnlyEntityType)model.FindEntityType(typeof(Quarks)); + var model = modelBuilder.FinalizeModel(); + var entityType = model.FindEntityType(typeof(Quarks)); Assert.Equal("_forUp", entityType.FindProperty("Up").GetFieldName()); Assert.Equal("_forDown", entityType.FindProperty("Down").GetFieldName()); @@ -918,17 +1049,18 @@ public virtual void Properties_can_be_set_to_generate_values_on_Add() b.HasKey(e => e.Id); b.Property(e => e.Up).ValueGeneratedOnAddOrUpdate(); b.Property(e => e.Down).ValueGeneratedNever(); - b.Property("Charm").ValueGeneratedOnAdd(); + b.Property("Charm").Metadata.ValueGenerated = ValueGenerated.OnUpdateSometimes; b.Property("Strange").ValueGeneratedNever(); b.Property("Top").ValueGeneratedOnAddOrUpdate(); b.Property("Bottom").ValueGeneratedOnUpdate(); }); - var entityType = modelBuilder.Model.FindEntityType(typeof(Quarks)); + var model = modelBuilder.FinalizeModel(); + var entityType = model.FindEntityType(typeof(Quarks)); Assert.Equal(ValueGenerated.OnAdd, entityType.FindProperty(Customer.IdProperty.Name).ValueGenerated); Assert.Equal(ValueGenerated.OnAddOrUpdate, entityType.FindProperty("Up").ValueGenerated); Assert.Equal(ValueGenerated.Never, entityType.FindProperty("Down").ValueGenerated); - Assert.Equal(ValueGenerated.OnAdd, entityType.FindProperty("Charm").ValueGenerated); + Assert.Equal(ValueGenerated.OnUpdateSometimes, entityType.FindProperty("Charm").ValueGenerated); Assert.Equal(ValueGenerated.Never, entityType.FindProperty("Strange").ValueGenerated); Assert.Equal(ValueGenerated.OnAddOrUpdate, entityType.FindProperty("Top").ValueGenerated); Assert.Equal(ValueGenerated.OnUpdate, entityType.FindProperty("Bottom").ValueGenerated); @@ -938,7 +1070,6 @@ public virtual void Properties_can_be_set_to_generate_values_on_Add() public virtual void Properties_can_set_row_version() { var modelBuilder = CreateModelBuilder(); - var model = modelBuilder.Model; modelBuilder.Entity( b => @@ -949,7 +1080,7 @@ public virtual void Properties_can_set_row_version() b.Property("Charm").IsRowVersion(); }); - modelBuilder.FinalizeModel(); + var model = modelBuilder.FinalizeModel(); var entityType = model.FindEntityType(typeof(Quarks)); @@ -966,7 +1097,6 @@ public virtual void Properties_can_set_row_version() public virtual void Can_set_max_length_for_properties() { var modelBuilder = CreateModelBuilder(); - var model = modelBuilder.Model; modelBuilder.Entity( b => @@ -979,6 +1109,7 @@ public virtual void Can_set_max_length_for_properties() b.Property("Bottom").HasMaxLength(100); }); + var model = modelBuilder.FinalizeModel(); var entityType = model.FindEntityType(typeof(Quarks)); Assert.Null(entityType.FindProperty(Customer.IdProperty.Name).GetMaxLength()); @@ -990,11 +1121,112 @@ public virtual void Can_set_max_length_for_properties() Assert.Equal(100, entityType.FindProperty("Bottom").GetMaxLength()); } + [ConditionalFact] + public virtual void Can_set_max_length_for_property_type() + { + var modelBuilder = CreateModelBuilder(c => + { + c.Properties().HaveMaxLength(0); + c.Properties().HaveMaxLength(100); + }); + + modelBuilder.Entity( + b => + { + b.Property("Charm"); + b.Property("Strange"); + b.Property("Top"); + b.Property("Bottom"); + }); + + var model = modelBuilder.FinalizeModel(); + var entityType = model.FindEntityType(typeof(Quarks)); + + Assert.Equal(0, entityType.FindProperty(Customer.IdProperty.Name).GetMaxLength()); + Assert.Equal(0, entityType.FindProperty("Up").GetMaxLength()); + Assert.Equal(100, entityType.FindProperty("Down").GetMaxLength()); + Assert.Equal(0, entityType.FindProperty("Charm").GetMaxLength()); + Assert.Equal(100, entityType.FindProperty("Strange").GetMaxLength()); + Assert.Equal(0, entityType.FindProperty("Top").GetMaxLength()); + Assert.Equal(100, entityType.FindProperty("Bottom").GetMaxLength()); + } + + [ConditionalFact] + public virtual void Can_set_precision_and_scale_for_properties() + { + var modelBuilder = CreateModelBuilder(); + + modelBuilder.Entity( + b => + { + b.Property(e => e.Up).HasPrecision(1, 0); + b.Property(e => e.Down).HasPrecision(100, 10); + b.Property("Charm").HasPrecision(1, 0); + b.Property("Strange").HasPrecision(100, 10); + b.Property("Top").HasPrecision(1, 0); + b.Property("Bottom").HasPrecision(100, 10); + }); + + var model = modelBuilder.FinalizeModel(); + var entityType = model.FindEntityType(typeof(Quarks)); + + Assert.Null(entityType.FindProperty(Customer.IdProperty.Name).GetPrecision()); + Assert.Null(entityType.FindProperty(Customer.IdProperty.Name).GetScale()); + Assert.Equal(1, entityType.FindProperty("Up").GetPrecision()); + Assert.Equal(0, entityType.FindProperty("Up").GetScale()); + Assert.Equal(100, entityType.FindProperty("Down").GetPrecision()); + Assert.Equal(10, entityType.FindProperty("Down").GetScale()); + Assert.Equal(1, entityType.FindProperty("Charm").GetPrecision()); + Assert.Equal(0, entityType.FindProperty("Charm").GetScale()); + Assert.Equal(100, entityType.FindProperty("Strange").GetPrecision()); + Assert.Equal(10, entityType.FindProperty("Strange").GetScale()); + Assert.Equal(1, entityType.FindProperty("Top").GetPrecision()); + Assert.Equal(0, entityType.FindProperty("Top").GetScale()); + Assert.Equal(100, entityType.FindProperty("Bottom").GetPrecision()); + Assert.Equal(10, entityType.FindProperty("Bottom").GetScale()); + } + + [ConditionalFact] + public virtual void Can_set_precision_and_scale_for_property_type() + { + var modelBuilder = CreateModelBuilder(c => + { + c.Properties().HavePrecision(1, 0); + c.Properties().HavePrecision(100, 10); + }); + + modelBuilder.Entity( + b => + { + b.Property("Charm"); + b.Property("Strange"); + b.Property("Top"); + b.Property("Bottom"); + }); + + var model = modelBuilder.FinalizeModel(); + var entityType = model.FindEntityType(typeof(Quarks)); + + Assert.Equal(1, entityType.FindProperty(Customer.IdProperty.Name).GetPrecision()); + Assert.Equal(0, entityType.FindProperty(Customer.IdProperty.Name).GetScale()); + Assert.Equal(1, entityType.FindProperty("Up").GetPrecision()); + Assert.Equal(0, entityType.FindProperty("Up").GetScale()); + Assert.Equal(100, entityType.FindProperty("Down").GetPrecision()); + Assert.Equal(10, entityType.FindProperty("Down").GetScale()); + Assert.Equal(1, entityType.FindProperty("Charm").GetPrecision()); + Assert.Equal(0, entityType.FindProperty("Charm").GetScale()); + Assert.Equal(100, entityType.FindProperty("Strange").GetPrecision()); + Assert.Equal(10, entityType.FindProperty("Strange").GetScale()); + Assert.Equal(1, entityType.FindProperty("Top").GetPrecision()); + Assert.Equal(0, entityType.FindProperty("Top").GetScale()); + Assert.Equal(100, entityType.FindProperty("Bottom").GetPrecision()); + Assert.Equal(10, entityType.FindProperty("Bottom").GetScale()); + } + [ConditionalFact] public virtual void Can_set_custom_value_generator_for_properties() { var modelBuilder = CreateModelBuilder(); - var model = modelBuilder.Model; modelBuilder.Entity( b => @@ -1007,7 +1239,7 @@ public virtual void Can_set_custom_value_generator_for_properties() b.Property("Bottom").HasValueGeneratorFactory(); }); - modelBuilder.FinalizeModel(); + var model = modelBuilder.FinalizeModel(); var entityType = model.FindEntityType(typeof(Quarks)); @@ -1202,12 +1434,10 @@ protected class ThreeDee public int[,,] Three { get; set; } } - [ConditionalFact] public virtual void Can_set_unicode_for_properties() { var modelBuilder = CreateModelBuilder(); - var model = modelBuilder.Model; modelBuilder.Entity( b => @@ -1220,6 +1450,7 @@ public virtual void Can_set_unicode_for_properties() b.Property("Bottom").IsUnicode(false); }); + var model = modelBuilder.FinalizeModel(); var entityType = model.FindEntityType(typeof(Quarks)); Assert.Null(entityType.FindProperty(Customer.IdProperty.Name).IsUnicode()); @@ -1231,6 +1462,36 @@ public virtual void Can_set_unicode_for_properties() Assert.False(entityType.FindProperty("Bottom").IsUnicode()); } + [ConditionalFact] + public virtual void Can_set_unicode_for_property_type() + { + var modelBuilder = CreateModelBuilder(c => + { + c.Properties().AreUnicode(); + c.Properties().AreUnicode(false); + }); + + modelBuilder.Entity( + b => + { + b.Property("Charm"); + b.Property("Strange"); + b.Property("Top"); + b.Property("Bottom"); + }); + + var model = modelBuilder.FinalizeModel(); + var entityType = model.FindEntityType(typeof(Quarks)); + + Assert.True(entityType.FindProperty(Customer.IdProperty.Name).IsUnicode()); + Assert.True(entityType.FindProperty("Up").IsUnicode()); + Assert.False(entityType.FindProperty("Down").IsUnicode()); + Assert.True(entityType.FindProperty("Charm").IsUnicode()); + Assert.False(entityType.FindProperty("Strange").IsUnicode()); + Assert.True(entityType.FindProperty("Top").IsUnicode()); + Assert.False(entityType.FindProperty("Bottom").IsUnicode()); + } + [ConditionalFact] public virtual void PropertyBuilder_methods_can_be_chained() { @@ -1246,6 +1507,7 @@ public virtual void PropertyBuilder_methods_can_be_chained() .ValueGeneratedOnUpdate() .IsUnicode() .HasMaxLength(100) + .HasPrecision(10, 1) .HasValueGenerator() .HasValueGenerator(typeof(CustomValueGenerator)) .HasValueGeneratorFactory() @@ -1258,12 +1520,12 @@ public virtual void PropertyBuilder_methods_can_be_chained() public virtual void Can_add_index() { var modelBuilder = CreateModelBuilder(); - var model = modelBuilder.Model; modelBuilder .Entity() .HasIndex(ix => ix.Name); + var model = modelBuilder.FinalizeModel(); var entityType = model.FindEntityType(typeof(Customer)); var index = entityType.GetIndexes().Single(); @@ -1274,7 +1536,6 @@ public virtual void Can_add_index() public virtual void Can_add_index_when_no_clr_property() { var modelBuilder = CreateModelBuilder(); - var model = modelBuilder.Model; modelBuilder .Entity( @@ -1284,6 +1545,7 @@ public virtual void Can_add_index_when_no_clr_property() b.HasIndex("Index"); }); + var model = modelBuilder.FinalizeModel(); var entityType = model.FindEntityType(typeof(Customer)); var index = entityType.GetIndexes().Single(); @@ -1320,7 +1582,6 @@ public virtual void Can_add_multiple_indexes() public virtual void Can_add_contained_indexes() { var modelBuilder = CreateModelBuilder(); - var model = modelBuilder.Model; var entityBuilder = modelBuilder.Entity(); var firstIndexBuilder = entityBuilder.HasIndex( @@ -1328,6 +1589,7 @@ public virtual void Can_add_contained_indexes() var secondIndexBuilder = entityBuilder.HasIndex( ix => new { ix.Id }); + var model = modelBuilder.FinalizeModel(); var entityType = (IReadOnlyEntityType)model.FindEntityType(typeof(Customer)); Assert.Equal(2, entityType.GetIndexes().Count()); @@ -1357,7 +1619,7 @@ public virtual void Can_set_primary_key_by_convention_for_user_specified_shadow_ public virtual void Can_ignore_explicit_interface_implementation_property() { var modelBuilder = CreateModelBuilder(); - modelBuilder.Entity().Ignore(e => ((IEntityBase)e).Target); + modelBuilder.Entity().HasNoKey().Ignore(e => ((IEntityBase)e).Target); Assert.DoesNotContain( nameof(IEntityBase.Target), @@ -1377,7 +1639,8 @@ public virtual void Can_set_key_on_an_entity_with_fields() modelBuilder.Entity().HasKey(e => e.Id); - var entity = modelBuilder.Model.FindEntityType(typeof(EntityWithFields)); + var model = modelBuilder.FinalizeModel(); + var entity = model.FindEntityType(typeof(EntityWithFields)); var primaryKey = entity.FindPrimaryKey(); Assert.NotNull(primaryKey); var property = Assert.Single(primaryKey.Properties); @@ -1393,7 +1656,8 @@ public virtual void Can_set_composite_key_on_an_entity_with_fields() modelBuilder.Entity().HasKey(e => new { e.TenantId, e.CompanyId }); - var entity = modelBuilder.Model.FindEntityType(typeof(EntityWithFields)); + var model = modelBuilder.FinalizeModel(); + var entity = model.FindEntityType(typeof(EntityWithFields)); var primaryKeyProperties = entity.FindPrimaryKey().Properties; Assert.Equal(2, primaryKeyProperties.Count); var first = primaryKeyProperties[0]; @@ -1414,7 +1678,7 @@ public virtual void Can_set_alternate_key_on_an_entity_with_fields() modelBuilder.Entity().HasAlternateKey(e => e.CompanyId); var entity = modelBuilder.Model.FindEntityType(typeof(EntityWithFields)); - var properties = modelBuilder.Model.FindEntityType(typeof(EntityWithFields)).GetProperties(); + var properties = entity.GetProperties(); Assert.Single(properties); var property = properties.Single(); Assert.Equal(nameof(EntityWithFields.CompanyId), property.Name); @@ -1453,7 +1717,8 @@ public virtual void Can_call_Property_on_an_entity_with_fields() modelBuilder.Entity().Property(e => e.Id); - var properties = modelBuilder.Model.FindEntityType(typeof(EntityWithFields)).GetProperties(); + var model = modelBuilder.FinalizeModel(); + var properties = model.FindEntityType(typeof(EntityWithFields)).GetProperties(); var property = Assert.Single(properties); Assert.Equal(nameof(EntityWithFields.Id), property.Name); Assert.Null(property.PropertyInfo); @@ -1465,9 +1730,10 @@ public virtual void Can_set_index_on_an_entity_with_fields() { var modelBuilder = InMemoryTestHelpers.Instance.CreateConventionBuilder(); - modelBuilder.Entity().HasIndex(e => e.CompanyId); + modelBuilder.Entity().HasNoKey().HasIndex(e => e.CompanyId); - var indexes = modelBuilder.Model.FindEntityType(typeof(EntityWithFields)).GetIndexes(); + var model = modelBuilder.FinalizeModel(); + var indexes = model.FindEntityType(typeof(EntityWithFields)).GetIndexes(); var index = Assert.Single(indexes); var property = Assert.Single(index.Properties); Assert.Null(property.PropertyInfo); @@ -1479,9 +1745,10 @@ public virtual void Can_set_composite_index_on_an_entity_with_fields() { var modelBuilder = InMemoryTestHelpers.Instance.CreateConventionBuilder(); - modelBuilder.Entity().HasIndex(e => new { e.TenantId, e.CompanyId }); + modelBuilder.Entity().HasNoKey().HasIndex(e => new { e.TenantId, e.CompanyId }); - var indexes = modelBuilder.Model.FindEntityType(typeof(EntityWithFields)).GetIndexes(); + var model = modelBuilder.FinalizeModel(); + var indexes = model.FindEntityType(typeof(EntityWithFields)).GetIndexes(); var index = Assert.Single(indexes); Assert.Equal(2, index.Properties.Count); var properties = index.Properties; @@ -1504,7 +1771,8 @@ public virtual void Can_ignore_a_field_on_an_entity_with_fields() .Ignore(e => e.CompanyId) .HasKey(e => e.Id); - var entity = modelBuilder.Model.FindEntityType(typeof(EntityWithFields)); + var model = modelBuilder.FinalizeModel(); + var entity = model.FindEntityType(typeof(EntityWithFields)); var property = Assert.Single(entity.GetProperties()); Assert.Equal(nameof(EntityWithFields.Id), property.Name); } @@ -1519,7 +1787,8 @@ public virtual void Can_ignore_a_field_on_a_keyless_entity_with_fields() .Ignore(e => e.FirstName) .Property(e => e.LastName); - var entity = modelBuilder.Model.FindEntityType(typeof(KeylessEntityWithFields)); + var model = modelBuilder.FinalizeModel(); + var entity = model.FindEntityType(typeof(KeylessEntityWithFields)); var property = Assert.Single(entity.GetProperties()); Assert.Equal(nameof(KeylessEntityWithFields.LastName), property.Name); } @@ -1540,7 +1809,7 @@ public virtual void Can_add_seed_data_objects() var finalModel = modelBuilder.FinalizeModel(); - var customer = model.FindEntityType(typeof(Beta)); + var customer = finalModel.FindEntityType(typeof(Beta)); var data = customer.GetSeedData(); Assert.Equal(2, data.Count()); Assert.Equal(-1, data.First()[nameof(Beta.Id)]); @@ -1553,7 +1822,6 @@ public virtual void Can_add_seed_data_objects() public virtual void Can_add_seed_data_anonymous_objects() { var modelBuilder = CreateModelBuilder(); - var model = modelBuilder.Model; modelBuilder.Entity( c => { @@ -1563,7 +1831,7 @@ public virtual void Can_add_seed_data_anonymous_objects() c.HasData(customers); }); - modelBuilder.FinalizeModel(); + var model = modelBuilder.FinalizeModel(); var customer = model.FindEntityType(typeof(Beta)); var data = customer.GetSeedData(); @@ -1580,10 +1848,10 @@ public virtual void Private_property_is_not_discovered_by_convention() modelBuilder.Ignore(); modelBuilder.Entity(); - modelBuilder.FinalizeModel(); + var model = modelBuilder.FinalizeModel(); Assert.Empty( - modelBuilder.Model.FindEntityType(typeof(Gamma)).GetProperties() + model.FindEntityType(typeof(Gamma)).GetProperties() .Where(p => p.Name == "PrivateProperty")); } @@ -1591,7 +1859,7 @@ public virtual void Private_property_is_not_discovered_by_convention() public virtual void Can_add_seed_data_objects_indexed_property() { var modelBuilder = CreateModelBuilder(); - var model = modelBuilder.Model; + modelBuilder.Entity( b => { @@ -1602,7 +1870,7 @@ public virtual void Can_add_seed_data_objects_indexed_property() b.HasData(d); }); - modelBuilder.FinalizeModel(); + var model = modelBuilder.FinalizeModel(); var entityType = model.FindEntityType(typeof(IndexedClass)); var data = Assert.Single(entityType.GetSeedData()); @@ -1615,7 +1883,7 @@ public virtual void Can_add_seed_data_objects_indexed_property() public virtual void Can_add_seed_data_anonymous_objects_indexed_property() { var modelBuilder = CreateModelBuilder(); - var model = modelBuilder.Model; + modelBuilder.Entity( b => { @@ -1624,7 +1892,7 @@ public virtual void Can_add_seed_data_anonymous_objects_indexed_property() b.HasData(new { Id = -1, Required = 2 }); }); - modelBuilder.FinalizeModel(); + var model = modelBuilder.FinalizeModel(); var entityType = model.FindEntityType(typeof(IndexedClass)); var data = Assert.Single(entityType.GetSeedData()); @@ -1637,7 +1905,6 @@ public virtual void Can_add_seed_data_anonymous_objects_indexed_property() public virtual void Can_add_seed_data_objects_indexed_property_dictionary() { var modelBuilder = CreateModelBuilder(); - var model = modelBuilder.Model; modelBuilder.Entity( b => { @@ -1648,7 +1915,7 @@ public virtual void Can_add_seed_data_objects_indexed_property_dictionary() b.HasData(d); }); - modelBuilder.FinalizeModel(); + var model = modelBuilder.FinalizeModel(); var entityType = model.FindEntityType(typeof(IndexedClassByDictionary)); var data = Assert.Single(entityType.GetSeedData()); diff --git a/test/EFCore.Tests/ModelBuilding/TestModel.cs b/test/EFCore.Tests/ModelBuilding/TestModel.cs index e2b7978afd5..88aac4e0bd6 100644 --- a/test/EFCore.Tests/ModelBuilding/TestModel.cs +++ b/test/EFCore.Tests/ModelBuilding/TestModel.cs @@ -248,7 +248,7 @@ protected class OrderDetails : DetailsBase } // INotify interfaces not really implemented; just marking the classes to test metadata construction - private class Quarks : INotifyPropertyChanging, INotifyPropertyChanged + protected class Quarks : INotifyPropertyChanging, INotifyPropertyChanged { private int _forUp; private string _forDown; @@ -282,7 +282,7 @@ public string Down #pragma warning restore 67 } - private class Hob + protected class Hob { public string Id1 { get; set; } public string Id2 { get; set; } @@ -294,7 +294,7 @@ private class Hob public ICollection Nobs { get; set; } } - private class Nob + protected class Nob { public int Id1 { get; set; } public int Id2 { get; set; }