From 1250db059de831d110dc1f80f3c24a1c98437047 Mon Sep 17 00:00:00 2001 From: Andriy Svyryd Date: Wed, 23 Mar 2022 10:34:51 -0700 Subject: [PATCH] Switch the update pipeline to use the relational model Add custom differ logic for seed data Fixes #22063 --- ...ntityFrameworkRelationalServicesBuilder.cs | 6 +- .../RelationalModelDependencies.cs | 26 +- .../RelationalModelValidator.cs | 40 +- src/EFCore.Relational/Metadata/IColumn.cs | 3 +- src/EFCore.Relational/Metadata/IColumnBase.cs | 12 + .../Metadata/IForeignKeyConstraint.cs | 7 +- .../Metadata/Internal/ForeignKeyConstraint.cs | 34 +- .../Metadata/Internal/RelationalModel.cs | 246 ++++----- .../Internal/TableBaseIdentityComparer.cs | 47 ++ .../Metadata/Internal/UniqueConstraint.cs | 25 + .../Internal/MigrationsModelDiffer.cs | 22 +- .../Properties/RelationalStrings.Designer.cs | 10 +- .../Properties/RelationalStrings.resx | 3 + .../Internal/TypeMappedRelationalParameter.cs | 6 +- .../Storage/RelationalTypeMapping.cs | 182 +++---- .../Update/ColumnModificationParameters.cs | 4 - .../Update/Internal/CommandBatchPreparer.cs | 8 +- .../CommandBatchPreparerDependencies.cs | 10 - .../Internal/CompositeRowValueFactory.cs | 202 ++++++++ ...ource.cs => IRowForeignKeyValueFactory.cs} | 21 +- ...s => IRowForeignKeyValueFactoryFactory.cs} | 9 +- ...tory.cs => IRowForeignKeyValueFactory`.cs} | 41 +- ...alueIndexFactory.cs => IRowIdentityMap.cs} | 10 +- .../Update/Internal/IRowKeyValueFactory.cs | 21 + ...Index.cs => IRowKeyValueFactoryFactory.cs} | 4 +- .../Update/Internal/IRowKeyValueFactory`.cs | 45 ++ .../Update/Internal/KeyValueIndex.cs | 35 +- .../Internal/RowForeignKeyValueFactory.cs | 469 ++++++++++++++++++ .../RowForeignKeyValueFactoryFactory.cs | 35 ++ .../Update/Internal/RowIdentityMap.cs | 137 +++++ .../Internal/RowKeyValueFactoryFactory.cs | 278 +++++++++++ .../Internal/CompositeValueFactory.cs | 111 +---- .../SimplePrincipalKeyValueFactory.cs | 9 +- .../RelationalModelValidatorTest.cs | 19 +- .../Update/CommandBatchPreparerTest.cs | 2 +- .../SqlServerModelValidatorTest.cs | 15 - .../SqliteModelValidatorTest.cs | 18 - 37 files changed, 1686 insertions(+), 486 deletions(-) create mode 100644 src/EFCore.Relational/Metadata/Internal/TableBaseIdentityComparer.cs create mode 100644 src/EFCore.Relational/Update/Internal/CompositeRowValueFactory.cs rename src/EFCore.Relational/Update/Internal/{KeyValueIndexFactorySource.cs => IRowForeignKeyValueFactory.cs} (65%) rename src/EFCore.Relational/Update/Internal/{IKeyValueIndexFactorySource.cs => IRowForeignKeyValueFactoryFactory.cs} (71%) rename src/EFCore.Relational/Update/Internal/{KeyValueIndexFactory.cs => IRowForeignKeyValueFactory`.cs} (53%) rename src/EFCore.Relational/Update/Internal/{IKeyValueIndexFactory.cs => IRowIdentityMap.cs} (85%) create mode 100644 src/EFCore.Relational/Update/Internal/IRowKeyValueFactory.cs rename src/EFCore.Relational/Update/Internal/{IKeyValueIndex.cs => IRowKeyValueFactoryFactory.cs} (91%) create mode 100644 src/EFCore.Relational/Update/Internal/IRowKeyValueFactory`.cs create mode 100644 src/EFCore.Relational/Update/Internal/RowForeignKeyValueFactory.cs create mode 100644 src/EFCore.Relational/Update/Internal/RowForeignKeyValueFactoryFactory.cs create mode 100644 src/EFCore.Relational/Update/Internal/RowIdentityMap.cs create mode 100644 src/EFCore.Relational/Update/Internal/RowKeyValueFactoryFactory.cs diff --git a/src/EFCore.Relational/Infrastructure/EntityFrameworkRelationalServicesBuilder.cs b/src/EFCore.Relational/Infrastructure/EntityFrameworkRelationalServicesBuilder.cs index 43708a7b249..13a6cbb92f6 100644 --- a/src/EFCore.Relational/Infrastructure/EntityFrameworkRelationalServicesBuilder.cs +++ b/src/EFCore.Relational/Infrastructure/EntityFrameworkRelationalServicesBuilder.cs @@ -44,7 +44,8 @@ public class EntityFrameworkRelationalServicesBuilder : EntityFrameworkServicesB public static readonly IDictionary RelationalServices = new Dictionary { - { typeof(IKeyValueIndexFactorySource), new ServiceCharacteristics(ServiceLifetime.Singleton) }, + { typeof(IRowForeignKeyValueFactoryFactory), new ServiceCharacteristics(ServiceLifetime.Singleton) }, + { typeof(IRowKeyValueFactoryFactory), new ServiceCharacteristics(ServiceLifetime.Singleton) }, { typeof(IParameterNameGeneratorFactory), new ServiceCharacteristics(ServiceLifetime.Singleton) }, { typeof(IComparer), new ServiceCharacteristics(ServiceLifetime.Singleton) }, { typeof(IMigrationsIdGenerator), new ServiceCharacteristics(ServiceLifetime.Singleton) }, @@ -125,7 +126,8 @@ public override EntityFrameworkServicesBuilder TryAddCoreServices() TryAdd(); TryAdd, ModificationCommandComparer>(); TryAdd(); - TryAdd(); + TryAdd(); + TryAdd(); TryAdd(); TryAdd(); TryAdd(); diff --git a/src/EFCore.Relational/Infrastructure/RelationalModelDependencies.cs b/src/EFCore.Relational/Infrastructure/RelationalModelDependencies.cs index 725acf89e50..fbe6ed942fd 100644 --- a/src/EFCore.Relational/Infrastructure/RelationalModelDependencies.cs +++ b/src/EFCore.Relational/Infrastructure/RelationalModelDependencies.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using Microsoft.EntityFrameworkCore.Update.Internal; + namespace Microsoft.EntityFrameworkCore.Infrastructure; /// @@ -45,7 +47,29 @@ public sealed record RelationalModelDependencies /// the constructor at any point in this process. /// [EntityFrameworkInternal] - public RelationalModelDependencies() + public RelationalModelDependencies( + IRowKeyValueFactoryFactory rowKeyValueFactoryFactory, + IRowForeignKeyValueFactoryFactory foreignKeyRowValueFactorySource) { + RowKeyValueFactoryFactory = rowKeyValueFactoryFactory; + RowForeignKeyValueFactoryFactory = foreignKeyRowValueFactorySource; } + + /// + /// 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 IRowKeyValueFactoryFactory RowKeyValueFactoryFactory { get; init; } + + /// + /// 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 IRowForeignKeyValueFactoryFactory RowForeignKeyValueFactoryFactory { get; init; } } diff --git a/src/EFCore.Relational/Infrastructure/RelationalModelValidator.cs b/src/EFCore.Relational/Infrastructure/RelationalModelValidator.cs index b1066abdcfe..c37bef9800f 100644 --- a/src/EFCore.Relational/Infrastructure/RelationalModelValidator.cs +++ b/src/EFCore.Relational/Infrastructure/RelationalModelValidator.cs @@ -559,10 +559,10 @@ protected virtual void ValidateSharedViewCompatibility( mappedTypes.Add(entityType); } - foreach (var (table, mappedTypes) in views) + foreach (var (view, mappedTypes) in views) { - ValidateSharedViewCompatibility(mappedTypes, table.Name, table.Schema, logger); - ValidateSharedColumnsCompatibility(mappedTypes, table, logger); + ValidateSharedViewCompatibility(mappedTypes, view.Name, view.Schema, logger); + ValidateSharedColumnsCompatibility(mappedTypes, view, logger); } } @@ -866,10 +866,12 @@ protected virtual void ValidateCompatible( storeObject.DisplayName())); } + var typeMapping = property.GetRelationalTypeMapping(); + var duplicateTypeMapping = duplicateProperty.GetRelationalTypeMapping(); var currentTypeString = property.GetColumnType(storeObject) - ?? property.GetRelationalTypeMapping().StoreType; + ?? typeMapping.StoreType; var previousTypeString = duplicateProperty.GetColumnType(storeObject) - ?? duplicateProperty.GetRelationalTypeMapping().StoreType; + ?? duplicateTypeMapping.StoreType; if (!string.Equals(currentTypeString, previousTypeString, StringComparison.OrdinalIgnoreCase)) { throw new InvalidOperationException( @@ -884,6 +886,22 @@ protected virtual void ValidateCompatible( currentTypeString)); } + var currentProviderType = typeMapping.Converter?.ProviderClrType ?? typeMapping.ClrType; + var previousProviderType = duplicateTypeMapping.Converter?.ProviderClrType ?? duplicateTypeMapping.ClrType; + if (currentProviderType != previousProviderType) + { + throw new InvalidOperationException( + RelationalStrings.DuplicateColumnNameProviderTypeMismatch( + duplicateProperty.DeclaringEntityType.DisplayName(), + duplicateProperty.Name, + property.DeclaringEntityType.DisplayName(), + property.Name, + columnName, + storeObject.DisplayName(), + previousProviderType.ShortDisplayName(), + currentProviderType.ShortDisplayName())); + } + var currentComputedColumnSql = property.GetComputedColumnSql(storeObject) ?? ""; var previousComputedColumnSql = duplicateProperty.GetComputedColumnSql(storeObject) ?? ""; if (!currentComputedColumnSql.Equals(previousComputedColumnSql, StringComparison.OrdinalIgnoreCase)) @@ -1340,8 +1358,8 @@ protected override void ValidateInheritanceMapping( RelationalStrings.NonTphMappingStrategy(mappingStrategy, entityType.DisplayName())); } - ValidateTPHMapping(entityType, forTables: false); - ValidateTPHMapping(entityType, forTables: true); + ValidateTphMapping(entityType, forTables: false); + ValidateTphMapping(entityType, forTables: true); ValidateDiscriminatorValues(entityType); } else @@ -1362,8 +1380,8 @@ protected override void ValidateInheritanceMapping( RelationalStrings.KeylessMappingStrategy(mappingStrategy ?? RelationalAnnotationNames.TptMappingStrategy, entityType.DisplayName())); } - ValidateNonTPHMapping(entityType, forTables: false); - ValidateNonTPHMapping(entityType, forTables: true); + ValidateNonTphMapping(entityType, forTables: false); + ValidateNonTphMapping(entityType, forTables: true); } } } @@ -1387,7 +1405,7 @@ protected virtual void ValidateMappingStrategy(string? mappingStrategy, IEntityT }; } - private static void ValidateNonTPHMapping(IEntityType rootEntityType, bool forTables) + private static void ValidateNonTphMapping(IEntityType rootEntityType, bool forTables) { var derivedTypes = new Dictionary<(string, string?), IEntityType>(); foreach (var entityType in rootEntityType.GetDerivedTypesInclusive()) @@ -1413,7 +1431,7 @@ private static void ValidateNonTPHMapping(IEntityType rootEntityType, bool forTa } } - private static void ValidateTPHMapping(IEntityType rootEntityType, bool forTables) + private static void ValidateTphMapping(IEntityType rootEntityType, bool forTables) { string? firstName = null; string? firstSchema = null; diff --git a/src/EFCore.Relational/Metadata/IColumn.cs b/src/EFCore.Relational/Metadata/IColumn.cs index 3a933958693..030e53e30b5 100644 --- a/src/EFCore.Relational/Metadata/IColumn.cs +++ b/src/EFCore.Relational/Metadata/IColumn.cs @@ -98,8 +98,7 @@ public virtual bool TryGetDefaultValue(out object? defaultValue) continue; } - var converter = property.GetValueConverter() ?? PropertyMappings.First().TypeMapping.Converter; - + var converter = property.GetValueConverter() ?? mapping.TypeMapping.Converter; if (converter != null) { defaultValue = converter.ConvertToProvider(defaultValue); diff --git a/src/EFCore.Relational/Metadata/IColumnBase.cs b/src/EFCore.Relational/Metadata/IColumnBase.cs index 5489e0bdb06..24ffbed682a 100644 --- a/src/EFCore.Relational/Metadata/IColumnBase.cs +++ b/src/EFCore.Relational/Metadata/IColumnBase.cs @@ -21,6 +21,18 @@ public interface IColumnBase : IAnnotatable /// string StoreType { get; } + /// + /// Gets the provider type. + /// + Type ProviderClrType + { + get + { + var typeMapping = PropertyMappings.First().TypeMapping; + return typeMapping.Converter?.ProviderClrType ?? typeMapping.ClrType; + } + } + /// /// Gets the value indicating whether the column can contain NULL. /// diff --git a/src/EFCore.Relational/Metadata/IForeignKeyConstraint.cs b/src/EFCore.Relational/Metadata/IForeignKeyConstraint.cs index 18c0b4ea011..2a0314c4bb0 100644 --- a/src/EFCore.Relational/Metadata/IForeignKeyConstraint.cs +++ b/src/EFCore.Relational/Metadata/IForeignKeyConstraint.cs @@ -42,7 +42,12 @@ public interface IForeignKeyConstraint : IAnnotatable /// /// Gets the columns that are referenced by the foreign key constraint. /// - IReadOnlyList PrincipalColumns { get; } + IReadOnlyList PrincipalColumns => PrincipalUniqueConstraint.Columns; + + /// + /// Gets the unique constraint on the columns referenced by the foreign key constraint. + /// + IUniqueConstraint PrincipalUniqueConstraint { get; } /// /// Gets the action to be performed when the referenced row is deleted. diff --git a/src/EFCore.Relational/Metadata/Internal/ForeignKeyConstraint.cs b/src/EFCore.Relational/Metadata/Internal/ForeignKeyConstraint.cs index 23ad7ddcf8c..471f466462f 100644 --- a/src/EFCore.Relational/Metadata/Internal/ForeignKeyConstraint.cs +++ b/src/EFCore.Relational/Metadata/Internal/ForeignKeyConstraint.cs @@ -1,6 +1,9 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using Microsoft.EntityFrameworkCore.Internal; +using Microsoft.EntityFrameworkCore.Update.Internal; + namespace Microsoft.EntityFrameworkCore.Metadata.Internal; /// @@ -22,14 +25,14 @@ public ForeignKeyConstraint( Table table, Table principalTable, IReadOnlyList columns, - IReadOnlyList principalColumns, + UniqueConstraint principalUniqueConstraint, ReferentialAction onDeleteAction) { Name = name; Table = table; PrincipalTable = principalTable; Columns = columns; - PrincipalColumns = principalColumns; + PrincipalUniqueConstraint = principalUniqueConstraint; OnDeleteAction = onDeleteAction; } @@ -74,7 +77,15 @@ public ForeignKeyConstraint( /// 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 PrincipalColumns { get; } + public virtual IReadOnlyList PrincipalColumns => PrincipalUniqueConstraint.Columns; + + /// + /// 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 UniqueConstraint PrincipalUniqueConstraint { get; } /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -88,6 +99,19 @@ public override bool IsReadOnly /// public virtual ReferentialAction OnDeleteAction { get; set; } + private IRowForeignKeyValueFactory? _foreignKeyRowValueFactory; + + /// + /// 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 IRowForeignKeyValueFactory GetRowForeignKeyValueFactory() + => NonCapturingLazyInitializer.EnsureInitialized( + ref _foreignKeyRowValueFactory, this, + static constraint => constraint.Table.Model.Model.GetRelationalDependencies().RowForeignKeyValueFactoryFactory.Create(constraint)); + /// /// 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 @@ -116,4 +140,8 @@ IReadOnlyList IForeignKeyConstraint.Columns /// IReadOnlyList IForeignKeyConstraint.PrincipalColumns => PrincipalColumns; + + /// + IUniqueConstraint IForeignKeyConstraint.PrincipalUniqueConstraint + => PrincipalUniqueConstraint; } diff --git a/src/EFCore.Relational/Metadata/Internal/RelationalModel.cs b/src/EFCore.Relational/Metadata/Internal/RelationalModel.cs index 2a2e6c775e5..47f06a8eef6 100644 --- a/src/EFCore.Relational/Metadata/Internal/RelationalModel.cs +++ b/src/EFCore.Relational/Metadata/Internal/RelationalModel.cs @@ -152,6 +152,7 @@ public static IRelationalModel Create( { PopulateRowInternalForeignKeys(table); PopulateTableConfiguration(table, designTime); + PopulateForeignKeyConstraints(table); if (relationalAnnotationProvider != null) { @@ -799,117 +800,6 @@ private static void PopulateTableConfiguration(Table table, bool designTime) } var entityType = entityTypeMapping.EntityType; - foreach (var foreignKey in entityType.GetForeignKeys()) - { - var firstPrincipalMapping = true; - foreach (var principalMapping in foreignKey.PrincipalEntityType.GetTableMappings().Reverse()) - { - if (firstPrincipalMapping - && !principalMapping.IncludesDerivedTypes - && foreignKey.PrincipalEntityType.GetDirectlyDerivedTypes().Any(e => e.GetTableMappings().Any())) - { - // Derived principal entity types are mapped to different tables, so the constraint is not enforceable - // TODO: Allow this to be overriden #15854 - break; - } - - firstPrincipalMapping = false; - - var principalTable = (Table)principalMapping.Table; - var name = foreignKey.GetConstraintName( - storeObject, - StoreObjectIdentifier.Table(principalTable.Name, principalTable.Schema)); - if (name == null) - { - continue; - } - - var foreignKeyConstraints = foreignKey.FindRuntimeAnnotationValue(RelationalAnnotationNames.ForeignKeyMappings) - as SortedSet; - if (table.ForeignKeyConstraints.TryGetValue(name, out var constraint)) - { - if (foreignKeyConstraints == null) - { - foreignKeyConstraints = new SortedSet(ForeignKeyConstraintComparer.Instance); - foreignKey.AddRuntimeAnnotation(RelationalAnnotationNames.ForeignKeyMappings, foreignKeyConstraints); - } - - foreignKeyConstraints.Add(constraint); - - constraint.MappedForeignKeys.Add(foreignKey); - break; - } - - var principalColumns = new Column[foreignKey.Properties.Count]; - for (var i = 0; i < principalColumns.Length; i++) - { - if (principalTable.FindColumn(foreignKey.PrincipalKey.Properties[i]) is Column principalColumn) - { - principalColumns[i] = principalColumn; - } - else - { - principalColumns = null; - break; - } - } - - if (principalColumns == null) - { - continue; - } - - var columns = new Column[foreignKey.Properties.Count]; - for (var i = 0; i < columns.Length; i++) - { - if (table.FindColumn(foreignKey.Properties[i]) is Column foreignKeyColumn) - { - columns[i] = foreignKeyColumn; - } - else - { - columns = null; - break; - } - } - - if (columns == null) - { - break; - } - - if (columns.SequenceEqual(principalColumns)) - { - // Principal and dependent properties are mapped to the same columns so the constraint is redundant - break; - } - - if (entityTypeMapping.IncludesDerivedTypes - && foreignKey.DeclaringEntityType != entityType - && entityType.FindPrimaryKey() is IKey primaryKey - && foreignKey.Properties.SequenceEqual(primaryKey.Properties)) - { - // The identifying FK constraint is needed to be created only on the table that corresponds - // to the declaring entity type - break; - } - - constraint = new ForeignKeyConstraint( - name, table, principalTable, columns, principalColumns, ToReferentialAction(foreignKey.DeleteBehavior)); - constraint.MappedForeignKeys.Add(foreignKey); - - if (foreignKeyConstraints == null) - { - foreignKeyConstraints = new SortedSet(ForeignKeyConstraintComparer.Instance); - foreignKey.AddRuntimeAnnotation(RelationalAnnotationNames.ForeignKeyMappings, foreignKeyConstraints); - } - - foreignKeyConstraints.Add(constraint); - table.ForeignKeyConstraints.Add(name, constraint); - break; - } - } - foreach (var key in entityType.GetKeys()) { var name = key.GetName(storeObject); @@ -1167,6 +1057,140 @@ private static void PopulateRowInternalForeignKeys(TableBase table) } } + private static void PopulateForeignKeyConstraints(Table table) + { + var storeObject = StoreObjectIdentifier.Table(table.Name, table.Schema); + foreach (var entityTypeMapping in ((ITable)table).EntityTypeMappings) + { + if (!entityTypeMapping.IncludesDerivedTypes + && entityTypeMapping.EntityType.GetTableMappings().Any(m => m.IncludesDerivedTypes)) + { + continue; + } + + var entityType = entityTypeMapping.EntityType; + foreach (var foreignKey in entityType.GetForeignKeys()) + { + var firstPrincipalMapping = true; + foreach (var principalMapping in foreignKey.PrincipalEntityType.GetTableMappings().Reverse()) + { + if (firstPrincipalMapping + && !principalMapping.IncludesDerivedTypes + && foreignKey.PrincipalEntityType.GetDirectlyDerivedTypes().Any(e => e.GetTableMappings().Any())) + { + // Derived principal entity types are mapped to different tables, so the constraint is not enforceable + // TODO: Allow this to be overriden #15854 + break; + } + + firstPrincipalMapping = false; + + var principalTable = (Table)principalMapping.Table; + var principalStoreObject = StoreObjectIdentifier.Table(principalTable.Name, principalTable.Schema); + var name = foreignKey.GetConstraintName(storeObject, principalStoreObject); + if (name == null) + { + continue; + } + + var foreignKeyConstraints = foreignKey.FindRuntimeAnnotationValue(RelationalAnnotationNames.ForeignKeyMappings) + as SortedSet; + if (table.ForeignKeyConstraints.TryGetValue(name, out var constraint)) + { + if (foreignKeyConstraints == null) + { + foreignKeyConstraints = new SortedSet(ForeignKeyConstraintComparer.Instance); + foreignKey.AddRuntimeAnnotation(RelationalAnnotationNames.ForeignKeyMappings, foreignKeyConstraints); + } + + foreignKeyConstraints.Add(constraint); + + constraint.MappedForeignKeys.Add(foreignKey); + break; + } + + var principalColumns = new Column[foreignKey.Properties.Count]; + for (var i = 0; i < principalColumns.Length; i++) + { + if (principalTable.FindColumn(foreignKey.PrincipalKey.Properties[i]) is Column principalColumn) + { + principalColumns[i] = principalColumn; + } + else + { + principalColumns = null; + break; + } + } + + if (principalColumns == null) + { + continue; + } + + var columns = new Column[foreignKey.Properties.Count]; + for (var i = 0; i < columns.Length; i++) + { + if (table.FindColumn(foreignKey.Properties[i]) is Column foreignKeyColumn) + { + columns[i] = foreignKeyColumn; + } + else + { + columns = null; + break; + } + } + + if (columns == null) + { + break; + } + + if (columns.SequenceEqual(principalColumns)) + { + // Principal and dependent properties are mapped to the same columns so the constraint is redundant + break; + } + + if (entityTypeMapping.IncludesDerivedTypes + && foreignKey.DeclaringEntityType != entityType + && entityType.FindPrimaryKey() is IKey primaryKey + && foreignKey.Properties.SequenceEqual(primaryKey.Properties)) + { + // The identifying FK constraint is needed to be created only on the table that corresponds + // to the declaring entity type + break; + } + + var principalUniqueConstraintName = foreignKey.PrincipalKey.GetName(principalStoreObject); + if (principalUniqueConstraintName == null) + { + continue; + } + + var principalUniqueConstraint = principalTable.FindUniqueConstraint(principalUniqueConstraintName)!; + + Check.DebugAssert(principalUniqueConstraint != null, "Invalid unique constraint " + principalUniqueConstraintName); + + constraint = new ForeignKeyConstraint( + name, table, principalTable, columns, principalUniqueConstraint, ToReferentialAction(foreignKey.DeleteBehavior)); + constraint.MappedForeignKeys.Add(foreignKey); + + if (foreignKeyConstraints == null) + { + foreignKeyConstraints = new SortedSet(ForeignKeyConstraintComparer.Instance); + foreignKey.AddRuntimeAnnotation(RelationalAnnotationNames.ForeignKeyMappings, foreignKeyConstraints); + } + + foreignKeyConstraints.Add(constraint); + table.ForeignKeyConstraints.Add(name, constraint); + 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 diff --git a/src/EFCore.Relational/Metadata/Internal/TableBaseIdentityComparer.cs b/src/EFCore.Relational/Metadata/Internal/TableBaseIdentityComparer.cs new file mode 100644 index 00000000000..1972b3eedf5 --- /dev/null +++ b/src/EFCore.Relational/Metadata/Internal/TableBaseIdentityComparer.cs @@ -0,0 +1,47 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +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 TableBaseIdentityComparer : IEqualityComparer +{ + /// + public bool Equals(ITableBase? x, ITableBase? y) + { + if (ReferenceEquals(x, y)) + { + return true; + } + + if (x is null) + { + return y is null; + } + + if (y is null) + { + return false; + } + + return x.Name == y.Name + && x.Schema == y.Schema; + } + + /// + public int GetHashCode([DisallowNull] ITableBase obj) + { + var hash = new HashCode(); + hash.Add(obj.Name); + hash.Add(obj.Schema); + + return hash.ToHashCode(); + } +} diff --git a/src/EFCore.Relational/Metadata/Internal/UniqueConstraint.cs b/src/EFCore.Relational/Metadata/Internal/UniqueConstraint.cs index 547bbd08a72..a568422f835 100644 --- a/src/EFCore.Relational/Metadata/Internal/UniqueConstraint.cs +++ b/src/EFCore.Relational/Metadata/Internal/UniqueConstraint.cs @@ -1,6 +1,9 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using Microsoft.EntityFrameworkCore.Internal; +using Microsoft.EntityFrameworkCore.Update.Internal; + namespace Microsoft.EntityFrameworkCore.Metadata.Internal; /// @@ -63,6 +66,28 @@ public UniqueConstraint( public override bool IsReadOnly => Table.Model.IsReadOnly; + private object? _rowKeyValueFactory; + + /// + /// 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 IRowKeyValueFactory GetRowKeyValueFactory() + => (IRowKeyValueFactory)NonCapturingLazyInitializer.EnsureInitialized( + ref _rowKeyValueFactory, this, + static constraint => constraint.Table.Model.Model.GetRelationalDependencies().RowKeyValueFactoryFactory.Create(constraint)); + + /// + /// 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 GetKeyType() + => Columns.Count > 1 ? typeof(object[]) : ((IColumnBase)Columns.First()).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 diff --git a/src/EFCore.Relational/Migrations/Internal/MigrationsModelDiffer.cs b/src/EFCore.Relational/Migrations/Internal/MigrationsModelDiffer.cs index 7e9ebc3ba4e..24e3f3bf51d 100644 --- a/src/EFCore.Relational/Migrations/Internal/MigrationsModelDiffer.cs +++ b/src/EFCore.Relational/Migrations/Internal/MigrationsModelDiffer.cs @@ -1691,7 +1691,7 @@ protected virtual void TrackData( foreach (var targetEntityType in target.Model.GetEntityTypes()) { - foreach (var targetSeed in targetEntityType.GetSeedData()) + foreach (var targetSeed in targetEntityType.GetSeedData(providerValues: true)) { var targetEntry = _targetUpdateAdapter.CreateEntry(targetSeed, targetEntityType); if (targetEntry.ToEntityEntry().Entity is Dictionary targetBag) @@ -2165,6 +2165,26 @@ protected virtual IEnumerable GetDataOperations( } } + /// + /// 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. + /// + private IModificationCommand CreateCommand(IDictionary values, IEntityType entityType) + { + var i = 0; + var valuesArray = new object?[entityType.PropertyCount()]; + foreach (var property in entityType.GetProperties()) + { + valuesArray[i++] = values.TryGetValue(property.Name, out var value) + ? value + : property.ClrType.GetDefaultValue(); + } + + return entry; + } + private IEnumerable GetDataOperations( bool forSource, Dictionary>? changedTableMappings, diff --git a/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs b/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs index b3727c74382..f21f5200323 100644 --- a/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs +++ b/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs @@ -429,6 +429,14 @@ public static string DuplicateColumnNamePrecisionMismatch(object? entityType1, o GetString("DuplicateColumnNamePrecisionMismatch", nameof(entityType1), nameof(property1), nameof(entityType2), nameof(property2), nameof(columnName), nameof(table), nameof(precision1), nameof(precision2)), entityType1, property1, entityType2, property2, columnName, table, precision1, precision2); + /// + /// '{entityType1}.{property1}' and '{entityType2}.{property2}' are both mapped to column '{columnName}' in '{table}', but are configured to use provider types ('{type1}' and '{type2}'). + /// + public static string DuplicateColumnNameProviderTypeMismatch(object? entityType1, object? property1, object? entityType2, object? property2, object? columnName, object? table, object? type1, object? type2) + => string.Format( + GetString("DuplicateColumnNameProviderTypeMismatch", nameof(entityType1), nameof(property1), nameof(entityType2), nameof(property2), nameof(columnName), nameof(table), nameof(type1), nameof(type2)), + entityType1, property1, entityType2, property2, columnName, table, type1, type2); + /// /// '{entityType1}.{property1}' and '{entityType2}.{property2}' are both mapped to column '{columnName}' in '{table}', but are configured with different scales ('{scale1}' and '{scale2}'). /// @@ -1192,7 +1200,7 @@ public static string TriggerOnUnmappedEntityType(object? trigger, object? entity trigger, entityType); /// - /// Trigger '{trigger}' with table '{triggerTable}' is defined on entity type '{entityType}', which is mapped to table '{entityTable}'. See https://aka.ms/efcore-docs-triggers for more information on triggers. + /// Trigger '{trigger}' for table '{triggerTable}' is defined on entity type '{entityType}', which is mapped to table '{entityTable}'. See https://aka.ms/efcore-docs-triggers for more information on triggers. /// public static string TriggerWithMismatchedTable(object? trigger, object? triggerTable, object? entityType, object? entityTable) => string.Format( diff --git a/src/EFCore.Relational/Properties/RelationalStrings.resx b/src/EFCore.Relational/Properties/RelationalStrings.resx index 6e09693e958..1fab0671d0e 100644 --- a/src/EFCore.Relational/Properties/RelationalStrings.resx +++ b/src/EFCore.Relational/Properties/RelationalStrings.resx @@ -274,6 +274,9 @@ '{entityType1}.{property1}' and '{entityType2}.{property2}' are both mapped to column '{columnName}' in '{table}', but are configured with different precisions ('{precision1}' and '{precision2}'). + + '{entityType1}.{property1}' and '{entityType2}.{property2}' are both mapped to column '{columnName}' in '{table}', but are configured to use provider types ('{type1}' and '{type2}'). + '{entityType1}.{property1}' and '{entityType2}.{property2}' are both mapped to column '{columnName}' in '{table}', but are configured with different scales ('{scale1}' and '{scale2}'). diff --git a/src/EFCore.Relational/Storage/Internal/TypeMappedRelationalParameter.cs b/src/EFCore.Relational/Storage/Internal/TypeMappedRelationalParameter.cs index 93f7223cc0f..2bef9a633be 100644 --- a/src/EFCore.Relational/Storage/Internal/TypeMappedRelationalParameter.cs +++ b/src/EFCore.Relational/Storage/Internal/TypeMappedRelationalParameter.cs @@ -50,8 +50,6 @@ public TypeMappedRelationalParameter( /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public override void AddDbParameter(DbCommand command, object? value) - => command.Parameters - .Add( - RelationalTypeMapping - .CreateParameter(command, Name, value, IsNullable)); + => command.Parameters.Add( + RelationalTypeMapping.CreateParameter(command, Name, value, IsNullable)); } diff --git a/src/EFCore.Relational/Storage/RelationalTypeMapping.cs b/src/EFCore.Relational/Storage/RelationalTypeMapping.cs index fff9daad971..5aa90200b6d 100644 --- a/src/EFCore.Relational/Storage/RelationalTypeMapping.cs +++ b/src/EFCore.Relational/Storage/RelationalTypeMapping.cs @@ -3,6 +3,7 @@ using System.Data; using System.Globalization; +using Microsoft.EntityFrameworkCore.Internal; namespace Microsoft.EntityFrameworkCore.Storage; @@ -260,6 +261,8 @@ protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters p => this; } + private ValueComparer? _comparer; + /// /// Initializes a new instance of the class. /// @@ -274,55 +277,17 @@ protected RelationalTypeMapping(RelationalTypeMappingParameters parameters) StoreTypeNameBase = storeTypeNameBase; StoreType = ProcessStoreType(parameters, storeType, storeTypeNameBase); - } - /// - /// Processes the store type name to add appropriate postfix/prefix text as needed. - /// - /// The parameters for this mapping. - /// The specified store type name. - /// The calculated based name - /// The store type name to use. - protected virtual string ProcessStoreType( - RelationalTypeMappingParameters parameters, - string storeType, - string storeTypeNameBase) - { - var size = parameters.Size; - - if (size != null - && parameters.StoreTypePostfix == StoreTypePostfix.Size) - { - storeType = storeTypeNameBase + "(" + size + ")"; - } - else if (parameters.StoreTypePostfix == StoreTypePostfix.PrecisionAndScale - || parameters.StoreTypePostfix == StoreTypePostfix.Precision) + static string GetBaseName(string storeType) { - var precision = parameters.Precision; - if (precision != null) + var openParen = storeType.IndexOf("(", StringComparison.Ordinal); + if (openParen >= 0) { - var scale = parameters.Scale; - storeType = storeTypeNameBase - + "(" - + (scale == null || parameters.StoreTypePostfix == StoreTypePostfix.Precision - ? precision.ToString() - : precision + "," + scale) - + ")"; + storeType = storeType[..openParen]; } - } - return storeType; - } - - private static string GetBaseName(string storeType) - { - var openParen = storeType.IndexOf("(", StringComparison.Ordinal); - if (openParen >= 0) - { - storeType = storeType[..openParen]; + return storeType; } - - return storeType; } /// @@ -357,48 +322,6 @@ protected RelationalTypeMapping( /// protected new virtual RelationalTypeMappingParameters Parameters { get; } - /// - /// Creates a copy of this mapping. - /// - /// The parameters for this mapping. - /// The newly created mapping. - protected abstract RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters); - - /// - /// Creates a copy of this mapping. - /// - /// The name of the database type. - /// The size of data the property is configured to store, or null if no size is configured. - /// The newly created mapping. - public virtual RelationalTypeMapping Clone(string storeType, int? size) - => Clone(Parameters.WithStoreTypeAndSize(storeType, size)); - - /// - /// Creates a copy of this mapping. - /// - /// The precision of data the property is configured to store, or null if no size is configured. - /// The scale of data the property is configured to store, or null if no size is configured. - /// The newly created mapping. - public virtual RelationalTypeMapping Clone(int? precision, int? scale) - => Clone(Parameters.WithPrecisionAndScale(precision, scale)); - - /// - /// Returns a new copy of this type mapping with the given - /// added. - /// - /// The converter to use. - /// A new type mapping - public override CoreTypeMapping Clone(ValueConverter? converter) - => Clone(Parameters.WithComposedConverter(converter)); - - /// - /// Clones the type mapping to update facets from the mapping info, if needed. - /// - /// The mapping info containing the facets to use. - /// The cloned mapping, or the original mapping if no clone was needed. - public virtual RelationalTypeMapping Clone(in RelationalTypeMappingInfo mappingInfo) - => Clone(Parameters.WithTypeMappingInfo(mappingInfo)); - /// /// Gets the name of the database type. /// @@ -457,6 +380,95 @@ public virtual bool IsFixedLength protected virtual string SqlLiteralFormatString => "{0}"; + /// + /// A for the provider CLR type values. + /// + public virtual ValueComparer ProviderComparer + => NonCapturingLazyInitializer.EnsureInitialized( + ref _comparer, + this, + static c => ValueComparer.CreateDefault(c.Converter?.ProviderClrType ?? c.ClrType, favorStructuralComparisons: true)); + + /// + /// Creates a copy of this mapping. + /// + /// The parameters for this mapping. + /// The newly created mapping. + protected abstract RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters); + + /// + /// Creates a copy of this mapping. + /// + /// The name of the database type. + /// The size of data the property is configured to store, or null if no size is configured. + /// The newly created mapping. + public virtual RelationalTypeMapping Clone(string storeType, int? size) + => Clone(Parameters.WithStoreTypeAndSize(storeType, size)); + + /// + /// Creates a copy of this mapping. + /// + /// The precision of data the property is configured to store, or null if no size is configured. + /// The scale of data the property is configured to store, or null if no size is configured. + /// The newly created mapping. + public virtual RelationalTypeMapping Clone(int? precision, int? scale) + => Clone(Parameters.WithPrecisionAndScale(precision, scale)); + + /// + /// Returns a new copy of this type mapping with the given + /// added. + /// + /// The converter to use. + /// A new type mapping + public override CoreTypeMapping Clone(ValueConverter? converter) + => Clone(Parameters.WithComposedConverter(converter)); + + /// + /// Clones the type mapping to update facets from the mapping info, if needed. + /// + /// The mapping info containing the facets to use. + /// The cloned mapping, or the original mapping if no clone was needed. + public virtual RelationalTypeMapping Clone(in RelationalTypeMappingInfo mappingInfo) + => Clone(Parameters.WithTypeMappingInfo(mappingInfo)); + + /// + /// Processes the store type name to add appropriate postfix/prefix text as needed. + /// + /// The parameters for this mapping. + /// The specified store type name. + /// The calculated based name + /// The store type name to use. + protected virtual string ProcessStoreType( + RelationalTypeMappingParameters parameters, + string storeType, + string storeTypeNameBase) + { + var size = parameters.Size; + + if (size != null + && parameters.StoreTypePostfix == StoreTypePostfix.Size) + { + storeType = storeTypeNameBase + "(" + size + ")"; + } + else if (parameters.StoreTypePostfix == StoreTypePostfix.PrecisionAndScale + || parameters.StoreTypePostfix == StoreTypePostfix.Precision) + { + var precision = parameters.Precision; + if (precision != null) + { + var scale = parameters.Scale; + storeType = storeTypeNameBase + + "(" + + (scale == null || parameters.StoreTypePostfix == StoreTypePostfix.Precision + ? precision.ToString() + : precision + "," + scale) + + ")"; + } + } + + return storeType; + } + /// /// Creates a with the appropriate type information configured. /// diff --git a/src/EFCore.Relational/Update/ColumnModificationParameters.cs b/src/EFCore.Relational/Update/ColumnModificationParameters.cs index 7e063db2625..61794cc2194 100644 --- a/src/EFCore.Relational/Update/ColumnModificationParameters.cs +++ b/src/EFCore.Relational/Update/ColumnModificationParameters.cs @@ -131,8 +131,6 @@ public ColumnModificationParameters( GenerateParameterName = null; Entry = null; - - //IsConcurrencyToken = false; } /// @@ -175,7 +173,5 @@ public ColumnModificationParameters( GenerateParameterName = generateParameterName; Entry = entry; - - //IsConcurrencyToken = false; } } diff --git a/src/EFCore.Relational/Update/Internal/CommandBatchPreparer.cs b/src/EFCore.Relational/Update/Internal/CommandBatchPreparer.cs index 28e73094e27..03ec34ca042 100644 --- a/src/EFCore.Relational/Update/Internal/CommandBatchPreparer.cs +++ b/src/EFCore.Relational/Update/Internal/CommandBatchPreparer.cs @@ -575,13 +575,11 @@ private void AddForeignKeyEdges( var dependentKeyValue = Dependencies.KeyValueIndexFactorySource .GetKeyValueIndexFactory(foreignKey.PrincipalKey) .CreateDependentKeyValue(entry, foreignKey); - if (dependentKeyValue == null) + if (dependentKeyValue != null) { - continue; + AddMatchingPredecessorEdge( + predecessorsMap, dependentKeyValue, commandGraph, command, foreignKey); } - - AddMatchingPredecessorEdge( - predecessorsMap, dependentKeyValue, commandGraph, command, foreignKey); } } diff --git a/src/EFCore.Relational/Update/Internal/CommandBatchPreparerDependencies.cs b/src/EFCore.Relational/Update/Internal/CommandBatchPreparerDependencies.cs index 21ee0a9b876..8260e0a8061 100644 --- a/src/EFCore.Relational/Update/Internal/CommandBatchPreparerDependencies.cs +++ b/src/EFCore.Relational/Update/Internal/CommandBatchPreparerDependencies.cs @@ -49,7 +49,6 @@ public CommandBatchPreparerDependencies( IModificationCommandBatchFactory modificationCommandBatchFactory, IParameterNameGeneratorFactory parameterNameGeneratorFactory, IComparer modificationCommandComparer, - IKeyValueIndexFactorySource keyValueIndexFactorySource, IModificationCommandFactory modificationCommandFactory, ILoggingOptions loggingOptions, IDiagnosticsLogger updateLogger, @@ -58,7 +57,6 @@ public CommandBatchPreparerDependencies( ModificationCommandBatchFactory = modificationCommandBatchFactory; ParameterNameGeneratorFactory = parameterNameGeneratorFactory; ModificationCommandComparer = modificationCommandComparer; - KeyValueIndexFactorySource = keyValueIndexFactorySource; ModificationCommandFactory = modificationCommandFactory; LoggingOptions = loggingOptions; UpdateLogger = updateLogger; @@ -89,14 +87,6 @@ public CommandBatchPreparerDependencies( /// public IComparer ModificationCommandComparer { get; init; } - /// - /// 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 IKeyValueIndexFactorySource KeyValueIndexFactorySource { get; init; } - /// /// 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.Relational/Update/Internal/CompositeRowValueFactory.cs b/src/EFCore.Relational/Update/Internal/CompositeRowValueFactory.cs new file mode 100644 index 00000000000..1200ba4f42a --- /dev/null +++ b/src/EFCore.Relational/Update/Internal/CompositeRowValueFactory.cs @@ -0,0 +1,202 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.EntityFrameworkCore.Update.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 CompositeRowValueFactory : IRowForeignKeyValueFactory +{ + /// + /// 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 CompositeRowValueFactory(IReadOnlyList columns) + { + Columns = columns; + EqualityComparer = CreateEqualityComparer(columns); + } + + /// + /// 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 IEqualityComparer EqualityComparer { 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. + /// + protected virtual IReadOnlyList Columns { get; } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual bool TryCreateFromBuffer(in ValueBuffer valueBuffer, [NotNullWhen(true)] out object[]? key) + { + key = new object[Columns.Count]; + var index = 0; + + foreach (var property in Columns) + { + var value = valueBuffer[property.GetIndex()]; + if (value == null) + { + key = null; + return false; + } + + key[index++] = value; + } + + return true; + } + + /// + /// 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 TryCreateFromCurrentValues(IUpdateEntry entry, [NotNullWhen(true)] out object[]? key) + => TryCreateFromEntry(entry, (e, p) => e.GetCurrentValue(p), out key); + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual bool TryCreateFromPreStoreGeneratedCurrentValues(IUpdateEntry entry, [NotNullWhen(true)] out object[]? key) + => TryCreateFromEntry(entry, (e, p) => e.GetPreStoreGeneratedCurrentValue(p), out key); + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual bool TryCreateFromOriginalValues(IUpdateEntry entry, [NotNullWhen(true)] out object[]? key) + => TryCreateFromEntry(entry, (e, p) => e.GetOriginalValue(p), out key); + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual bool TryCreateFromRelationshipSnapshot(IUpdateEntry entry, [NotNullWhen(true)] out object[]? key) + => TryCreateFromEntry(entry, (e, p) => e.GetRelationshipSnapshotValue(p), out key); + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + protected virtual bool TryCreateFromEntry( + IUpdateEntry entry, + Func getValue, + [NotNullWhen(true)] out object[]? key) + { + key = new object[Columns.Count]; + var index = 0; + + foreach (var property in Columns) + { + var value = getValue(entry, property); + if (value == null) + { + key = null; + return false; + } + + key[index++] = value; + } + + return true; + } + + /// + /// 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. + /// + protected static IEqualityComparer CreateEqualityComparer(IReadOnlyList properties) + => new CompositeCustomComparer(properties.Select(p => p.GetKeyValueComparer()).ToList()); + + private sealed class CompositeCustomComparer : IEqualityComparer + { + private readonly Func[] _equals; + private readonly Func[] _hashCodes; + + public CompositeCustomComparer(IList comparers) + { + _equals = comparers.Select(c => (Func)c.Equals).ToArray(); + _hashCodes = comparers.Select(c => (Func)c.GetHashCode).ToArray(); + } + + public bool Equals(object[]? x, object[]? y) + { + if (ReferenceEquals(x, y)) + { + return true; + } + + if (x is null) + { + return y is null; + } + + if (y is null) + { + return false; + } + + if (x.Length != y.Length) + { + return false; + } + + for (var i = 0; i < x.Length; i++) + { + if (!_equals[i](x[i], y[i])) + { + return false; + } + } + + return true; + } + + public int GetHashCode(object[] obj) + { + var hashCode = 0; + + // ReSharper disable once ForCanBeConvertedToForeach + // ReSharper disable once LoopCanBeConvertedToQuery + for (var i = 0; i < obj.Length; i++) + { + hashCode = (hashCode * 397) ^ _hashCodes[i](obj[i]); + } + + return hashCode; + } + } +} diff --git a/src/EFCore.Relational/Update/Internal/KeyValueIndexFactorySource.cs b/src/EFCore.Relational/Update/Internal/IRowForeignKeyValueFactory.cs similarity index 65% rename from src/EFCore.Relational/Update/Internal/KeyValueIndexFactorySource.cs rename to src/EFCore.Relational/Update/Internal/IRowForeignKeyValueFactory.cs index 4a554a30d1c..6c191d39ffc 100644 --- a/src/EFCore.Relational/Update/Internal/KeyValueIndexFactorySource.cs +++ b/src/EFCore.Relational/Update/Internal/IRowForeignKeyValueFactory.cs @@ -1,9 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Collections.Concurrent; -using JetBrains.Annotations; - namespace Microsoft.EntityFrameworkCore.Update.Internal; /// @@ -12,18 +9,15 @@ namespace Microsoft.EntityFrameworkCore.Update.Internal; /// 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 KeyValueIndexFactorySource : IKeyValueIndexFactorySource +public interface IRowForeignKeyValueFactory { - private readonly ConcurrentDictionary _factories = 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 /// 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 IKeyValueIndexFactory GetKeyValueIndexFactory(IKey key) - => _factories.GetOrAdd(key, Create); + object CreatePrincipalKeyValueIndex(IModificationCommand command, bool fromOriginalValues = false); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -31,14 +25,5 @@ public virtual IKeyValueIndexFactory GetKeyValueIndexFactory(IKey key) /// 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 IKeyValueIndexFactory Create(IKey key) - => (IKeyValueIndexFactory)typeof(KeyValueIndexFactorySource).GetTypeInfo() - .GetDeclaredMethod(nameof(CreateFactory))! - .MakeGenericMethod(key.GetKeyType()) - .Invoke(null, new object[] { key })!; - - [UsedImplicitly] - private static IKeyValueIndexFactory CreateFactory(IKey key) - where TKey : notnull - => new KeyValueIndexFactory(key.GetPrincipalKeyValueFactory()); + object? CreateDependentKeyValueIndex(IModificationCommand command, bool fromOriginalValues = false); } diff --git a/src/EFCore.Relational/Update/Internal/IKeyValueIndexFactorySource.cs b/src/EFCore.Relational/Update/Internal/IRowForeignKeyValueFactoryFactory.cs similarity index 71% rename from src/EFCore.Relational/Update/Internal/IKeyValueIndexFactorySource.cs rename to src/EFCore.Relational/Update/Internal/IRowForeignKeyValueFactoryFactory.cs index aec9e4db31f..f4db0739336 100644 --- a/src/EFCore.Relational/Update/Internal/IKeyValueIndexFactorySource.cs +++ b/src/EFCore.Relational/Update/Internal/IRowForeignKeyValueFactoryFactory.cs @@ -9,12 +9,7 @@ namespace Microsoft.EntityFrameworkCore.Update.Internal; /// 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. /// -/// -/// The service lifetime is . This means a single instance -/// is used by many instances. The implementation must be thread-safe. -/// This service cannot depend on services registered as . -/// -public interface IKeyValueIndexFactorySource +public interface IRowForeignKeyValueFactoryFactory { /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -22,5 +17,5 @@ public interface IKeyValueIndexFactorySource /// 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. /// - IKeyValueIndexFactory GetKeyValueIndexFactory(IKey key); + IRowForeignKeyValueFactory Create(IForeignKeyConstraint foreignKey); } diff --git a/src/EFCore.Relational/Update/Internal/KeyValueIndexFactory.cs b/src/EFCore.Relational/Update/Internal/IRowForeignKeyValueFactory`.cs similarity index 53% rename from src/EFCore.Relational/Update/Internal/KeyValueIndexFactory.cs rename to src/EFCore.Relational/Update/Internal/IRowForeignKeyValueFactory`.cs index c46fa8d4d4e..34ab43787be 100644 --- a/src/EFCore.Relational/Update/Internal/KeyValueIndexFactory.cs +++ b/src/EFCore.Relational/Update/Internal/IRowForeignKeyValueFactory`.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Diagnostics.CodeAnalysis; + namespace Microsoft.EntityFrameworkCore.Update.Internal; /// @@ -9,33 +11,15 @@ namespace Microsoft.EntityFrameworkCore.Update.Internal; /// 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 KeyValueIndexFactory : IKeyValueIndexFactory +public interface IRowForeignKeyValueFactory : IRowForeignKeyValueFactory { - private readonly IPrincipalKeyValueFactory _principalKeyValueFactory; - - /// - /// 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 KeyValueIndexFactory(IPrincipalKeyValueFactory principalKeyValueFactory) - { - _principalKeyValueFactory = principalKeyValueFactory; - } - /// /// 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 IKeyValueIndex CreatePrincipalKeyValue(IUpdateEntry entry, IForeignKey? foreignKey) - => new KeyValueIndex( - foreignKey, - _principalKeyValueFactory.CreateFromCurrentValues(entry), - _principalKeyValueFactory.EqualityComparer, - fromOriginalValues: false); + bool TryCreateDependentKeyValue(object?[] keyValues, [NotNullWhen(true)] out TKey? key); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -43,12 +27,7 @@ public virtual IKeyValueIndex CreatePrincipalKeyValue(IUpdateEntry entry, IForei /// 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 IKeyValueIndex CreatePrincipalKeyValueFromOriginalValues(IUpdateEntry entry, IForeignKey? foreignKey) - => new KeyValueIndex( - foreignKey, - _principalKeyValueFactory.CreateFromOriginalValues(entry), - _principalKeyValueFactory.EqualityComparer, - fromOriginalValues: true); + bool TryCreateDependentKeyValue(IDictionary keyPropertyValues, IEntityType entityType, [NotNullWhen(true)] out TKey? key); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -56,10 +35,7 @@ public virtual IKeyValueIndex CreatePrincipalKeyValueFromOriginalValues(IUpdateE /// 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 IKeyValueIndex? CreateDependentKeyValue(IUpdateEntry entry, IForeignKey foreignKey) - => foreignKey.GetDependentKeyValueFactory()!.TryCreateFromCurrentValues(entry, out var keyValue) - ? new KeyValueIndex(foreignKey, keyValue, _principalKeyValueFactory.EqualityComparer, fromOriginalValues: false) - : null; + bool TryCreateDependentKeyValue(IModificationCommand command, bool fromOriginalValues, [NotNullWhen(true)] out TKey? key); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -67,8 +43,5 @@ public virtual IKeyValueIndex CreatePrincipalKeyValueFromOriginalValues(IUpdateE /// 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 IKeyValueIndex? CreateDependentKeyValueFromOriginalValues(IUpdateEntry entry, IForeignKey foreignKey) - => foreignKey.GetDependentKeyValueFactory()!.TryCreateFromOriginalValues(entry, out var keyValue) - ? new KeyValueIndex(foreignKey, keyValue, _principalKeyValueFactory.EqualityComparer, fromOriginalValues: true) - : null; + IEqualityComparer EqualityComparer { get; } } diff --git a/src/EFCore.Relational/Update/Internal/IKeyValueIndexFactory.cs b/src/EFCore.Relational/Update/Internal/IRowIdentityMap.cs similarity index 85% rename from src/EFCore.Relational/Update/Internal/IKeyValueIndexFactory.cs rename to src/EFCore.Relational/Update/Internal/IRowIdentityMap.cs index cf87907b336..bd5c7b89845 100644 --- a/src/EFCore.Relational/Update/Internal/IKeyValueIndexFactory.cs +++ b/src/EFCore.Relational/Update/Internal/IRowIdentityMap.cs @@ -9,7 +9,7 @@ namespace Microsoft.EntityFrameworkCore.Update.Internal; /// 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 interface IKeyValueIndexFactory +public interface IRowIdentityMap { /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -17,7 +17,7 @@ public interface IKeyValueIndexFactory /// 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. /// - IKeyValueIndex CreatePrincipalKeyValue(IUpdateEntry entry, IForeignKey? foreignKey); + IModificationCommand? TryGetEntry(object?[] keyValues); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -25,7 +25,7 @@ public interface IKeyValueIndexFactory /// 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. /// - IKeyValueIndex CreatePrincipalKeyValueFromOriginalValues(IUpdateEntry entry, IForeignKey? foreignKey); + IModificationCommand? TryGetEntry(IDictionary keyPropertyValues, IEntityType entityType); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -33,7 +33,7 @@ public interface IKeyValueIndexFactory /// 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. /// - IKeyValueIndex? CreateDependentKeyValue(IUpdateEntry entry, IForeignKey foreignKey); + void Add(IModificationCommand command); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -41,5 +41,5 @@ public interface IKeyValueIndexFactory /// 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. /// - IKeyValueIndex? CreateDependentKeyValueFromOriginalValues(IUpdateEntry entry, IForeignKey foreignKey); + void Remove(IModificationCommand command); } diff --git a/src/EFCore.Relational/Update/Internal/IRowKeyValueFactory.cs b/src/EFCore.Relational/Update/Internal/IRowKeyValueFactory.cs new file mode 100644 index 00000000000..faaa9194ab1 --- /dev/null +++ b/src/EFCore.Relational/Update/Internal/IRowKeyValueFactory.cs @@ -0,0 +1,21 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.EntityFrameworkCore.Update.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 interface IRowKeyValueFactory +{ + /// + /// 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. + /// + object CreateKeyValueIndex(IModificationCommand command, bool fromOriginalValues = false); +} diff --git a/src/EFCore.Relational/Update/Internal/IKeyValueIndex.cs b/src/EFCore.Relational/Update/Internal/IRowKeyValueFactoryFactory.cs similarity index 91% rename from src/EFCore.Relational/Update/Internal/IKeyValueIndex.cs rename to src/EFCore.Relational/Update/Internal/IRowKeyValueFactoryFactory.cs index 4005109ef2e..c3c27cf9c63 100644 --- a/src/EFCore.Relational/Update/Internal/IKeyValueIndex.cs +++ b/src/EFCore.Relational/Update/Internal/IRowKeyValueFactoryFactory.cs @@ -9,7 +9,7 @@ namespace Microsoft.EntityFrameworkCore.Update.Internal; /// 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 interface IKeyValueIndex +public interface IRowKeyValueFactoryFactory { /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -17,5 +17,5 @@ public interface IKeyValueIndex /// 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. /// - IKeyValueIndex WithOriginalValuesFlag(); + IRowKeyValueFactory Create(IUniqueConstraint key); } diff --git a/src/EFCore.Relational/Update/Internal/IRowKeyValueFactory`.cs b/src/EFCore.Relational/Update/Internal/IRowKeyValueFactory`.cs new file mode 100644 index 00000000000..41308a5d09c --- /dev/null +++ b/src/EFCore.Relational/Update/Internal/IRowKeyValueFactory`.cs @@ -0,0 +1,45 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.EntityFrameworkCore.Update.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 interface IRowKeyValueFactory : IRowKeyValueFactory +{ + /// + /// 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. + /// + TKey CreateKeyValue(object[] keyValues); + + /// + /// 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. + /// + TKey CreateKeyValue(IDictionary keyPropertyValues, IEntityType 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. + /// + TKey CreateKeyValue(IModificationCommand command, bool fromOriginalValues = false); + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + IEqualityComparer EqualityComparer { get; } +} diff --git a/src/EFCore.Relational/Update/Internal/KeyValueIndex.cs b/src/EFCore.Relational/Update/Internal/KeyValueIndex.cs index 079208fca14..c18233c5b32 100644 --- a/src/EFCore.Relational/Update/Internal/KeyValueIndex.cs +++ b/src/EFCore.Relational/Update/Internal/KeyValueIndex.cs @@ -9,12 +9,11 @@ namespace Microsoft.EntityFrameworkCore.Update.Internal; /// 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 KeyValueIndex : IKeyValueIndex +public sealed class KeyValueIndex { - private readonly IForeignKey? _foreignKey; + private readonly object _metadata; private readonly TKey _keyValue; private readonly IEqualityComparer _keyComparer; - private readonly bool _fromOriginalValues; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -23,29 +22,17 @@ public sealed class KeyValueIndex : IKeyValueIndex /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public KeyValueIndex( - IForeignKey? foreignKey, + object metadata, TKey keyValue, - IEqualityComparer keyComparer, - bool fromOriginalValues) + IEqualityComparer keyComparer) { - _foreignKey = foreignKey; + _metadata = metadata; _keyValue = keyValue; - _fromOriginalValues = fromOriginalValues; _keyComparer = keyComparer; } - /// - /// 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 IKeyValueIndex WithOriginalValuesFlag() - => new KeyValueIndex(_foreignKey, _keyValue, _keyComparer, fromOriginalValues: true); - private bool Equals(KeyValueIndex other) - => other._fromOriginalValues == _fromOriginalValues - && other._foreignKey == _foreignKey + => other._metadata == _metadata && _keyComparer.Equals(_keyValue, other._keyValue); /// @@ -55,10 +42,8 @@ private bool Equals(KeyValueIndex other) /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public override bool Equals(object? obj) - => !(obj is null) - && (ReferenceEquals(this, obj) - || obj.GetType() == GetType() - && Equals((KeyValueIndex)obj)); + => ReferenceEquals(this, obj) + || (obj is KeyValueIndex other && Equals(other)); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -69,9 +54,7 @@ public override bool Equals(object? obj) public override int GetHashCode() { var hash = new HashCode(); - hash.Add(typeof(TKey)); - hash.Add(_fromOriginalValues); - hash.Add(_foreignKey); + hash.Add(_metadata); hash.Add(_keyValue, _keyComparer); return hash.ToHashCode(); } diff --git a/src/EFCore.Relational/Update/Internal/RowForeignKeyValueFactory.cs b/src/EFCore.Relational/Update/Internal/RowForeignKeyValueFactory.cs new file mode 100644 index 00000000000..71c6cf3b8f1 --- /dev/null +++ b/src/EFCore.Relational/Update/Internal/RowForeignKeyValueFactory.cs @@ -0,0 +1,469 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.EntityFrameworkCore.Metadata.Internal; + +namespace Microsoft.EntityFrameworkCore.Update.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 RowForeignKeyValueFactory : IRowForeignKeyValueFactory +{ + private readonly IForeignKeyConstraint _foreignKey; + private readonly IRowKeyValueFactory _principalKeyValueFactory; + private readonly IRowForeignKeyValueFactory _rowDependentKeyValueFactory; + + /// + /// 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 RowForeignKeyValueFactory(IForeignKeyConstraint foreignKey) + { + _foreignKey = foreignKey; + _principalKeyValueFactory = ((UniqueConstraint)foreignKey.PrincipalUniqueConstraint).GetRowKeyValueFactory(); + _rowDependentKeyValueFactory = CreateRowDependentKeyValueFactory(foreignKey); + } + + /// + /// 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 object CreatePrincipalKeyValueIndex(IModificationCommand command, bool fromOriginalValues = false) + => new KeyValueIndex( + _foreignKey, + _principalKeyValueFactory.CreateKeyValue(command, fromOriginalValues), + _principalKeyValueFactory.EqualityComparer, + fromOriginalValues); + + /// + /// 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 object? CreateDependentKeyValueIndex(IModificationCommand command, bool fromOriginalValues = false) + => ((ForeignKeyConstraint)_foreignKey).GetRowForeignKeyValueFactory()..TryCreateFromCurrentValues(entry, out var keyValue) + ? new KeyValueIndex(foreignKey, keyValue, _principalKeyValueFactory.EqualityComparer, fromOriginalValues: false) + : 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 IKeyValueIndex? CreateDependentKeyValueFromOriginalValues(IUpdateEntry entry, IForeignKey foreignKey) + => foreignKey.GetDependentKeyValueFactory()!.TryCreateFromOriginalValues(entry, out var keyValue) + ? new KeyValueIndex(foreignKey, keyValue, _principalKeyValueFactory.EqualityComparer, fromOriginalValues: true) + : null; + + private IRowForeignKeyValueFactory CreateRowDependentKeyValueFactory(IForeignKeyConstraint foreignKey) + => foreignKey.Columns.Count == 1 + ? CreateSimple(foreignKey) + : new CompositeValueFactory(foreignKey.Columns); + + private IDependentKeyValueFactory CreateSimple(IForeignKeyConstraint foreignKey) + { + var dependentProperty = foreignKey.Properties.Single(); + var principalType = foreignKey.PrincipalKey.Properties.Single().ClrType; + var propertyAccessors = dependentProperty.GetPropertyAccessors(); + + if (dependentProperty.ClrType.IsNullableType() + && principalType.IsNullableType()) + { + return new SimpleFullyNullableDependentKeyValueFactory(dependentProperty, propertyAccessors); + } + + if (dependentProperty.ClrType.IsNullableType()) + { + return (IDependentKeyValueFactory)Activator.CreateInstance( + typeof(SimpleNullableDependentKeyValueFactory), dependentProperty, propertyAccessors)!; + } + + return principalType.IsNullableType() + ? (IDependentKeyValueFactory)Activator.CreateInstance( + typeof(SimpleNullablePrincipalDependentKeyValueFactory<>).MakeGenericType( + typeof(TKey).UnwrapNullableType()), dependentProperty, propertyAccessors)! + : new SimpleNonNullableDependentKeyValueFactory(dependentProperty, propertyAccessors); + } + + private class SimpleFullyNullableDependentKeyValueFactory : IRowForeignKeyValueFactory + { + private readonly PropertyAccessors _propertyAccessors; + + /// + /// 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 SimpleFullyNullableDependentKeyValueFactory( + IProperty property, + PropertyAccessors propertyAccessors) + { + _propertyAccessors = propertyAccessors; + EqualityComparer = property.CreateKeyEqualityComparer(); + } + + /// + /// 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 IEqualityComparer EqualityComparer { get; } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual bool TryCreateFromBuffer(in ValueBuffer valueBuffer, [NotNullWhen(true)] out TKey? key) + { + key = (TKey)_propertyAccessors.ValueBufferGetter!(valueBuffer); + return key != 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 TryCreateFromCurrentValues(IUpdateEntry entry, [NotNullWhen(true)] out TKey? key) + { + key = ((Func)_propertyAccessors.CurrentValueGetter)(entry); + return key != 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 TryCreateFromPreStoreGeneratedCurrentValues(IUpdateEntry entry, [NotNullWhen(true)] out TKey? key) + { + key = ((Func)_propertyAccessors.PreStoreGeneratedCurrentValueGetter)(entry); + return key != 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 TryCreateFromOriginalValues(IUpdateEntry entry, [NotNullWhen(true)] out TKey? key) + { + key = ((Func)_propertyAccessors.OriginalValueGetter!)(entry); + return key != 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 TryCreateFromRelationshipSnapshot(IUpdateEntry entry, [NotNullWhen(true)] out TKey? key) + { + key = ((Func)_propertyAccessors.RelationshipSnapshotGetter)(entry); + return key != null; + } + } + + private class SimpleNullableDependentKeyValueFactory : IRowForeignKeyValueFactory + where TKey : struct + { + private readonly PropertyAccessors _propertyAccessors; + + /// + /// 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 SimpleNullableDependentKeyValueFactory( + IProperty property, + PropertyAccessors propertyAccessors) + { + _propertyAccessors = propertyAccessors; + EqualityComparer = property.CreateKeyEqualityComparer(); + } + + /// + /// 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 IEqualityComparer EqualityComparer { get; } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual bool TryCreateFromBuffer(in ValueBuffer valueBuffer, out TKey key) + { + var value = _propertyAccessors.ValueBufferGetter!(valueBuffer); + if (value == null) + { + key = default; + return false; + } + + key = (TKey)value; + return true; + } + + /// + /// 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 TryCreateFromCurrentValues(IUpdateEntry entry, out TKey key) + => HandleNullableValue(((Func)_propertyAccessors.CurrentValueGetter)(entry), out key); + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual bool TryCreateFromPreStoreGeneratedCurrentValues(IUpdateEntry entry, out TKey key) + => HandleNullableValue( + ((Func)_propertyAccessors.PreStoreGeneratedCurrentValueGetter)(entry), out key); + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual bool TryCreateFromOriginalValues(IUpdateEntry entry, out TKey key) + => HandleNullableValue(((Func)_propertyAccessors.OriginalValueGetter!)(entry), out key); + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual bool TryCreateFromRelationshipSnapshot(IUpdateEntry entry, out TKey key) + => HandleNullableValue(((Func)_propertyAccessors.RelationshipSnapshotGetter)(entry), out key); + + private static bool HandleNullableValue(TKey? value, out TKey key) + { + if (value.HasValue) + { + key = (TKey)value; + return true; + } + + key = default; + return false; + } + } + + private class SimpleNullablePrincipalDependentKeyValueFactory : IRowForeignKeyValueFactory + where TNonNullableKey : struct + { + private readonly PropertyAccessors _propertyAccessors; + + /// + /// 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 SimpleNullablePrincipalDependentKeyValueFactory( + IProperty property, + PropertyAccessors propertyAccessors) + { + _propertyAccessors = propertyAccessors; + EqualityComparer = property.CreateKeyEqualityComparer(); + } + + /// + /// 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 IEqualityComparer EqualityComparer { get; } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual bool TryCreateFromBuffer(in ValueBuffer valueBuffer, [NotNullWhen(true)] out TKey? key) + { + var value = _propertyAccessors.ValueBufferGetter!(valueBuffer); + if (value == null) + { + key = default; + return false; + } + + key = (TKey)value; + return true; + } + + /// + /// 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 TryCreateFromCurrentValues(IUpdateEntry entry, [NotNullWhen(true)] out TKey? key) + { + key = (TKey)(object)((Func)_propertyAccessors.CurrentValueGetter)(entry)!; + return true; + } + + /// + /// 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 TryCreateFromPreStoreGeneratedCurrentValues(IUpdateEntry entry, [NotNullWhen(true)] out TKey? key) + { + key = (TKey)(object)((Func)_propertyAccessors.PreStoreGeneratedCurrentValueGetter)(entry)!; + return true; + } + + /// + /// 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 TryCreateFromOriginalValues(IUpdateEntry entry, [NotNullWhen(true)] out TKey? key) + { + key = (TKey)(object)((Func)_propertyAccessors.OriginalValueGetter!)(entry)!; + return true; + } + + /// + /// 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 TryCreateFromRelationshipSnapshot(IUpdateEntry entry, [NotNullWhen(true)] out TKey? key) + { + key = (TKey)(object)((Func)_propertyAccessors.RelationshipSnapshotGetter)(entry)!; + return true; + } + } + + private class SimpleNonNullableDependentKeyValueFactory : IRowForeignKeyValueFactory + { + private readonly PropertyAccessors _propertyAccessors; + + /// + /// 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 SimpleNonNullableDependentKeyValueFactory( + IProperty property, + PropertyAccessors propertyAccessors) + { + _propertyAccessors = propertyAccessors; + EqualityComparer = property.CreateKeyEqualityComparer(); + } + + /// + /// 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 IEqualityComparer EqualityComparer { get; } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual bool TryCreateFromBuffer(in ValueBuffer valueBuffer, [NotNullWhen(true)] out TKey? key) + { + var value = _propertyAccessors.ValueBufferGetter!(valueBuffer); + if (value == null) + { + key = default; + return false; + } + + key = (TKey)value; + return true; + } + + /// + /// 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 TryCreateFromCurrentValues(IUpdateEntry entry, [NotNullWhen(true)] out TKey? key) + { + key = ((Func)_propertyAccessors.CurrentValueGetter)(entry)!; + return true; + } + + /// + /// 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 TryCreateFromPreStoreGeneratedCurrentValues(IUpdateEntry entry, [NotNullWhen(true)] out TKey? key) + { + key = ((Func)_propertyAccessors.PreStoreGeneratedCurrentValueGetter)(entry)!; + return true; + } + + /// + /// 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 TryCreateFromOriginalValues(IUpdateEntry entry, [NotNullWhen(true)] out TKey? key) + { + key = ((Func)_propertyAccessors.OriginalValueGetter!)(entry)!; + return true; + } + + /// + /// 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 TryCreateFromRelationshipSnapshot(IUpdateEntry entry, [NotNullWhen(true)] out TKey? key) + { + key = ((Func)_propertyAccessors.RelationshipSnapshotGetter)(entry)!; + return true; + } + } +} diff --git a/src/EFCore.Relational/Update/Internal/RowForeignKeyValueFactoryFactory.cs b/src/EFCore.Relational/Update/Internal/RowForeignKeyValueFactoryFactory.cs new file mode 100644 index 00000000000..f8c05f833b6 --- /dev/null +++ b/src/EFCore.Relational/Update/Internal/RowForeignKeyValueFactoryFactory.cs @@ -0,0 +1,35 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Metadata.Internal; + +namespace Microsoft.EntityFrameworkCore.Update.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 RowForeignKeyValueFactoryFactory : IRowForeignKeyValueFactoryFactory +{ + /// + /// 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 IRowForeignKeyValueFactory Create(IForeignKeyConstraint foreignKey) + => (IRowForeignKeyValueFactory)_createMethod + .MakeGenericMethod(((UniqueConstraint)foreignKey.PrincipalUniqueConstraint).GetKeyType()) + .Invoke(null, new object[] { foreignKey })!; + + private readonly static MethodInfo _createMethod = typeof(RowForeignKeyValueFactoryFactory).GetTypeInfo() + .GetDeclaredMethod(nameof(CreateFactory))!; + + [UsedImplicitly] + private static IRowForeignKeyValueFactory CreateFactory(IForeignKeyConstraint foreignKey) + where TKey : notnull + => new RowForeignKeyValueFactory(foreignKey); +} diff --git a/src/EFCore.Relational/Update/Internal/RowIdentityMap.cs b/src/EFCore.Relational/Update/Internal/RowIdentityMap.cs new file mode 100644 index 00000000000..d2eb9f58dab --- /dev/null +++ b/src/EFCore.Relational/Update/Internal/RowIdentityMap.cs @@ -0,0 +1,137 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.EntityFrameworkCore.Metadata.Internal; + +namespace Microsoft.EntityFrameworkCore.Update.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 RowIdentityMap : IRowIdentityMap + where TKey : notnull +{ + private readonly bool _sensitiveLoggingEnabled; + private readonly IUniqueConstraint _key; + private readonly Dictionary _identityMap; + private readonly IRowKeyValueFactory _principalKeyValueFactory; + + /// + /// 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 RowIdentityMap( + IUniqueConstraint key, + bool sensitiveLoggingEnabled) + { + _sensitiveLoggingEnabled = sensitiveLoggingEnabled; + _key = key; + _principalKeyValueFactory = ((UniqueConstraint)_key).GetRowKeyValueFactory(); + _identityMap = new Dictionary(_principalKeyValueFactory.EqualityComparer); + } + + /// + /// 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 IModificationCommand? TryGetEntry(object?[] keyValues) + { + var key = _principalKeyValueFactory.CreateKeyValue(keyValues); + return key != null && _identityMap.TryGetValue(key, out var entry) ? entry : 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 IModificationCommand? TryGetEntry(IDictionary keyPropertyValues, IEntityType entityType) + { + var key = _principalKeyValueFactory.CreateKeyValue(keyPropertyValues, entityType); + return key != null && _identityMap.TryGetValue(key, out var entry) ? entry : 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 void Add(IModificationCommand entry) + => Add(_principalKeyValueFactory.CreateKeyValue(entry), entry); + + /// + /// 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. + /// + protected virtual void Add(TKey key, IModificationCommand entry) + => Add(key, entry, updateDuplicate: false); + + private void Add(TKey key, IModificationCommand entry, bool updateDuplicate) + { + if (_identityMap.TryGetValue(key, out var existingEntry)) + { + if (!updateDuplicate) + { + if (existingEntry == entry) + { + return; + } + + ThrowIdentityConflict(entry); + } + } + + _identityMap[key] = entry; + } + + private void ThrowIdentityConflict(IModificationCommand entry) + { + //if (_sensitiveLoggingEnabled) + //{ + // throw new InvalidOperationException( + // CoreStrings.IdentityConflictSensitive( + // entry.EntityType.DisplayName(), + // entry.BuildCurrentValuesString(Key.Columns))); + //} + + //throw new InvalidOperationException( + // CoreStrings.IdentityConflict( + // entry.EntityType.DisplayName(), + // Key.Columns.Format())); + } + + /// + /// 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 Remove(IModificationCommand entry) + => Remove(_principalKeyValueFactory.CreateKeyValue(entry), entry); + + /// + /// 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. + /// + protected virtual void Remove(TKey key, IModificationCommand entry) + { + if (_identityMap.TryGetValue(key, out var existingEntry) + && existingEntry == entry) + { + _identityMap.Remove(key); + } + } +} diff --git a/src/EFCore.Relational/Update/Internal/RowKeyValueFactoryFactory.cs b/src/EFCore.Relational/Update/Internal/RowKeyValueFactoryFactory.cs new file mode 100644 index 00000000000..355cb064d63 --- /dev/null +++ b/src/EFCore.Relational/Update/Internal/RowKeyValueFactoryFactory.cs @@ -0,0 +1,278 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Metadata.Internal; + +namespace Microsoft.EntityFrameworkCore.Update.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 RowKeyValueFactoryFactory : IRowKeyValueFactoryFactory +{ + /// + /// 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 IRowKeyValueFactory Create(IUniqueConstraint key) + => (IRowKeyValueFactory)_createMethod + .MakeGenericMethod(((UniqueConstraint)key).GetKeyType()) + .Invoke(null, new object[] { key })!; + + private readonly static MethodInfo _createMethod = typeof(RowKeyValueFactoryFactory).GetTypeInfo() + .GetDeclaredMethod(nameof(CreateFactory))!; + + [UsedImplicitly] + private static IRowKeyValueFactory CreateFactory(IUniqueConstraint key) + => key.Columns.Count == 1 + ? new SimpleRowKeyValueFactory(key.Columns.Single()) + : new CompositeRowKeyValueFactory(key); + + private class SimpleRowKeyValueFactory : IRowKeyValueFactory + { + private readonly IColumn _column; + + /// + /// 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 SimpleRowKeyValueFactory(IColumn column) + { + _column = column; + + var comparer = column.PropertyMappings.First().TypeMapping.ProviderComparer; + + EqualityComparer + = comparer != null + ? new NoNullsCustomEqualityComparer(comparer) + : typeof(IStructuralEquatable).IsAssignableFrom(typeof(TKey)) + ? new NoNullsStructuralEqualityComparer() + : EqualityComparer.Default; + } + + /// + /// 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 object? CreateFromKeyValues(object?[] keyValues) + => keyValues[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 object? CreateFromBuffer(ValueBuffer valueBuffer) + => _propertyAccessors.ValueBufferGetter!(valueBuffer); + + /// + /// 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 IProperty FindNullPropertyInKeyValues(object?[] keyValues) + => _column; + + /// + /// 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 TKey CreateFromCurrentValues(IUpdateEntry entry) + => ((Func)_propertyAccessors.CurrentValueGetter)(entry); + + /// + /// 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 IProperty FindNullPropertyInCurrentValues(IUpdateEntry entry) + => _column; + + /// + /// 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 TKey CreateFromOriginalValues(IUpdateEntry entry) + => ((Func)_propertyAccessors.OriginalValueGetter!)(entry); + + /// + /// 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 TKey CreateFromRelationshipSnapshot(IUpdateEntry entry) + => ((Func)_propertyAccessors.RelationshipSnapshotGetter)(entry); + + /// + /// 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 IEqualityComparer EqualityComparer { get; } + + private sealed class NoNullsStructuralEqualityComparer : IEqualityComparer + { + private readonly IEqualityComparer _comparer + = StructuralComparisons.StructuralEqualityComparer; + + public bool Equals(TKey? x, TKey? y) + => _comparer.Equals(x, y); + + public int GetHashCode([DisallowNull] TKey obj) + => _comparer.GetHashCode(obj); + } + + private sealed class NoNullsCustomEqualityComparer : IEqualityComparer + { + private readonly Func _equals; + private readonly Func _hashCode; + + public NoNullsCustomEqualityComparer(ValueComparer comparer) + { + if (comparer.Type != typeof(TKey) + && comparer.Type == typeof(TKey).UnwrapNullableType()) + { + comparer = comparer.ToNonNullNullableComparer(); + } + + _equals = (Func)comparer.EqualsExpression.Compile(); + _hashCode = (Func)comparer.HashCodeExpression.Compile(); + } + + public bool Equals(TKey? x, TKey? y) + => _equals(x, y); + + public int GetHashCode([DisallowNull] TKey obj) + => _hashCode(obj); + } + } + + private class CompositeRowKeyValueFactory : CompositeRowValueFactory, IRowKeyValueFactory + { + /// + /// 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 CompositeRowKeyValueFactory(IUniqueConstraint key) + : base(key.Columns) + { + } + + /// + /// 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 object? CreateFromKeyValues(object?[] keyValues) + => keyValues.Any(v => v == null) ? null : keyValues; + + /// + /// 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 object? CreateFromBuffer(ValueBuffer valueBuffer) + => TryCreateFromBuffer(valueBuffer, out var values) ? values : 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 IProperty FindNullPropertyInKeyValues(object?[] keyValues) + { + var index = -1; + for (var i = 0; i < keyValues.Length; i++) + { + if (keyValues[i] == null) + { + index = i; + break; + } + } + + return Columns[index]; + } + + /// + /// 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 object[] CreateFromCurrentValues(IUpdateEntry entry) + => CreateFromEntry(entry, (e, p) => e.GetCurrentValue(p)); + + /// + /// 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 IProperty? FindNullPropertyInCurrentValues(IUpdateEntry entry) + => Columns.FirstOrDefault(p => entry.GetCurrentValue(p) == 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 object[] CreateFromOriginalValues(IUpdateEntry entry) + => CreateFromEntry(entry, (e, p) => e.GetOriginalValue(p)); + + /// + /// 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 object[] CreateFromRelationshipSnapshot(IUpdateEntry entry) + => CreateFromEntry(entry, (e, p) => e.GetRelationshipSnapshotValue(p)); + + private object[] CreateFromEntry( + IUpdateEntry entry, + Func getValue) + { + var values = new object[Columns.Count]; + var index = 0; + + foreach (var property in Columns) + { + var value = getValue(entry, property); + if (value == null) + { + return default!; + } + + values[index++] = value; + } + + return values; + } + } +} diff --git a/src/EFCore/ChangeTracking/Internal/CompositeValueFactory.cs b/src/EFCore/ChangeTracking/Internal/CompositeValueFactory.cs index a7e6192af5a..bcf384bcfdb 100644 --- a/src/EFCore/ChangeTracking/Internal/CompositeValueFactory.cs +++ b/src/EFCore/ChangeTracking/Internal/CompositeValueFactory.cs @@ -1,7 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Collections; using System.Diagnostics.CodeAnalysis; namespace Microsoft.EntityFrameworkCore.ChangeTracking.Internal; @@ -140,15 +139,7 @@ protected virtual bool TryCreateFromEntry( /// doing so can result in application failures when updating to a new Entity Framework Core release. /// protected static IEqualityComparer CreateEqualityComparer(IReadOnlyList properties) - { - var comparers = properties.Select(p => p.GetKeyValueComparer()).ToList(); - - return comparers.All(c => c != null) - ? new CompositeCustomComparer(comparers) - : properties.Any(p => typeof(IStructuralEquatable).IsAssignableFrom(p.ClrType)) - ? new StructuralCompositeComparer() - : new CompositeComparer(); - } + => new CompositeCustomComparer(properties.Select(p => p.GetKeyValueComparer()).ToList()); private sealed class CompositeCustomComparer : IEqualityComparer { @@ -208,104 +199,4 @@ public int GetHashCode(object[] obj) return hashCode; } } - - private sealed class CompositeComparer : IEqualityComparer - { - public bool Equals(object[]? x, object[]? y) - { - if (ReferenceEquals(x, y)) - { - return true; - } - - if (x is null) - { - return y is null; - } - - if (y is null) - { - return false; - } - - if (x.Length != y.Length) - { - return false; - } - - for (var i = 0; i < x.Length; i++) - { - if (!Equals(x[i], y[i])) - { - return false; - } - } - - return true; - } - - public int GetHashCode(object[] obj) - { - var hash = new HashCode(); - foreach (var value in obj) - { - hash.Add(value); - } - - return hash.ToHashCode(); - } - } - - private sealed class StructuralCompositeComparer : IEqualityComparer - { - private readonly IEqualityComparer _structuralEqualityComparer - = StructuralComparisons.StructuralEqualityComparer; - - public bool Equals(object[]? x, object[]? y) - { - if (ReferenceEquals(x, y)) - { - return true; - } - - if (x is null) - { - return y is null; - } - - if (y is null) - { - return false; - } - - if (x.Length != y.Length) - { - return false; - } - - for (var i = 0; i < x.Length; i++) - { - if (!_structuralEqualityComparer.Equals(x[i], y[i])) - { - return false; - } - } - - return true; - } - - public int GetHashCode(object[] obj) - { - var hashCode = 0; - - // ReSharper disable once ForCanBeConvertedToForeach - // ReSharper disable once LoopCanBeConvertedToQuery - for (var i = 0; i < obj.Length; i++) - { - hashCode = (hashCode * 397) ^ _structuralEqualityComparer.GetHashCode(obj[i]); - } - - return hashCode; - } - } } diff --git a/src/EFCore/ChangeTracking/Internal/SimplePrincipalKeyValueFactory.cs b/src/EFCore/ChangeTracking/Internal/SimplePrincipalKeyValueFactory.cs index 0281204de8e..c890413d708 100644 --- a/src/EFCore/ChangeTracking/Internal/SimplePrincipalKeyValueFactory.cs +++ b/src/EFCore/ChangeTracking/Internal/SimplePrincipalKeyValueFactory.cs @@ -29,14 +29,7 @@ public SimplePrincipalKeyValueFactory(IProperty property) _property = property; _propertyAccessors = _property.GetPropertyAccessors(); - var comparer = property.GetKeyValueComparer(); - - EqualityComparer - = comparer != null - ? new NoNullsCustomEqualityComparer(comparer) - : typeof(IStructuralEquatable).IsAssignableFrom(typeof(TKey)) - ? new NoNullsStructuralEqualityComparer() - : EqualityComparer.Default; + EqualityComparer = new NoNullsCustomEqualityComparer(property.GetKeyValueComparer()); } /// diff --git a/test/EFCore.Relational.Tests/Infrastructure/RelationalModelValidatorTest.cs b/test/EFCore.Relational.Tests/Infrastructure/RelationalModelValidatorTest.cs index 1666ad475ca..2817895160f 100644 --- a/test/EFCore.Relational.Tests/Infrastructure/RelationalModelValidatorTest.cs +++ b/test/EFCore.Relational.Tests/Infrastructure/RelationalModelValidatorTest.cs @@ -429,7 +429,7 @@ public virtual void Warns_on_not_configured_shared_columns_with_shared_table() } [ConditionalFact] - public virtual void Detects_incompatible_shared_columns_with_shared_table() + public virtual void Detects_incompatible_shared_columns_in_shared_table_with_different_data_types() { var modelBuilder = CreateConventionalModelBuilder(); @@ -445,6 +445,23 @@ public virtual void Detects_incompatible_shared_columns_with_shared_table() modelBuilder); } + [ConditionalFact] + public virtual void Detects_incompatible_shared_columns_in_shared_table_with_different_provider_types() + { + var modelBuilder = CreateConventionalModelBuilder(); + + modelBuilder.Entity().HasOne().WithOne(b => b.A).HasForeignKey(a => a.Id).HasPrincipalKey(b => b.Id).IsRequired(); + modelBuilder.Entity().Property(a => a.P0).HasColumnName(nameof(A.P0)).HasColumnType("someInt").HasConversion(); + modelBuilder.Entity().ToTable("Table"); + modelBuilder.Entity().Property(b => b.P0).HasColumnName(nameof(A.P0)).HasColumnType("someInt"); + modelBuilder.Entity().ToTable("Table"); + + VerifyError( + RelationalStrings.DuplicateColumnNameProviderTypeMismatch( + nameof(A), nameof(A.P0), nameof(B), nameof(B.P0), nameof(B.P0), "Table", "long", "int"), + modelBuilder); + } + [ConditionalFact] public virtual void Detects_incompatible_shared_check_constraints_with_shared_table() { diff --git a/test/EFCore.Relational.Tests/Update/CommandBatchPreparerTest.cs b/test/EFCore.Relational.Tests/Update/CommandBatchPreparerTest.cs index cf221e4cac1..965663a9c56 100644 --- a/test/EFCore.Relational.Tests/Update/CommandBatchPreparerTest.cs +++ b/test/EFCore.Relational.Tests/Update/CommandBatchPreparerTest.cs @@ -975,7 +975,7 @@ public ICommandBatchPreparer CreateCommandBatchPreparer( modificationCommandBatchFactory, new ParameterNameGeneratorFactory(new ParameterNameGeneratorDependencies()), new ModificationCommandComparer(), - new KeyValueIndexFactorySource(), + new RowForeignKeyValueFactoryFactory(), new ModificationCommandFactory(), loggingOptions, new FakeDiagnosticsLogger(), diff --git a/test/EFCore.SqlServer.Tests/Infrastructure/SqlServerModelValidatorTest.cs b/test/EFCore.SqlServer.Tests/Infrastructure/SqlServerModelValidatorTest.cs index 4b58e546831..211a2353f82 100644 --- a/test/EFCore.SqlServer.Tests/Infrastructure/SqlServerModelValidatorTest.cs +++ b/test/EFCore.SqlServer.Tests/Infrastructure/SqlServerModelValidatorTest.cs @@ -25,21 +25,6 @@ public override void Detects_duplicate_column_names() modelBuilder); } - public override void Detects_incompatible_shared_columns_with_shared_table() - { - var modelBuilder = CreateConventionalModelBuilder(); - - modelBuilder.Entity().HasOne().WithOne().HasForeignKey(a => a.Id).HasPrincipalKey(b => b.Id).IsRequired(); - modelBuilder.Entity().Property(a => a.P0).HasColumnName(nameof(A.P0)).HasColumnType("someInt"); - modelBuilder.Entity().ToTable("Table"); - modelBuilder.Entity().Property(a => a.P0).HasColumnName(nameof(A.P0)); - modelBuilder.Entity().ToTable("Table"); - - VerifyError( - RelationalStrings.DuplicateColumnNameDataTypeMismatch( - nameof(A), nameof(A.P0), nameof(B), nameof(B.P0), nameof(B.P0), "Table", "someInt", "int"), modelBuilder); - } - public override void Detects_duplicate_columns_in_derived_types_with_different_types() { var modelBuilder = CreateConventionalModelBuilder(); diff --git a/test/EFCore.Sqlite.Tests/Infrastructure/SqliteModelValidatorTest.cs b/test/EFCore.Sqlite.Tests/Infrastructure/SqliteModelValidatorTest.cs index 45cc92f9317..cbcd5ffff04 100644 --- a/test/EFCore.Sqlite.Tests/Infrastructure/SqliteModelValidatorTest.cs +++ b/test/EFCore.Sqlite.Tests/Infrastructure/SqliteModelValidatorTest.cs @@ -50,24 +50,6 @@ public virtual void Detects_duplicate_column_names_within_hierarchy_with_differe nameof(Cat), nameof(Cat.Breed), nameof(Dog), nameof(Dog.Breed), nameof(Cat.Breed), nameof(Animal)), modelBuilder); } - public override void Detects_incompatible_shared_columns_with_shared_table() - { - var modelBuilder = CreateConventionalModelBuilder(); - - modelBuilder.Entity().HasOne().WithOne().HasForeignKey(a => a.Id).HasPrincipalKey(b => b.Id).IsRequired(); - modelBuilder.Entity().Property(a => a.P0).HasColumnName(nameof(A.P0)).HasColumnType("someInt"); - modelBuilder.Entity().ToTable("Table"); - modelBuilder.Entity().Property(a => a.P0).HasColumnName(nameof(A.P0)); - modelBuilder.Entity().ToTable("Table"); - - modelBuilder.Entity().Property(b => b.P0); - modelBuilder.Entity().Property(d => d.P0); - - VerifyError( - RelationalStrings.DuplicateColumnNameDataTypeMismatch( - nameof(A), nameof(A.P0), nameof(B), nameof(B.P0), nameof(B.P0), "Table", "someInt", "INTEGER"), modelBuilder); - } - [ConditionalFact] public void Detects_schemas() {