From 397562909493d2f33c7eed9c3194640ba43a5813 Mon Sep 17 00:00:00 2001 From: Andriy Svyryd Date: Tue, 4 Aug 2020 15:01:04 -0700 Subject: [PATCH] Remove TypeMapping annotation Sort columns and column mappings for PK first Fixes #21772 --- .../Internal/ColumnMappingBaseComparer.cs | 2 +- .../Metadata/Internal/ColumnNameComparer.cs | 77 +++++++++++++++++++ .../Metadata/Internal/Table.cs | 46 ++++++++++- .../Metadata/Internal/TableBase.cs | 2 +- .../Update/ModificationCommand.cs | 24 ++++-- .../ConventionPropertyExtensions.cs | 20 ++++- .../Extensions/MutablePropertyExtensions.cs | 11 +++ src/EFCore/Extensions/PropertyExtensions.cs | 19 +++-- .../Builders/IConventionPropertyBuilder.cs | 23 ++++++ .../Conventions/TypeMappingConvention.cs | 5 +- .../Metadata/Internal/CoreAnnotationNames.cs | 9 --- .../Internal/InternalPropertyBuilder.cs | 38 +++++++++ src/EFCore/Metadata/Internal/Property.cs | 41 ++++++++++ .../Design/CSharpMigrationsGeneratorTest.cs | 5 -- .../Migrations/ModelSnapshotSqlServerTest.cs | 5 +- .../RelationalModelValidatorTest.cs | 5 +- .../Metadata/RelationalModelTest.cs | 10 +-- .../Internal/MigrationsModelDifferTest.cs | 4 +- .../Storage/RelationalParameterBuilderTest.cs | 2 +- .../TestUtilities/AnnotationComparer.cs | 1 - .../SqlServerModelValidatorTest.cs | 4 +- .../SqliteModelValidatorTest.cs | 6 +- .../Internal/ChangeDetectorTest.cs | 8 +- 23 files changed, 301 insertions(+), 66 deletions(-) create mode 100644 src/EFCore.Relational/Metadata/Internal/ColumnNameComparer.cs diff --git a/src/EFCore.Relational/Metadata/Internal/ColumnMappingBaseComparer.cs b/src/EFCore.Relational/Metadata/Internal/ColumnMappingBaseComparer.cs index 37e5cc2505f..e3e42802fd9 100644 --- a/src/EFCore.Relational/Metadata/Internal/ColumnMappingBaseComparer.cs +++ b/src/EFCore.Relational/Metadata/Internal/ColumnMappingBaseComparer.cs @@ -34,7 +34,7 @@ private ColumnMappingBaseComparer() /// public int Compare(IColumnMappingBase x, IColumnMappingBase y) { - var result = y.Column.IsNullable.CompareTo(x.Column.IsNullable); + var result = y.Property.IsPrimaryKey().CompareTo(x.Property.IsPrimaryKey()); if (result != 0) { return result; diff --git a/src/EFCore.Relational/Metadata/Internal/ColumnNameComparer.cs b/src/EFCore.Relational/Metadata/Internal/ColumnNameComparer.cs new file mode 100644 index 00000000000..e00aa09a1bf --- /dev/null +++ b/src/EFCore.Relational/Metadata/Internal/ColumnNameComparer.cs @@ -0,0 +1,77 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.EntityFrameworkCore.Metadata.Internal +{ + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public sealed class ColumnNameComparer : IComparer + { + private readonly Table _table; + + /// + /// 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 ColumnNameComparer([NotNull] Table table) + { + _table = table; + } + + /// + /// 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(string x, string y) + { + var xIndex = -1; + var yIndex = -1; + + var columns = _table.PrimaryKey?.Columns; + if (columns != null) + { + for (var i = 0; i < columns.Count; i++) + { + var name = columns[i].Name; + if (name == x) + { + xIndex = i; + } + + if (name == y) + { + yIndex = i; + } + } + } + + if (xIndex == -1 + && yIndex == -1) + { + return StringComparer.Ordinal.Compare(x, y); + } + + if (xIndex > -1 + && yIndex > -1) + { + return xIndex - yIndex; + } + + return xIndex > yIndex + ? -1 + : 1; + } + } +} diff --git a/src/EFCore.Relational/Metadata/Internal/Table.cs b/src/EFCore.Relational/Metadata/Internal/Table.cs index 14127b4ae9e..bde9005dde8 100644 --- a/src/EFCore.Relational/Metadata/Internal/Table.cs +++ b/src/EFCore.Relational/Metadata/Internal/Table.cs @@ -1,7 +1,6 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; @@ -18,6 +17,8 @@ namespace Microsoft.EntityFrameworkCore.Metadata.Internal /// public class Table : TableBase, ITable { + private UniqueConstraint _primaryKey; + /// /// 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 @@ -27,6 +28,7 @@ public class Table : TableBase, ITable public Table([NotNull] string name, [CanBeNull] string schema, [NotNull] RelationalModel model) : base(name, schema, model) { + Columns = new SortedDictionary(new ColumnNameComparer(this)); } /// @@ -44,7 +46,47 @@ public Table([NotNull] string name, [CanBeNull] string schema, [NotNull] Relatio /// 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 UniqueConstraint PrimaryKey { get; [param: NotNull] set; } + public virtual UniqueConstraint PrimaryKey + { + get => _primaryKey; [param: NotNull] + set + { + var oldPrimaryKey = _primaryKey; + if (oldPrimaryKey != null) + { + foreach (var column in oldPrimaryKey.Columns) + { + Columns.Remove(column.Name); + } + } + + if (value != null) + { + foreach (var column in value.Columns) + { + Columns.Remove(column.Name); + } + } + + _primaryKey = value; + + if (oldPrimaryKey != null) + { + foreach (var column in oldPrimaryKey.Columns) + { + Columns.TryAdd(column.Name, column); + } + } + + if (value != null) + { + foreach (var column in value.Columns) + { + Columns.TryAdd(column.Name, column); + } + } + } + } /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to diff --git a/src/EFCore.Relational/Metadata/Internal/TableBase.cs b/src/EFCore.Relational/Metadata/Internal/TableBase.cs index 62c90a45f44..9475009f3e2 100644 --- a/src/EFCore.Relational/Metadata/Internal/TableBase.cs +++ b/src/EFCore.Relational/Metadata/Internal/TableBase.cs @@ -62,7 +62,7 @@ public TableBase([NotNull] string name, [CanBeNull] string schema, [NotNull] Rel /// 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 SortedDictionary Columns { get; } + public virtual SortedDictionary Columns { get; [param: NotNull] protected set; } = new SortedDictionary(StringComparer.Ordinal); /// diff --git a/src/EFCore.Relational/Update/ModificationCommand.cs b/src/EFCore.Relational/Update/ModificationCommand.cs index 2e091d7cc76..bdf28fe2e9b 100644 --- a/src/EFCore.Relational/Update/ModificationCommand.cs +++ b/src/EFCore.Relational/Update/ModificationCommand.cs @@ -284,17 +284,27 @@ private IReadOnlyList GenerateColumnModifications() && (entry.EntityState == EntityState.Deleted || entry.EntityState == EntityState.Added); - foreach (var property in entry.EntityType.GetProperties()) + ITableMappingBase tableMapping = null; + foreach (var mapping in entry.EntityType.GetTableMappings()) { - var columnMapping = property.GetTableColumnMappings() - .Where(m => m.TableMapping.Table.Name == TableName && m.TableMapping.Table.Schema == Schema) - .FirstOrDefault(); - if (columnMapping == null) + var table = ((ITableMappingBase)mapping).Table; + if (table.Name == TableName + && table.Schema == Schema) { - continue; + tableMapping = mapping; + break; } + } - var column = columnMapping.Column; + if (tableMapping == null) + { + continue; + } + + foreach (var columnMapping in tableMapping.ColumnMappings) + { + var property = columnMapping.Property; + var column = (IColumn)columnMapping.Column; var isKey = property.IsPrimaryKey(); var isCondition = !adding && (isKey || property.IsConcurrencyToken); var readValue = state != EntityState.Deleted && entry.IsStoreGenerated(property); diff --git a/src/EFCore/Extensions/ConventionPropertyExtensions.cs b/src/EFCore/Extensions/ConventionPropertyExtensions.cs index 640f3c9703c..06f2815befa 100644 --- a/src/EFCore/Extensions/ConventionPropertyExtensions.cs +++ b/src/EFCore/Extensions/ConventionPropertyExtensions.cs @@ -8,6 +8,7 @@ using Microsoft.EntityFrameworkCore.ChangeTracking; using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Metadata.Internal; +using Microsoft.EntityFrameworkCore.Storage; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using Microsoft.EntityFrameworkCore.ValueGeneration; @@ -81,13 +82,26 @@ public static IConventionKey FindContainingPrimaryKey([NotNull] this IConvention public static IEnumerable GetContainingKeys([NotNull] this IConventionProperty property) => ((Property)property).GetContainingKeys(); + /// + /// Sets the for the given property + /// + /// The property. + /// The for this property. + /// Indicates whether the configuration was specified using a data annotation. + public static CoreTypeMapping SetTypeMapping( + [NotNull] this IConventionProperty property, + [NotNull] CoreTypeMapping typeMapping, + bool fromDataAnnotation = false) + => ((Property)property).SetTypeMapping( + typeMapping, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + /// /// Gets the for . /// /// The property. /// The for . public static ConfigurationSource? GetTypeMappingConfigurationSource([NotNull] this IConventionProperty property) - => property.FindAnnotation(CoreAnnotationNames.TypeMapping)?.GetConfigurationSource(); + => ((Property)property).GetTypeMappingConfigurationSource(); /// /// Sets the maximum length of data that is allowed in this property. For example, if the property is a ' @@ -115,7 +129,7 @@ public static IEnumerable GetContainingKeys([NotNull] this IConv /// For example, if the property is a /// then this is the maximum number of digits. /// - /// The property to get the precision of. + /// The property. /// The maximum number of digits that is allowed in this property. /// Indicates whether the configuration was specified using a data annotation. public static int? SetPrecision([NotNull] this IConventionProperty property, int? precision, bool fromDataAnnotation = false) @@ -135,7 +149,7 @@ public static IEnumerable GetContainingKeys([NotNull] this IConv /// For example, if the property is a /// then this is the maximum number of decimal places. /// - /// The property to get the precision of. + /// The property. /// The maximum number of decimal places that is allowed in this property. /// Indicates whether the configuration was specified using a data annotation. public static int? SetScale([NotNull] this IConventionProperty property, int? scale, bool fromDataAnnotation = false) diff --git a/src/EFCore/Extensions/MutablePropertyExtensions.cs b/src/EFCore/Extensions/MutablePropertyExtensions.cs index 5b9f3299a1d..5c31ef0b749 100644 --- a/src/EFCore/Extensions/MutablePropertyExtensions.cs +++ b/src/EFCore/Extensions/MutablePropertyExtensions.cs @@ -8,6 +8,7 @@ using Microsoft.EntityFrameworkCore.ChangeTracking; using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Metadata.Internal; +using Microsoft.EntityFrameworkCore.Storage; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using Microsoft.EntityFrameworkCore.ValueGeneration; @@ -198,6 +199,16 @@ public static void SetValueConverter([NotNull] this IMutableProperty property, [ public static void SetProviderClrType([NotNull] this IMutableProperty property, [CanBeNull] Type providerClrType) => property.AsProperty().SetProviderClrType(providerClrType, ConfigurationSource.Explicit); + /// + /// Sets the for the given property + /// + /// The property. + /// The for this property. + public static CoreTypeMapping SetTypeMapping( + [NotNull] this IMutableProperty property, + [NotNull] CoreTypeMapping typeMapping) + => ((Property)property).SetTypeMapping(typeMapping, ConfigurationSource.Explicit); + /// /// Sets the custom for this property. /// diff --git a/src/EFCore/Extensions/PropertyExtensions.cs b/src/EFCore/Extensions/PropertyExtensions.cs index f96879def4b..50e8e27ab7e 100644 --- a/src/EFCore/Extensions/PropertyExtensions.cs +++ b/src/EFCore/Extensions/PropertyExtensions.cs @@ -33,8 +33,7 @@ public static class PropertyExtensions /// The type mapping. public static CoreTypeMapping GetTypeMapping([NotNull] this IProperty property) { - var mapping = (CoreTypeMapping)property[CoreAnnotationNames.TypeMapping]; - + var mapping = ((Property)property).TypeMapping; if (mapping == null) { throw new InvalidOperationException( @@ -50,7 +49,7 @@ public static CoreTypeMapping GetTypeMapping([NotNull] this IProperty property) /// The property. /// The type mapping, or if none was found. public static CoreTypeMapping FindTypeMapping([NotNull] this IProperty property) - => (CoreTypeMapping)property[CoreAnnotationNames.TypeMapping]; + => ((Property)property).TypeMapping; /// /// Finds the first principal property that the given property is constrained by @@ -124,7 +123,7 @@ private static void AddPrincipals(IProperty property, List visited) /// The property to check. /// if the property is used as a foreign key, otherwise . public static bool IsForeignKey([NotNull] this IProperty property) - => Check.NotNull(property, nameof(property)).AsProperty().ForeignKeys != null; + => Check.NotNull((Property)property, nameof(property)).ForeignKeys != null; /// /// Gets a value indicating whether this property is used as an index (or part of a composite index). @@ -132,7 +131,7 @@ public static bool IsForeignKey([NotNull] this IProperty property) /// The property to check. /// if the property is used as an index, otherwise . public static bool IsIndex([NotNull] this IProperty property) - => Check.NotNull(property, nameof(property)).AsProperty().Indexes != null; + => Check.NotNull((Property)property, nameof(property)).Indexes != null; /// /// Gets a value indicating whether this property is used as a unique index (or part of a unique composite index). @@ -157,7 +156,7 @@ public static bool IsPrimaryKey([NotNull] this IProperty property) /// The property to check. /// if the property is used as a key, otherwise . public static bool IsKey([NotNull] this IProperty property) - => Check.NotNull(property, nameof(property)).AsProperty().Keys != null; + => Check.NotNull((Property)property, nameof(property)).Keys != null; /// /// Gets all foreign keys that use this property (including composite foreign keys in which this property @@ -166,7 +165,7 @@ public static bool IsKey([NotNull] this IProperty property) /// The property to get foreign keys for. /// The foreign keys that use this property. public static IEnumerable GetContainingForeignKeys([NotNull] this IProperty property) - => Check.NotNull(property, nameof(property)).AsProperty().GetContainingForeignKeys(); + => Check.NotNull((Property)property, nameof(property)).GetContainingForeignKeys(); /// /// Gets all indexes that use this property (including composite indexes in which this property @@ -175,7 +174,7 @@ public static IEnumerable GetContainingForeignKeys([NotNull] this I /// The property to get indexes for. /// The indexes that use this property. public static IEnumerable GetContainingIndexes([NotNull] this IProperty property) - => Check.NotNull(property, nameof(property)).AsProperty().GetContainingIndexes(); + => Check.NotNull((Property)property, nameof(property)).GetContainingIndexes(); /// /// Gets the primary key that uses this property (including a composite primary key in which this property @@ -184,7 +183,7 @@ public static IEnumerable GetContainingIndexes([NotNull] this IProperty /// The property to get primary key for. /// The primary that use this property, or if it is not part of the primary key. public static IKey FindContainingPrimaryKey([NotNull] this IProperty property) - => Check.NotNull(property, nameof(property)).AsProperty().PrimaryKey; + => Check.NotNull((Property)property, nameof(property)).PrimaryKey; /// /// Gets all primary or alternate keys that use this property (including composite keys in which this property @@ -193,7 +192,7 @@ public static IKey FindContainingPrimaryKey([NotNull] this IProperty property) /// The property to get primary and alternate keys for. /// The primary and alternate keys that use this property. public static IEnumerable GetContainingKeys([NotNull] this IProperty property) - => Check.NotNull(property, nameof(property)).AsProperty().GetContainingKeys(); + => Check.NotNull((Property)property, nameof(property)).GetContainingKeys(); /// /// Gets the maximum length of data that is allowed in this property. For example, if the property is a ' diff --git a/src/EFCore/Metadata/Builders/IConventionPropertyBuilder.cs b/src/EFCore/Metadata/Builders/IConventionPropertyBuilder.cs index fef9d241c7e..83f990f5aaf 100644 --- a/src/EFCore/Metadata/Builders/IConventionPropertyBuilder.cs +++ b/src/EFCore/Metadata/Builders/IConventionPropertyBuilder.cs @@ -5,6 +5,7 @@ using System.Reflection; using JetBrains.Annotations; using Microsoft.EntityFrameworkCore.ChangeTracking; +using Microsoft.EntityFrameworkCore.Storage; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using Microsoft.EntityFrameworkCore.ValueGeneration; @@ -323,6 +324,28 @@ bool CanSetValueGenerator( /// bool CanSetConversion([CanBeNull] Type providerClrType, bool fromDataAnnotation = false); + /// + /// Configures the for this property. + /// + /// The type mapping, or to remove any previously set type mapping. + /// Indicates whether the configuration was specified using a data annotation. + /// + /// The same builder instance if the configuration was applied, + /// otherwise. + /// + IConventionPropertyBuilder HasTypeMapping([CanBeNull] CoreTypeMapping typeMapping, bool fromDataAnnotation = false); + + /// + /// Returns a value indicating whether the given + /// can be configured for this property from the current configuration source. + /// + /// The type mapping, or to remove any previously set type mapping. + /// Indicates whether the configuration was specified using a data annotation. + /// + /// if the given can be configured for this property. + /// + bool CanSetTypeMapping([CanBeNull] CoreTypeMapping typeMapping, bool fromDataAnnotation = false); + /// /// Configures the for this property. /// diff --git a/src/EFCore/Metadata/Conventions/TypeMappingConvention.cs b/src/EFCore/Metadata/Conventions/TypeMappingConvention.cs index 321542fcaad..b33ebf75a9e 100644 --- a/src/EFCore/Metadata/Conventions/TypeMappingConvention.cs +++ b/src/EFCore/Metadata/Conventions/TypeMappingConvention.cs @@ -5,7 +5,6 @@ using JetBrains.Annotations; using Microsoft.EntityFrameworkCore.Metadata.Builders; using Microsoft.EntityFrameworkCore.Metadata.Conventions.Infrastructure; -using Microsoft.EntityFrameworkCore.Metadata.Internal; namespace Microsoft.EntityFrameworkCore.Metadata.Conventions { @@ -33,9 +32,7 @@ public virtual void ProcessModelFinalizing(IConventionModelBuilder modelBuilder, { foreach (var property in modelBuilder.Metadata.GetEntityTypes().SelectMany(e => e.GetDeclaredProperties())) { - property.Builder.HasAnnotation( - CoreAnnotationNames.TypeMapping, - Dependencies.TypeMappingSource.FindMapping(property)); + property.Builder.HasTypeMapping(Dependencies.TypeMappingSource.FindMapping(property)); } } } diff --git a/src/EFCore/Metadata/Internal/CoreAnnotationNames.cs b/src/EFCore/Metadata/Internal/CoreAnnotationNames.cs index 88daa81d1b2..e170f185413 100644 --- a/src/EFCore/Metadata/Internal/CoreAnnotationNames.cs +++ b/src/EFCore/Metadata/Internal/CoreAnnotationNames.cs @@ -134,14 +134,6 @@ public static class CoreAnnotationNames /// public const string ServiceOnlyConstructorBinding = "ServiceOnlyConstructorBinding"; - /// - /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to - /// the same compatibility standards as public APIs. It may be changed or removed without notice in - /// any release. You should only use it directly in your code with extreme caution and knowing that - /// doing so can result in application failures when updating to a new Entity Framework Core release. - /// - public const string TypeMapping = "TypeMapping"; - /// /// 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 @@ -304,7 +296,6 @@ public static class CoreAnnotationNames DiscriminatorValue, ConstructorBinding, ServiceOnlyConstructorBinding, - TypeMapping, ValueConverter, ValueComparer, #pragma warning disable 618 diff --git a/src/EFCore/Metadata/Internal/InternalPropertyBuilder.cs b/src/EFCore/Metadata/Internal/InternalPropertyBuilder.cs index 65e35c85ac0..0ac3ab4efcb 100644 --- a/src/EFCore/Metadata/Internal/InternalPropertyBuilder.cs +++ b/src/EFCore/Metadata/Internal/InternalPropertyBuilder.cs @@ -10,6 +10,7 @@ using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Microsoft.EntityFrameworkCore.Storage; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using Microsoft.EntityFrameworkCore.Utilities; using Microsoft.EntityFrameworkCore.ValueGeneration; @@ -470,6 +471,35 @@ public virtual bool CanSetConversion([CanBeNull] Type providerClrType, Configura => configurationSource.Overrides(Metadata.GetProviderClrTypeConfigurationSource()) || Metadata.GetProviderClrType() == 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 InternalPropertyBuilder HasTypeMapping( + [CanBeNull] CoreTypeMapping typeMapping, ConfigurationSource configurationSource) + { + if (CanSetTypeMapping(typeMapping, configurationSource)) + { + Metadata.SetTypeMapping(typeMapping, configurationSource); + + return this; + } + + return null; + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual bool CanSetTypeMapping([CanBeNull] CoreTypeMapping typeMapping, ConfigurationSource? configurationSource) + => configurationSource.Overrides(Metadata.GetTypeMappingConfigurationSource()) + || Metadata.TypeMapping == typeMapping; + /// /// 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 @@ -920,6 +950,14 @@ IConventionPropertyBuilder IConventionPropertyBuilder.HasConversion(Type provide bool IConventionPropertyBuilder.CanSetConversion(Type providerClrType, bool fromDataAnnotation) => CanSetConversion(providerClrType, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + /// + IConventionPropertyBuilder IConventionPropertyBuilder.HasTypeMapping(CoreTypeMapping typeMapping, bool fromDataAnnotation) + => HasTypeMapping(typeMapping, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + + /// + bool IConventionPropertyBuilder.CanSetTypeMapping(CoreTypeMapping typeMapping, bool fromDataAnnotation) + => CanSetTypeMapping(typeMapping, 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 diff --git a/src/EFCore/Metadata/Internal/Property.cs b/src/EFCore/Metadata/Internal/Property.cs index 66d7d45897f..0c60862b497 100644 --- a/src/EFCore/Metadata/Internal/Property.cs +++ b/src/EFCore/Metadata/Internal/Property.cs @@ -11,6 +11,7 @@ using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Microsoft.EntityFrameworkCore.Storage; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using Microsoft.EntityFrameworkCore.Utilities; using Microsoft.EntityFrameworkCore.ValueGeneration; @@ -28,11 +29,13 @@ public class Property : PropertyBase, IMutableProperty, IConventionProperty private bool? _isConcurrencyToken; private bool? _isNullable; private ValueGenerated? _valueGenerated; + private CoreTypeMapping _typeMapping; private ConfigurationSource? _typeConfigurationSource; private ConfigurationSource? _isNullableConfigurationSource; private ConfigurationSource? _isConcurrencyTokenConfigurationSource; private ConfigurationSource? _valueGeneratedConfigurationSource; + private ConfigurationSource? _typeMappingConfigurationSource; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -453,6 +456,44 @@ public virtual Type SetProviderClrType([CanBeNull] Type providerClrType, Configu return 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 CoreTypeMapping TypeMapping + { + get => _typeMapping; + [param: NotNull] + set => SetTypeMapping(value, ConfigurationSource.Explicit); + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual CoreTypeMapping SetTypeMapping([NotNull] CoreTypeMapping typeMapping, ConfigurationSource configurationSource) + { + _typeMapping = typeMapping; + _typeMappingConfigurationSource = typeMapping == null + ? (ConfigurationSource?)null + : configurationSource.Max(_typeMappingConfigurationSource); + + return typeMapping; + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual ConfigurationSource? GetTypeMappingConfigurationSource() + => _typeMappingConfigurationSource; + /// /// 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/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationsGeneratorTest.cs b/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationsGeneratorTest.cs index 4abadc36e4b..6a6b6f74126 100644 --- a/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationsGeneratorTest.cs +++ b/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationsGeneratorTest.cs @@ -49,7 +49,6 @@ public void Test_new_annotations_handled_for_entity_types() CoreAnnotationNames.ProductVersion, CoreAnnotationNames.ValueGeneratorFactory, CoreAnnotationNames.OwnedTypes, - CoreAnnotationNames.TypeMapping, CoreAnnotationNames.ValueConverter, CoreAnnotationNames.ValueComparer, #pragma warning disable 618 @@ -211,10 +210,6 @@ public void Test_new_annotations_handled_for_properties() RelationalAnnotationNames.DefaultValue, ("1", $@"{columnMapping}{_nl}.{nameof(RelationalPropertyBuilderExtensions.HasDefaultValue)}(""1"")") }, - { - CoreAnnotationNames.TypeMapping, - (new LongTypeMapping("bigint"), $@"{_nl}.{nameof(RelationalPropertyBuilderExtensions.HasColumnType)}(""bigint"")") - }, { RelationalAnnotationNames.IsFixedLength, (true, $@"{columnMapping}{_nl}.{nameof(RelationalPropertyBuilderExtensions.IsFixedLength)}(true)") diff --git a/test/EFCore.Design.Tests/Migrations/ModelSnapshotSqlServerTest.cs b/test/EFCore.Design.Tests/Migrations/ModelSnapshotSqlServerTest.cs index e6dcc840e6b..e3d58a3bc84 100644 --- a/test/EFCore.Design.Tests/Migrations/ModelSnapshotSqlServerTest.cs +++ b/test/EFCore.Design.Tests/Migrations/ModelSnapshotSqlServerTest.cs @@ -2030,8 +2030,7 @@ public virtual void Property_annotations_are_stored_in_snapshot() { builder.Entity() .Property("Id") - .HasAnnotation("AnnotationName", "AnnotationValue") - .HasAnnotation(CoreAnnotationNames.TypeMapping, new IntTypeMapping("int")); + .HasAnnotation("AnnotationName", "AnnotationValue"); builder.Ignore(); }, @@ -2752,7 +2751,7 @@ public virtual void Property_multiple_annotations_are_stored_in_snapshot() o => { var property = o.GetEntityTypes().First().FindProperty("AlternateId"); - Assert.Equal(6, property.GetAnnotations().Count()); + Assert.Equal(5, property.GetAnnotations().Count()); Assert.Equal("AnnotationValue", property["AnnotationName"]); Assert.Equal("CName", property["Relational:ColumnName"]); Assert.Equal("int", property["Relational:ColumnType"]); diff --git a/test/EFCore.Relational.Tests/Infrastructure/RelationalModelValidatorTest.cs b/test/EFCore.Relational.Tests/Infrastructure/RelationalModelValidatorTest.cs index 1d4f434bebf..5e8cbe68fa4 100644 --- a/test/EFCore.Relational.Tests/Infrastructure/RelationalModelValidatorTest.cs +++ b/test/EFCore.Relational.Tests/Infrastructure/RelationalModelValidatorTest.cs @@ -1764,11 +1764,10 @@ public virtual void Non_TPH_as_a_result_of_DbFunction_throws() } private static void GenerateMapping(IMutableProperty property) - => property[CoreAnnotationNames.TypeMapping] - = new TestRelationalTypeMappingSource( + => property.SetTypeMapping(new TestRelationalTypeMappingSource( TestServiceFactory.Instance.Create(), TestServiceFactory.Instance.Create()) - .FindMapping(property); + .FindMapping(property)); protected override void SetBaseType(IMutableEntityType entityType, IMutableEntityType baseEntityType) { diff --git a/test/EFCore.Relational.Tests/Metadata/RelationalModelTest.cs b/test/EFCore.Relational.Tests/Metadata/RelationalModelTest.cs index 747168180f9..6e5c4d15619 100644 --- a/test/EFCore.Relational.Tests/Metadata/RelationalModelTest.cs +++ b/test/EFCore.Relational.Tests/Metadata/RelationalModelTest.cs @@ -71,7 +71,7 @@ private static void AssertDefaultMappings(IRelationalModel model) var orderMapping = orderType.GetDefaultMappings().Single(); Assert.True(orderMapping.IncludesDerivedTypes); Assert.Equal( - new[] { nameof(Order.AlternateId), nameof(Order.CustomerId), nameof(Order.OrderDate), nameof(Order.OrderId) }, + new[] { nameof(Order.OrderId), nameof(Order.AlternateId), nameof(Order.CustomerId), nameof(Order.OrderDate) }, orderMapping.ColumnMappings.Select(m => m.Property.Name)); var ordersTable = orderMapping.Table; @@ -142,7 +142,7 @@ private static void AssertViews(IRelationalModel model, Mapping mapping) Assert.Same(orderType.GetViewMappings(), orderType.GetViewOrTableMappings()); Assert.True(orderMapping.IncludesDerivedTypes); Assert.Equal( - new[] { nameof(Order.AlternateId), nameof(Order.CustomerId), nameof(Order.OrderDate), nameof(Order.OrderId) }, + new[] { nameof(Order.OrderId), nameof(Order.AlternateId), nameof(Order.CustomerId), nameof(Order.OrderDate) }, orderMapping.ColumnMappings.Select(m => m.Property.Name)); var ordersView = orderMapping.View; @@ -243,7 +243,7 @@ private static void AssertTables(IRelationalModel model, Mapping mapping) var orderMapping = orderType.GetTableMappings().Single(); Assert.True(orderMapping.IncludesDerivedTypes); Assert.Equal( - new[] { nameof(Order.AlternateId), nameof(Order.CustomerId), nameof(Order.OrderDate), nameof(Order.OrderId) }, + new[] { nameof(Order.OrderId), nameof(Order.AlternateId), nameof(Order.CustomerId), nameof(Order.OrderDate) }, orderMapping.ColumnMappings.Select(m => m.Property.Name)); var ordersTable = orderMapping.Table; @@ -252,14 +252,14 @@ private static void AssertTables(IRelationalModel model, Mapping mapping) new[] { nameof(Order), "OrderDetails.BillingAddress#Address", "OrderDetails.ShippingAddress#Address", nameof(OrderDetails) }, ordersTable.EntityTypeMappings.Select(m => m.EntityType.DisplayName())); Assert.Equal(new[] { + nameof(Order.OrderId), nameof(Order.AlternateId), nameof(Order.CustomerId), "Details_BillingAddress_City", "Details_BillingAddress_Street", "Details_ShippingAddress_City", "Details_ShippingAddress_Street", - nameof(Order.OrderDate), - nameof(Order.OrderId) + nameof(Order.OrderDate) }, ordersTable.Columns.Select(m => m.Name)); Assert.Equal("Order", ordersTable.Name); diff --git a/test/EFCore.Relational.Tests/Migrations/Internal/MigrationsModelDifferTest.cs b/test/EFCore.Relational.Tests/Migrations/Internal/MigrationsModelDifferTest.cs index d2aabff2c40..d215d3d6d02 100644 --- a/test/EFCore.Relational.Tests/Migrations/Internal/MigrationsModelDifferTest.cs +++ b/test/EFCore.Relational.Tests/Migrations/Internal/MigrationsModelDifferTest.cs @@ -5167,8 +5167,8 @@ public void Add_subtype_with_shared_column_with_seed_data() AssertMultidimensionalArray( m.Values, v => Assert.Equal(43, v), - v => Assert.Equal("Dog", v), - v => Assert.Equal("43", v)); + v => Assert.Equal("43", v), + v => Assert.Equal("Dog", v)); }), downOps => Assert.Collection( downOps, diff --git a/test/EFCore.Relational.Tests/Storage/RelationalParameterBuilderTest.cs b/test/EFCore.Relational.Tests/Storage/RelationalParameterBuilderTest.cs index 5606abdd532..1836d189ef5 100644 --- a/test/EFCore.Relational.Tests/Storage/RelationalParameterBuilderTest.cs +++ b/test/EFCore.Relational.Tests/Storage/RelationalParameterBuilderTest.cs @@ -79,7 +79,7 @@ public void Can_add_type_mapped_parameter_by_property(bool nullable) var property = ((IMutableModel)new Model()).AddEntityType("MyType").AddProperty("MyProp", typeof(string)); property.IsNullable = nullable; - property[CoreAnnotationNames.TypeMapping] = GetMapping(typeMapper, property); + property.SetTypeMapping(GetMapping(typeMapper, property)); var parameterBuilder = new RelationalCommandBuilder( new RelationalCommandBuilderDependencies( diff --git a/test/EFCore.Specification.Tests/TestUtilities/AnnotationComparer.cs b/test/EFCore.Specification.Tests/TestUtilities/AnnotationComparer.cs index a1e47ef6ec1..45d04265f88 100644 --- a/test/EFCore.Specification.Tests/TestUtilities/AnnotationComparer.cs +++ b/test/EFCore.Specification.Tests/TestUtilities/AnnotationComparer.cs @@ -30,7 +30,6 @@ public bool Equals(IAnnotation x, IAnnotation y) ? false : x.Name == y.Name && (x.Name == CoreAnnotationNames.ValueGeneratorFactory - || x.Name == CoreAnnotationNames.TypeMapping || Equals(x.Value, y.Value)); } diff --git a/test/EFCore.SqlServer.Tests/Infrastructure/SqlServerModelValidatorTest.cs b/test/EFCore.SqlServer.Tests/Infrastructure/SqlServerModelValidatorTest.cs index 4508a22f80a..44dcb008327 100644 --- a/test/EFCore.SqlServer.Tests/Infrastructure/SqlServerModelValidatorTest.cs +++ b/test/EFCore.SqlServer.Tests/Infrastructure/SqlServerModelValidatorTest.cs @@ -583,11 +583,11 @@ protected virtual void ConfigureProperty(IMutableProperty property, string confi } private static void GenerateMapping(IMutableProperty property) - => property[CoreAnnotationNames.TypeMapping] = + => property.SetTypeMapping( new SqlServerTypeMappingSource( TestServiceFactory.Instance.Create(), TestServiceFactory.Instance.Create()) - .FindMapping(property); + .FindMapping(property)); private class Cheese { diff --git a/test/EFCore.Sqlite.Tests/Infrastructure/SqliteModelValidatorTest.cs b/test/EFCore.Sqlite.Tests/Infrastructure/SqliteModelValidatorTest.cs index 25f447077b2..82a306a5e73 100644 --- a/test/EFCore.Sqlite.Tests/Infrastructure/SqliteModelValidatorTest.cs +++ b/test/EFCore.Sqlite.Tests/Infrastructure/SqliteModelValidatorTest.cs @@ -97,9 +97,9 @@ public void Detects_sequences() } private static void GenerateMapping(IMutableProperty property) - => property[CoreAnnotationNames.TypeMapping] - = TestServiceFactory.Instance.Create() - .FindMapping(property); + => property.SetTypeMapping( + TestServiceFactory.Instance.Create() + .FindMapping(property)); protected override TestHelpers TestHelpers => SqliteTestHelpers.Instance; } diff --git a/test/EFCore.Tests/ChangeTracking/Internal/ChangeDetectorTest.cs b/test/EFCore.Tests/ChangeTracking/Internal/ChangeDetectorTest.cs index 461107e5585..5ca7dcbec54 100644 --- a/test/EFCore.Tests/ChangeTracking/Internal/ChangeDetectorTest.cs +++ b/test/EFCore.Tests/ChangeTracking/Internal/ChangeDetectorTest.cs @@ -351,11 +351,11 @@ protected internal override void OnModelCreating(ModelBuilder modelBuilder) if (UseTypeMapping) { - property[CoreAnnotationNames.TypeMapping] - = new ConcreteTypeMapping(typeof(int[]), intArrayConverter, intArrayComparer); + property.SetTypeMapping( + new ConcreteTypeMapping(typeof(int[]), intArrayConverter, intArrayComparer)); - shadowProperty[CoreAnnotationNames.TypeMapping] - = new ConcreteTypeMapping(typeof(int[]), intArrayConverter, intArrayComparer); + shadowProperty.SetTypeMapping( + new ConcreteTypeMapping(typeof(int[]), intArrayConverter, intArrayComparer)); } else {